You are on page 1of 474

O ksice

Teixeira i Pacheco wci dziel si z czytelnikami sw wiedz i dowiadczeniem, uczc jak tworzy si aplikacje bazodanowe -- lokalne, wielowarstwowe i internetowe oraz nowe komponenty, biblioteki DLL itd. Czytajc t ksik, masz niepowtarzaln okazj podnie swoje kwalifikacje, gdy umoliwia ona zapoznanie si m.in. z zasadami tworzenia aplikacji midzyplatformowych, metodologi tworzenia komponentw i ich edytorw, filozofi programowania obiektowego i programowaniem wspbienym. Niniejsza ksika, napisana przez dwukrotnych laureatw nagrody za najlepsz ksik o Delphi, przyznanej przez czytelnikw Delphi Informant, stworzona przez programistw dla programistw, to miarodajny przewodnik po nowociach Delphi 6. Czytajc "Delphi 6. Vademecum profesjonalisty", poznasz midzy innymi: histori Delphi i techniczne oraz ekonomiczne uwarunkowania jego rozwoju, bogactwo rodowiska IDE i wspdziaanie jego elementw w procesie tworzenia aplikacji, najwaniejsze elementy jzyka Object Pascal, szczegy biblioteki CLX i zasady tworzenia aplikacji midzyplatformowych dla Windows i Linuksa, mechanizm komunikatw Windows i zasady ich obsugi w tworzonych aplikacjach, podstawy tworzenia i wykorzystywania bibliotek DLL, realizacj programowania wspbienego za pomoc wtkw i wkien oraz mechanizmw synchronizujcych, zastosowanie nowych komponentw bazodanowych dbExpress i dbGo for ADO.

Dedykacje
Ksik t dedykujemy ofiarom i bohaterom tragedii 11 wrzenia 2001 roku.

Z podzikowaniami dla mojej rodziny Helen, Coopera i Ryana. Bez ich wsparcia i yczliwoci nie ukoczybym nigdy tej ksiki prdzej chyba bym oszala. Steve Podzikowania dla mojej rodziny Anny, Amandy i Zacharego. Serdeczne dziki za wsparcie, mio i cierpliwo. Xavier

Podzikowania
Dzikujemy wszystkim tym, bez pomocy ktrych niniejsza ksika nie mogaby si ukaza. Jednoczenie informujemy, i odpowiedzialni jestemy osobicie za wszelkie tkwice w niej bdy i uchybienia. Przede wszystkim dzikujemy naszym wspautorom, bez ktrych wiedzy i dowiadczenia niniejszy przewodnik nie byby z pewnoci tak dobry. Ray Konopka (Mr. Component) jest autorem rozdziau 13. powiconego komponentom CLX; byskotliwy rozdzia 21. o technologii DataSnap jest dzieem guru Dana Misera; David Sampson, niekwestionowany ekspert od technologii CORBA, dzieli si swoj wiedz z Czytelnikami w rozdziale 191. Owocem talentu Roberta Dra Boba Swarta jest rozdzia 22. opisujcy tworzenie aplikacji ASP, za magik od Internetu Nick Hodges wyczarowa rozdzia 23. traktujcy o aplikacjach wykorzystujcych technologi WebSnap. Kolejna porcja podzikowa dla redaktorw technicznych Thomasa Theobalda i Johna Thomasa oraz ich wsppracownikw. Mimo absorbujcej pracy nad nowym oprogramowaniem, znaleli czas na zweryfikowanie zawartoci niniejszej ksiki. Podczas pisania naszego przewodnika nie szczdzili nam cennych rad i wskazwek nasi przyjaciele i wsppracownicy, midzy innymi (w kolejnoci alfabetycznej) Alain Lino Tadros, Anders Hejlsberg, Anders Ohlsson, Charlie Calvert, Victor Hornback, Chuck Jazdzewski, Daniel Polistchuck, Danny Thorpe, David Streever, Ellie Peters, Jeff Peters, Lance Bullock, Mark Duncan, Mike Dugan, Nick Hodges, Paul Qualls, Ruch Jones, Roland Bouchereau, Scott Frolich, Steve Beebe, Tom Butt i wielu innych, ktrych nie wymienilimy tu z powodu szczupoci miejsca. Na koniec, wielkie dziki dla Pearson Technology Group: Carola Ackermana, Christiny Smith, Dana Scherfa i wielu innych, ktrych nawet nie mielimy przyjemnoci pozna, a bez udziau ktrych ksika niniejsza nie mogaby sta si faktem.

Rozdzia 14. i rozdziay nastpne te znajd si oczywicie w drugim tomie niniejszej ksiki (przyp. red. wyd. pol.)

Autorzy
Steve Teixeira jest dyrektorem ds. Core Technology w, wiodcej na rynku zabezpiecze internetowych, firmie Zone Labs; poprzednio peni funkcj dyrektora technicznego w firmie ThinSpace, zajmujcej si aplikacjami dla telefonii bezprzewodowej. Wsppracowa rwnie z firm Full Moon Interactive, tworzc aplikacje dla ebiznesu. Jako badacz i projektant w firmie Borland odegra jedn z czoowych rl w powstaniu Delphi i C++Buildera. Naley do grupy najbardziej poczytnych autorw zajmujcych si informatyk jego cztery ksiki otrzymay najwysze wyrnienia czytelnikw; publikuje take na amach wielu czasopism powiconych programowaniu. Jego artykuy i ksiki zostay przetumaczone na kilkanacie jzykw. Jest uczestnikiem wielu spotka i konferencji przemysowych na caym wiecie. Xavier Pacheco jest prezesem i gwnym inynierem Xapware Technologies firmy konsultingowoprojektowej specjalizujcej si w tematyce accelerating visions. Jest aktywnym uczestnikiem wielu konferencji przemysowych, publikuje te na amach wielu czasopism powiconych Delphi. Jest niekwestionowanym ekspertem w dziedzinie Delphi o sawie midzynarodowej i czonkiem TeamB grupy wsppracownikwochotnikw Borlanda. Jest autorem ksiek z zakresu informatyki; cztery z nich otrzymay najwysze wyrnienia czytelnikw. Rwnie jego ksiki doczekay si wielojzycznych przekadw. Mieszka w Colorado Springs z on Ann i dwjk dzieci Amand i Zacharym. Bob Swart (znany take jako Dr. Bob www.drbob42.com) jest czonkiem UK Borland Connection, a take autorem, instruktorem i konsultantem w zakresie Delphi, Kylixa i C++Buildera. Mieszka w Helmond (Holandia). Stale wsppracuje z pismami The Delphi Magazine, Delphi Developer, UK-BUG Developers Magazine; uczestniczy take w dyskusjach online w ramach DevX, TechRepublic i Borland Community Web. Jest wspautorem ksiek The Revolutionary Guide to Delphi 2, Delphi 4 Unleashed, C++Builder 4 Unleashed, C++Builder 5 Developers Guide, Kylix Developers Guide i oczywicie niniejszej ksiki. Uczestniczy czynnie w seminariach powiconych Delphi i Kylixowi na caym wiecie, jest take autorem kursu instruktaowego Dr.Bobs Delphi Clinics. W czasie wolnym lubi oglda filmy najchtniej Star Trek Voyager i Deep Space Nine w towarzystwie 7-letniego syna Erika Marka Pascala i 5-letniej crki Natashy Louise Delphine. Dan Miser jest dyrektorem ds. bada i rozwoju w borlandowskiej grupie DSP, gdzie spdza wikszo swego czasu na badaniu i rozpoznawaniu nowych technologii. Dziaa rwnie w grupie badawczo-rozwojowej Delphi, gdzie odpowiedzialny jest za rozwj oprogramowania zwizanego z technologi DataSnap. Celem jego bada jest znalezienie sposobw wymiany informacji pomidzy rnymi systemami i rodowiskami std zainteresowanie rnymi technologiami przetwarzania rozproszonego, jak MIDAS, SOAP, DCOM, RMI, J2EE, EJB, Struts i RDS. Ma rwnie niemae zasugi w popularyzowaniu Delphi, m.in. jako wspautor serii Delphi Developers Guide. Jest autorem wielu artykuw w czasopismach informatycznych, czonkiem borlandowskiej grupy TeamB i lektorem na dorocznych konferencjach Borlanda.

David Sampson jest inynierem ds. badawczo-rozwojowych w borlandowskiej grupie RAD Tools Group, gdzie odpowiedzialny jest za integracj technologii CORBA z produktami typu RAD. Od wielu lat zajmuje si rozwojem Pascala, Delphi i C++, jest rwnie czynnym uczestnikiem deweloperskich konferencji Borlanda. Mieszka ze swoj on w Roswell (Georgia); pasjonuje si hokejem oraz aikido, pomaga take swojej onie opiekowa si sfor psw rasy Basenji.

Nick Hodges jest starszym inynierem ds. rozwojowych w firmie Lemanix Corporation w St. Paul w Minnesocie. Jest czonkiem borlandowskiej grupy TeamB, od wielu lat zajmuje si rozwojem Pascala i Delphi. Jest doradc w ramach Borland Conference Advisory Board, uczestniczy take (jako autor) w grupach dyskusyjnych Borlanda oraz jako lektor na borlandowskich konferencjach. Mieszka w St. Paul z on i dwjk dzieci, lubi czyta ksiki, biega, pomaga take onie w edukacji swych dzieci.

Ray Konopka jest zaoycielem firmy Raize Software Inc. i gwnym architektem CodeSite i Raize Components. Jest autorem niezwykle popularnej ksiki Developing Custom Delphi Components oraz znanej kolumny Delphi by Design na amach Visual Developer Magazine. Specjalizuje si w tworzeniu nowych komponentw i projektowaniu interfejsw uytkownika, jest take lektorem na konferencjach Borlanda na caym wiecie.

13

Przedmowa do wydania oryginalnego


Delphi 6 dwa lata pracy, cae ycie korzyci. Mam wielkie szczcie pracowa w Borlandzie od ponad 16 lat latem 1985 roku przyszedem tutaj, aby: 1) wspuczestniczy w tworzeniu programw narzdziowych nowej generacji (UCSD Pascal i zwizane z nim narzdzia wywoywane z wiersza polece staway si powoli przestarzae), 2) pomc w usprawnieniu procesu tworzenia oprogramowania (by moe kosztem czasu powiconego dla rodziny i przyjaci) oraz 3) przyczyni si do wzbogacenia warsztatu programistw (a wic rwnie mojego). Mio jest pracowa w zespole zajmujcym si od 18 lat ulepszaniem technologii programistycznych. Podobnie jak Turbo Pascal w 1983 roku, Delphi radykalnie zmienio oblicze programowania jedenacie lat pniej. Programowanie zorientowane obiektowo, programowanie dla Windows i programowanie obsugi baz danych stao si nagle o wiele atwiejsze ni dotychczas; kolejne wersje Delphi przyniosy ze sob dalsze uatwienia w zakresie tworzenia aplikacji rozproszonych i aplikacji internetowych. Mimo i przez lata wyposaalimy nasze produkty w rnorodne funkcje, mimo i napisalimy megabajty pomocy kontekstowej, wci wiele rzeczy pozostaje niedopowiedzianych i wielu jeszcze rad i wskazwek mona by udzieli programistom, by tworzone przez nich projekty byy lepsze. Czy wic entuzjastyczne przyjcie Delphi 5 nie byo w peni zasuone? Czy Delphi 5 nie przyczynio si do uproszczenia procesu tworzenia aplikacji internetowych i rozproszonych aplikacji bazodanowych, wpywajc tym samym na zwikszenie produktywnoci programistw? I co najwaniejsze czy zesp Delphi po raz kolejny bdzie w stanie stawi czoo oczekiwaniom dzisiejszych i jutrzejszych projektantw? Istotnie: autorzy Delphi powicili ponad dwa lata na zbieranie opinii uytkownikw Delphi, na obserwowanie, w jaki sposb faktycznie wykorzystuj oni dostpne narzdzia do tworzenia aplikacji, wreszcie na analizowanie problemw, ktre nios ze sob technologiczne wyzwania nowego tysiclecia. Owocem tych wysikw stay si uatwienia w tworzeniu aplikacji dla e-biznesu, usug sieciowych opartych na XML/SOAP, aplikacji integracyjnych B2b/B2C/P2P, aplikacji midzyplatformowych, rozproszonych aplikacji typu AppServer/EJB, i wielu innych aplikacji dla Windows Me/2000 i Office 2000. Po raz kolejny Steve Teixeira (ksywa T-Rex) i Xavier Pacheco (zwany po prostu X) dali wyraz swemu kunsztownemu rzemiosu, tworzc przewodnik pozwalajcy odkry gbi i poczu wiey oddech programowania w Delphi 6. To moi wieloletni koledzy, wspaniali pracownicy, lektorzy na naszych dorocznych konferencjach no i czonkowie wspaniaej wsplnoty Borlanda. Jestem przekonany, e ich najnowsze dzieo spotka si z przyjciem nie mniej entuzjastycznym ni ich poprzednie monografie powicone Delphi. ycz wielu radoci i oczywicie korzyci z programowania w Delphi 6 i jeszcze wspanialszych projektw.

David Intersimone (David I) Vice President, Developer Relation Borland Software Corporation
david@borland.com

15

Cz I Podstawy programowania

19

Rozdzia 1.

Programowanie w Delphi
W niniejszym rozdziale przedstawimy oglny zarys Delphi jego histori, moliwoci poszczeglnych wersji, przydatno do tworzenia programw dla Windows oraz inne informacje, ktre mog by pomocne kademu projektantowi. Zajmiemy si take kilkoma interesujcymi funkcjami IDE; niektre nie s znane nawet dowiadczonym projektantom. Zadaniem tego rozdziau nie jest jednak nauka tworzenia aplikacji w Delphi od podstaw staralimy si unika powtarzania informacji powszechnie dostpnych w dokumentacji, stawiajc raczej na metodyk wykorzystania istniejcych mechanizmw, wypracowan przez lata naszych dowiadcze z Delphi. Niewtpliwie skorzystaj na tym dowiadczeni projektanci pocztkujcym proponujemy natomiast rozpoczcie lektury od dokumentacji Delphi i stopniowe poznawanie funkcjonowania IDE oraz oglnych zasad tworzenia aplikacji na prostych przykadach; zdobycie niezbdnego dowiadczenia bdzie tylko kwesti czasu.

Rodzina produktw Delphi


Delphi 6 wystpuje w trzech rnych wersjach: Delphi 6 Personal, Delphi 6 Professional i Delphi 6 Enterprise. Kada z tych wersji adresowana jest do innej grupy projektantw, charakteryzujcych si okrelonymi potrzebami. Delphi 6 Personal jest wersj podstawow i najbardziej ograniczon. Zawiera wszystko, co niezbdne do rozpoczcia nauki Delphi i jest idealna dla hobbystw oraz twrcw prostych programw narzdziowych na potrzeby wasne. Techniczne moliwoci tej wersji s nastpujce: optymalizujcy 32-bitowy kompilator Object Pascala, oferujcy wiele nowych i ulepszonych elementw jzyka; biblioteka VCL (Visual Component Library), zawierajca ponad 85 standardowych komponentw; obsuga pakietw, umoliwiajca tworzenie maych rozmiarowo aplikacji i bibliotek komponentw; zintegrowane rodowisko IDE zawierajce edytor, debugger, projektanta formularzy i wiele innych produktywnych narzdzi; rozszerzenia IDE, m.in. wizualne dziedziczenie formularzy (Visual Form Inheritance), hierarchiczny podgld obiektw (Object TreeView), uzupenianie klas (Class Completion), Code Insight itp.

21

pena obsuga Win32 API, wcznie z COM, GDI DirectX, wielowtkowoci i rnymi pakietami SDK (Software Development Kits) Microsoftu i innych wytwrcw.

Licencja zwizana z wersj Personal nie zezwala na komercyjn dystrybucj aplikacji stworzonych za jej pomoc; mog by one wykorzystywane tylko do uytku osobistego. Wersja Delphi 6 Professional adresowana jest do tych profesjonalnych projektantw, dla ktrych zbdne s zaawansowane mechanizmy typowe dla aplikacji korporacyjnych (enterprise level). Zawiera ona wszystkie mechanizmy wersji Personal , a ponadto: ponad 255 standardowych komponentw VCL; ponad 160 komponentw CLX na potrzeby aplikacji midzyplatformowych dla Windows i Linuksa; obsug baz danych m.in. podsystem DataCLX, kontrolki bazodanowe, komponenty i sterowniki midzyplatformowe dbExpress, Active DataX Objects (ADO), Borland Database Engine (BDE) dla kompatybilnoci z wczeniejszymi wersjami Delphi, mechanizm tzw. wirtualnego zbioru danych (virtual dataset) umoliwiajcy integracj rnorodnych typw baz danych z VCL, Database Explorer, repozytorium danych i rodzime komponenty InterBase (InterBase Express); sterowniki InterBase i MySQL dla dbExpress; implementacj architektury DataCLX (poprzednio znanej jako MIDAS) z lokaln maszyn danych MyBase opart na XML-u; kreatory do tworzenia komponentw COM/COM+, takich jak kontrolki ActiveX, formularze aktywne (ActiveForms), serwery automatyzacji, arkusze waciwoci i komponenty transakcyjne; rnorodno narzdzi i komponentw niezalenych wytwrcw, m.in. narzdzia internetowe INDY, komponenty raportujce QuickReport, komponenty prezentacji graficznej TeeChart i komponenty internetowe FastNet firmy NetMasters; serwer bazy danych InterBase 6 z licencj na 5 uytkownikw; narzdzie dystrybucji sieciowej kontrolek ActiveX (Web Deployment); narzdzie do instalacji aplikacji InstallSHIELD MSI Light; interfejs OpenTools API umoliwiajcy samodzieln rozbudow funkcjonalnoci IDE i dostarczajcy interfejsu dla systemu kontroli wersji PVCS; narzdzia i komponenty NetCLX WebBroker umoliwiajce tworzenie internetowych aplikacji midzyplatformowych; kod rdowy bibliotek VCL, CLX, RTL i edytorw waciwoci.

Zgodnie z licencj, aplikacje stworzone za pomoc wersji Professional mog by rozpowszechniane w sposb komercyjny. Delphi 6 Enterprise to wersja najbardziej rozbudowana, przeznaczona dla projektantw tworzcych aplikacje na szczeblu przedsibiorstwa (enterprise level). Oprcz mechanizmw dostpnych w wersjach Personal i Professional zawiera take: ponad 300 standardowych komponentw VCL; obsug technologii BizSnap umoliwiajcej tworzenie aplikacji opartych na jzyku XML oraz usug sieciowych (Web Services); platform projektow WebSnap pozwalajc na integracj XML-a i technik skryptowych z aplikacjami internetowymi; implementacj specyfikacji CORBA na potrzeby tworzenia aplikacji klientw i serwerw, wraz z VisiBrokerem ORB w wersji 4.0x oraz Borland APP Serverem w wersji 4.5; oprogramowanie TeamSource zapewniajce kontrol wersji kodu rdowego na podstawie rnorodnych mechanizmw, m.in. ZIP i PVCS; narzdzia umoliwiajce atw zmian wersji jzykowej i lokalizacj aplikacji;

22

rodzime sterowniki SQLLinks BDE dla serwerw Oracle, MS SQL Server, InterBase, Informix, Sybase i DB2; sterowniki Oracle i DB2 dla dbExpress; zaawansowane narzdzia tworzenia aplikacji opartych na SQL-u, m.in. SQL Explorer, SQL Monitor, SQL Builder i obsuga kolumn abstrakcyjnych typw danych (ADT) w przegldarce tabelarycznej.

Podobnie jak w przypadku wersji Professional, licencja zezwala na komercyjn dystrybucj aplikacji stworzonych za pomoc wersji Enterprise.

Delphi co i dlaczego?
Na czsto zadawane pytania w rodzaju Co czyni Delphi tak dobrym? oraz Dlaczego powinienem raczej wybra Delphi ni ? wypracowalimy przez lata dwie odpowiedzi: krtk i dug. Krtka odpowied brzmi produktywno prostot i szybko tworzenia programw dla Windows za pomoc Delphi naprawd trudno przeceni. Komu ta lakoniczna odpowied nie wystarczy, musi cierpliwie wysucha odpowiedzi dugiej, wymieniajcej pi najwaniejszych cech, ktre decyduj o potdze Delphi oto one: komfort wizualnego projektowania aplikacji, szybko kompilacji kontra efektywno generowanego kodu, moliwoci jzyka programowania w kontekcie jego zoonoci, elastyczno i skalowalno architektury baz danych, wzorce projektowania i uytkowania wymuszone przez struktur rodowiska.

Cechy te zestawilimy w postaci picioramiennego diagramu przedstawionego na rysunku 1.1.

Rysunek 1.1. Graf produktywnoci Delphi Mimo i powysze kryteria wybrane zostay cokolwiek subiektywnie i nie uwzgldniaj kilku istotnych czynnikw, jak np. instalacja i rozpowszechnianie gotowych aplikacji, dokumentacja, czy wykorzystanie produktw niezalenych wytwrcw, s jednak wystarczajco przekonujce. Poniewa kade z przedstawionych kryteriw moe mie rn wag dla rnych projektantw, proponujemy wykonanie prostego eksperymentu: naley na kadej z piciu osi z rysunku 1.1 umieci punkt odlegy od rodka diagramu proporcjonalnie do istotnoci cechy reprezentowanej przez dan o; czc poszczeglne punkty liniami, otrzymamy piciokt im wiksze jego pole, tym wiksza przydatno Delphi dla konkretnego czytelnika.

Komfort wizualnego projektowania aplikacji


rodowisko wizualnego projektowania aplikacji narzdzi typu RAD mona zasadniczo podzieli na trzy wsppracujce ze sob elementy: edytor, debugger i projektant formularzy. I tak, w miar umieszczania na formularzu kolejnych komponentw, Delphi generuje automatycznie zwizany z tym kod rdowy; kod ten mona nastpnie modyfikowa i (lub) uzupenia za pomoc edytora, za po skompletowaniu mona go skompilowa, uruchomi i ledzi za pomoc zintegrowanego debuggera. 23

Edytor Delphi wyposaony jest w kilka uytecznych mechanizmw, z ktrych bodaj najbardziej spektakularnym jest CodeInsight, pozwalajcy zaoszczdzi czas powicony na wpisywanie z klawiatury; mechanizm ten opiera swe dziaanie na informacji uzyskiwanej z kompilatora (w przeciwiestwie do Visual Basica, pobierajcego analogiczn informacj z odpowiedniej biblioteki), co czyni go uytecznym w wielu rnych sytuacjach. Chocia domylne ustawienia edytora Delphi s w peni zadowalajce, wielu uytkownikw preferuje ustawienia charakterystyczne dla Visual Studio jako bardziej konfigurowalne. Zintegrowany debugger ostatnich wersji Delphi czy w sobie cechy Visual Studio z zaawansowanymi mechanizmami w rodzaju zdalnego ledzenia, doczania procesu, ledzenia pakietw i bibliotek DLL pod nadzorem aplikacji wywoujcej, obserwacji wykonywanego kodu maszynowego i rejestrw itp. Komfortu pracy dopenia moliwo ustalania dogodnego ukadu okien (z wykorzystaniem dokowania) i zapisywania tych ustawie w postaci nazwanych schematw. Ze zrozumiaych wzgldw brak jest natomiast w Delphi moliwoci zmiany kodu aplikacji w trakcie jej wykonywania, charakterystycznej dla rodowisk interpretowanych w rodzaju Visual Basica czy niektrych narzdzi Javy w rodowisku wykonujcym rasow kompilacj zaimplementowanie czego takiego z pewnoci byoby niezmiernie trudne. Projektant formularzy jest nieodczn czci rodowisk typu RAD Delphi, Visual Basica, C++Buildera i PowerBuildera. Wczeniejsze, klasyczne rodowiska projektowe, jak Visual C++ czy Borland C++, zorientowane s raczej na edytory dialogowe, te za nie posiadaj zdolnoci tak cisego integrowania z pozostaymi elementami rodowiska jak projektant formularzy. Brak tego ostatniego w wyrany sposb wpywa ujemnie na ogln produktywno caego narzdzia wszak jednym z elementw diagramu na rysunku 1.1 jest wanie projektowanie wizualne. Przez ostatnie lata Delphi i Visual Basic przecigay si w coraz to nowych pomysach ulepszania projektanta formularzy, lecz pod jednym przynajmniej wzgldem Visual Basic nie jest w stanie zdystansowa swego konkurenta. Ot Delphi stworzone zostao od pocztku w architekturze zorientowanej obiektowo, a to pociga za sob niebagatelne konsekwencje praktyczne w postaci zjawiska zwanego wizualnym dziedziczeniem formularzy (Visual Form Inheritance): wszelkie zmiany, wprowadzone do danego formularza, automatycznie odzwierciedlone zostaj we wszystkich dziedziczonych z niego formularzach . W szczeglnoci kady formularz dziedziczy wszelkie cechy swej klasy bazowej klasy TForm.

Szybko kompilacji kontra efektywno generowanego kodu


Wobec powtarzajcych si cyklicznie poszczeglnych etapw tworzenia kodu rdowego edycja, rekompilacja, testowanie, edycja, rekompilacja itd. szczeglnego znaczenia nabiera czas kompilowania kodu. Przy kosztownych czasowo kompilacjach uytkownik zmuszony jest do wprowadzania wikszej liczby poprawek na raz (tj. pomidzy kolejnymi kompilacjami), a to raczej nie przyczynia si do wzrostu produktywnoci. O zaletach efektywnego kodu wynikowego nie trzeba nikogo przekonywa dua szybko wykonywania programu i jego niewielki rozmiar zawsze bd mile widziane. Szybko kompilacji to jedna z najbardziej cenionych cech Delphi posiada ono bodaj najszybszy kompilator jzyka wysokiego poziomu na platformie Windows. Jzyk C++, tradycyjnie (i z koniecznoci) kompilujcy si wolniej od Pascala, w ostatnich wersjach Visual C++ i C++Buildera dowiadczy wielu zabiegw optymalizacyjnych w rodzaju konsolidacji przyrostowej (incremental linking), czy rnych strategii buforowania kodu; mimo to jego kompilatory i tak s rednio kilka razy wolniejsze od kompilatora Delphi. Doceniwszy szybko kompilatora, warto przyjrze si drugiej stronie medalu mianowicie efektywnoci tworzonego kodu wynikowego i zada istotne pytanie: czy ulepszanie jednego z tych czynnikw musi odbywa si kosztem drugiego? Okazuje si, e niekoniecznie: kompilator Delphi korzysta z tego samego zaplecza (back-end), co kompilator C++Buildera, obydwa wic produkuj rwnie efektywny kod wynikowy. Ostatnie wersje Visual C++ zdaj si dorwnywa Delphi pod wzgldem efektywnoci generowanego kodu (w wielu przypadkach kod ten jest nawet efektywniejszy), a to za spraw pewnych wyszukanych optymalizacji. I cho nie ma to wikszego znaczenia dla szeroko rozumianego tworzenia aplikacji, to moe sta si niezmiernie istotne w procesach dokonujcych intensywnych oblicze. W kontekcie oglnie pojtej kompilacji Visual Basic stanowi pewn osobliwo. W czasie projektowania aplikacji bazuje on na kodzie cakowicie interpretowanym, co zapewnia wystarczajc mobilno; kompilator przeksztacajcy gotowy kod do pliku .EXE plasuje si pod wzgldem szybkoci daleko za Delphi i C++, to

24

samo mona powiedzie o generowanym kodzie wynikowym. Ostatnia wersja Visual Basica Visual Basic.NET znajdujca si w fazie beta-testw, wprowadza pewne usprawnienia w tym obszarze. Java to kolejny interesujcy przypadek. Kompilatory narzdzi zbudowanych na jej bazie (JBuilder i Visual J++) dorwnuj szybkoci kompilatorowi Delphi, lecz efektywno generowanego kodu czsto pozostawia wiele do yczenia. Biorc pod uwage fakt, e Java jest jzykiem interpretowanym trudno liczy na zmian tego stanu rzeczy.

Moliwoci jzyka programowania w kontekcie jego zoonoci


Kady jzyk programowania oceniany jest gwnie w aspekcie swych moliwoci czyli popularnie rozumianej mocy oraz zoonoci, przekadajcej si przede wszystkim na atwo jego nauczenia si, prostot uywania i podatno na bdy. Nie trzeba dodawa, i kryteria te maj charakter na wskro subiektywny to, co dla pewnych osb jest dziecinnie atwe, dla innych moe by barier nie do pokonania; co, co wydaje si komu nieuzasadnionym ograniczeniem, dla kogo innego moe wrcz nosi znamiona elegancji. Nic wic dziwnego, i kwestia oceny danego jzyka programowania jest zwykle przedmiotem niekoczcych si sporw na grupach dyskusyjnych (i nie tylko), take ponisze konkluzje stanowi subiektywny punkt widzenia autorw. Jzykiem o zdecydowanie najwikszych moliwociach jest jzyk asemblera gdy istnieje moliwo bezporedniego operowania rozkazami kodu maszynowego, mona zaprogramowa dosownie wszystko, co teoretycznie da si zaprogramowa. Owa niemal nieograniczono okupiona jest jednak ogromn zoonoci, podatnoci na bdy i znacznymi utrudnieniami w pracy zespoowej. Trudno wyobrazi sobie nawet redniej wielkoci program dla Windows napisany cakowicie w asemblerze w miar wdrwki od jednego programisty do drugiego, zmieniajcych si koncepcji i pojawiajcych si nowych idei, tekst programu zdaje si mie coraz mniej wsplnego z jzykiem programowania i stopniowo zaczyna przypomina, nie przymierzajc, opowie zapisan w sanskrycie. Mimo ogromnej mocy asemblera, jego zoono okazuje si czynnikiem zdecydowanie dyskwalifikujcym (w aspekcie przydatnoci do tworzenia aplikacji). C++ to inny przypadek wszechmocnego jzyka. Dziki preprocesorowi, szablonom, przecianiu operatorw i wielu innym wspaniaociom mona za pomoc C++ stworzy wrcz wasny jzyk programowania! Pozostajce do dyspozycji programisty rodki jzykowe, o ile uywane s rozsdnie, umoliwiaj stworzenie czytelnego, atwego do konserwacji kodu. Problem jednak w owym rozsdnym uywaniu bogactwo konstrukcji C++ umoliwia bowiem take tworzenie kodu wygldajcego wrcz horrendalnie; jzyk ten nie posiada adnych rodkw prowadzcych do stworzenia logicznego i przemylanego projektu kwestia ta zalena jest cakowicie od odpowiedzialnoci programistw; jeli si dobrze zastanowi, mona w tym dostrzec pewien przejaw zoonoci. Jzykami, ktre wydaj si znakomicie godzi moc ze zoonoci s Object Pascal i Java. Obydwa charakteryzuj si pewnymi ograniczeniami, ktre nie pozwalaj zej na manowce i tym samym wymuszaj w pewnym stopniu logik projektowania. I tak, np. brak w Object Pascalu i Javie moliwoci wielokrotnego dziedziczenia klas, a jest moliwo implementacji przez dan klas wielu interfejsw; nie spotkamy tu take wygodnego, lecz czsto niebezpiecznego przeciania operatorw. Ponadto w obydwu rodowiskach zdecydowana wikszo zabiegw projektowych odbywa si na paszczynie kodu rdowego jzyka programowania, przy niewielkich moliwociach konfigurowania konsolidatora. Ograniczeniom tym towarzysz jednoczenie uyteczne mechanizmy nie powodujce wikszych zagroe zintegrowana obsuga wyjtkw, informacja o typach danych dostpna w czasie wykonania (RTTI) i acuchy z automatycznym zarzdzaniem pamici (jako rodzime typy jzyka programowania). Nie jest te kwesti przypadku, i zarwno Object Pascal, jak i Java to jzyki stworzone nie przez oficjalne komitety, lecz w ramach maych grup, bardzo dobrze wiedzcych, jak powinien wyglda jzyk programowania. Podstawowym zaoeniem, lecym u podstaw Visual Basica, bya moliwo szybkiej nauki programowania. Prosty pocztkowo jzyk zacz jednak z czasem obrasta w dodatkowe moliwoci i stawa si coraz bardziej zoony. Visual Basic wci posiada wiele ogranicze, ktre trzeba omija okrn drog, oczywicie za cen dodatkowej komplikacji projektu. Visual Basic.NET zdaje si czyni znaczce postpy w tym obszarze, jednak kosztem wstecznej kompatybilnoci.

25

Elastyczno i skalowalno architektury baz danych


Poniewa Borland nie posiada wasnej agendy do spraw baz danych, obecna wersja Delphi syntetyzuje w sobie te mechanizmy obsugi baz danych, ktre autorzy uznali za warte implementacji ze wzgldu na elastyczno i mnogo potencjalnych zastosowa. Mamy wic efektywn technologi dbExpress, cho efektywno ta okupiona jest w pewnym stopniu brakiem niektrych zaawansowanych elementw, poza tym repertuar sterownikw jest obecnie raczej ograniczony. BDE wci dziaa, i jest niezym rozwizaniem dla wielu aplikacji, gdy wsppracuje z wieloma rnorodnymi rdami danych; Borland nie ukrywa jednak zamiarw wycofania si z tej technologii. Komponenty ADO dostarczaj efektywnych rodkw komunikacji z danymi poprzez mechanizm ADO bd za porednictwem ODBC. Uytkownikom serwera InterBase Delphi oferuje zestaw rodzimych komponentw IBExpress. Uytkownik, dla ktrego aden z dostpnych standardowo mechanizmw nie okae si zadowalajcy, moe zorganizowa dostp do danych wedle swego wasnego pomysu, wykorzystujc architektur abstrakcyjnego zbioru danych, bd te moe zakupi u niezalenych wytwrcw gotowe rozwizanie tej kategorii. Mechanizm DataCLX umoliwia ponadto logiczny lub fizyczny podzia na warstwy, organizujce dostp do poszczeglnych rde danych. Produkty Microsoftu zdaj natomiast w swym wasnych kierunku, opierajc si na rodzimych rozwizaniach typu ODBC, OLE DB itp.

Wzorce projektowania i uytkowania wymuszone przez struktur rodowiska


Elementem determinujcym najsilniej sposb korzystania z Delphi jest (przy caym szacunku dla pozostaych elementw) biblioteka VCL. Moliwo manipulowania waciwociami i zdarzeniami komponentw w trakcie projektowania, moliwo tworzenia nowych komponentw drog dziedziczenia cech komponentw istniejcych to wszystko przyczynia si do skrcenia czasu budowania aplikacji i czyni sam proces budowania bardziej odpornym na bdy uytkownika. Konkurencyjne rodowiska projektowe bazujce na komponentach nie s tak elastyczne. Przykadowo kontrolki ActiveX, porwnywalne pod wzgldem elastycznoci na etapie projektowania, nie udostpniaj ju moliwoci dziedziczenia klas (na potrzeby tworzenia nowych kontrolek). rodowiska tradycyjne, jak OWL czy MFC, wymagaj natomiast od uytkownika niezej znajomoci szczegw technicznych, by mona je byo wykorzystywa w sposb naprawd produktywny i oczywicie pozbawione s tej elastycznoci na etapie projektowania, jak maj komponenty VCL. Microsoftowa biblioteka .NET wydaje si drog we waciwym kierunku, tym bardziej, e jej komponenty wsppracuj z niektrymi rodowiskami RAD, jak Visual C++ czy Visual Basic.

Nieco historii
Delphi 6 stanowi kontynuacj linii rozwojowej, ktr ponad 17 lat temu zapocztkowa Anders Hejlsberg tworzc pierwsz wersj Turbo Pascala. Jego wyjtkowymi cechami byy: niezwykle szybka kompilacja, efektywna diagnostyka bdw, wygodna obsuga oraz co tu ukrywa ogromne bogactwo jzyka w porwnaniu z innymi wersjami Pascala, gwnie dla wszechobecnych wwczas komputerw klasy mainframe. Kolejne wersje Turbo Pascala zdaway si potwierdza t pochlebn opini wobec niesychanie szybko postpujcego rozwoju informatyki i jej zastosowa oraz lawinowo rosncej popularnoci mikrokomputerw, programici stawali przed coraz to nowymi wyzwaniami, a z perspektywy czasu mona dzi miao stwierdzi, i Turbo Pascal jako narzdzie programistyczne speni pokadane w nim oczekiwania. Pojawienie si programowania modularnego (podzia na moduy w wersji 4.0), nakadkowanie i wykorzystanie jzyka asemblera oraz wbudowany debugger (wersja 5.0), wprowadzenie elementw programowania obiektowego (wersja 5.5) i wreszcie sprostanie standardom DPMI (wersja 7.0) umoliwiajce pokonanie coraz bardziej dotkliwej bariery 640kB pamici operacyjnej to tylko niektre z istotnych zalet kolejnych wersji Turbo Pascala. Nie jest wic niczym niezwykym, i Delphi okazao si godnym nastpc Turbo Pascala. Programici otrzymali wanie jego szst wersj, ktra jednoczenie jest pit wersj przeznaczon dla rodowiska 32-bitowego i

26

jednoczenie pierwsz, ktra przeamuje granice Windows, umoliwiajc tworzenie aplikacji take dla Linuksa, a w przyszoci (zgodnie z zapowiedziami Borlanda) dla wielu innych platform. Aby uzmysowi sobie postp, ktry dokona si w ponad siedmioletnim ywocie Delphi, przeledmy pokrtce histori jego kolejnych wersji.

Delphi 1
Na pocztku kariery mikrokomputerw, w czasie niepodzielnego panowania szacownego DOS-u, programici nie mieli zbyt duego wyboru narzdzi wybr w sprowadza si w zasadzie do wymuszonego kompromisu pomidzy wygodnym i produktywnym, lecz mao sprawnym BASIC-iem (w rnorodnych odmianach) oraz niezwykle efektywnym, lecz trudnym do opanowania jzykiem asemblera. Pojawienie si Turbo Pascala oznaczao zasypanie tej do gbokiej przepaci. Nie inaczej rzecz si miaa w rodowisku Windows 3.1 wybr sprowadza si do pisania w C++ lub budowania w Visual Basicu. Pojawienie si Delphi 1.0 byo prawdziwym przeomem: oferowao ono zarwno konstruowania aplikacji z elementw wizualnych, jak i wczania tradycyjnych moduw tekstowych, przy jednoczesnym tworzeniu niezalenych plikw wykonywalnych, wykorzystaniu bibliotek DLL, dostpie do mechanizmw obsugi baz danych, co czynio z niego niezwykle udany kompromis midzy skrajnymi biegunami pisania i budowania. Efektywno Delphi jako narzdzia projektowego bya tak uderzajca, e na okrelenie jego i narzdzi jemu podobnych w przyszoci stworzono powszechny ju dzi akronim RAD, stanowicy skrt od angielskiego okrelenia byskawicznego tworzenia aplikacji (Rapid Application Development). Kombinacja kompilatora, narzdzi projektowania wizualnego i efektywnej obsugi baz danych to bya naprawd kuszca propozycja dla dotychczasowych entuzjastw Visual Basica. Take uytkownicy Turbo Pascala postanowili przesi si na platform bardziej nowoczesn szczeglnie gdy okazao si, i nowy Pascal jest nieporwnanie bardziej dojrzay. Zesp Visual Basica w Microsofcie poczu si zagroony wolny i ociay Visual Basic 3 nie mg si rwna z Delphi 1. By wwczas rok 1995 i Borland wygra apelacj po przegranym z Lotusem procesie dotyczcym podobiestwa Quattro do 1-2-3. Postanawiajc konkurowa z Microsoftem na rynku aplikacji, Borland sprzeda Quattro Novellowi i zabra si za unowoczenianie dBasea i Paradoxa, aby uczyni z nich narzdzia dla profesjonalistw dotychczas przeznaczone byy one raczej dla uytkownikw-amatorw. Microsoft oczywicie nie prnowa, prbujc bez zbytniego rozgosu przecign na swoj stron znaczn cz projektantw aplikacji bazodanowych dla Windows tym bardziej, e ujawniy si pewne bdy w Delphi i nowej edycji Borland C++.

Delphi 2
Rok pniej Delphi zagocio po raz pierwszy na 32-bitowej platformie Windows 95/ NT, oferujc, oprcz zoptymalizowanego, niezwykle efektywnego 32-bitowego kompilatora, poszerzon bibliotek komponentw, znacznie udoskonalone mechanizmy obsugi baz danych, rewelacyjn obsug acuchw tekstowych, wizualne dziedziczenie formularzy (Visual Form Inheritance), wykorzystanie technologii OLE i zarazem zgodno z 16-bitowymi projektami (oczywicie przy niezbdnym minimum niemoliwych do uniknicia ogranicze, jednak co pokazaa praktyka niezbyt uciliwych). Wedug zgodnej opinii programistw-projektantw, Delphi 2 stanowio milowy krok na drodze rozwoju Pascala i narzdzi typu RAD. By rok 1996. Kilka miesicy wczeniej uytkownicy Windows otrzymali wersj 32-bitow Windows 95 co w historii stanowio najbardziej spektakularny krok od czasu wersji 3.0. Stworzyo to oczywicie zapotrzebowanie na odpowiednie dla rodowiska 32-bitowego narzdzia projektowe. Jednym z nich Borland postanowi uczyni swj produkt Delphi. Ciekawostk jest, i pierwotnie nowe narzdzie miao si nazywa Delphi32, co akcentowaoby jego zwizek z platform 32-bitow; Borland postanowi jednak zachowa spjn numeracj, nadajc mu ostatecznie nazw Delphi 2 dla podkrelenia, i Delphi jest produktem w peni dojrzaym, i w celu uniknicia znanego na rynku programistycznym tzw. syndromu wersji 1.0. Odpowiedzi ze strony Microsoftu by Visual Basic 4, charakteryzujcy si kiepsk efektywnoci, brakiem przenonoci pomidzy platformami 16- i 32-bitow oraz kluczowymi wadami w samym projekcie; nie zniechcio to jednak licznej rzeszy programistw korzystajcych wci z Visual Basica. Borland z kolei

27

prowadzi rozpoznanie rynku aplikacji klient-serwer, opanowanego przez aplikacje w rodzaju PowerBuildera, z zamiarem ulokowania na nim Delphi (jako narzdzia projektowego). Obecna wersja Delphi miaa jednak niewielkie szanse wobec konkurencji w tej dziedzinie. Wypracowanie waciwej strategii na rynku klientw korporacyjnych miao dla Borlanda tym wiksze znaczenie, i rynek uytkownikw dBasea i Paradoxa okaza si niewystarczajcy, za zyski z C++ kurczyy si coraz bardziej. To normalne, i ludzie popeniaj bdy; nie unikn ich take Borland, wykupujc firm Open Environment Corporation producenta oprogramowania typu middleware, oferujcego zasadniczo dwa produkty: pierwszy z nich, bazujcy na technologii DCE, sta si pierwowzorem dzisiejszej implementacji mechanizmu CORBA (Common Object Request Broker Architecture); drugi, oparty na rozproszonej technologii OLE, zosta usunity w cie przez technologi DCOM.

Delphi 3
Jest rzecz oczywist, i projektowanie kolejnych wersji Delphi nastpowao na podstawie precyzyjnie okrelonych celw projektowych. Tak wic podstawowym zadaniem Delphi 1 byo dostarczenie programistom Turbo Pascala narzdzia umoliwiajcego poczenie ich wieloletniego dowiadczenia z zaletami programowania wizualnego, reprezentowanego wwczas przez czsto przywoywany Visual Basic. Delphi 2 to przede wszystkim zacztek obecnoci Delphi w rodowisku Win32, z zachowaniem elementw kompatybilnoci ze rodowiskiem 16-bitowym i z nowymi elementami bazodanowymi typu klient-serwer, niezbdnymi dla aplikacji korporacyjnych. W takim kontekcie Delphi 3 postrzega naley przede wszystkim jako implementacj kolejnych technologii towarzyszcych programowaniu w Windows a wic obiektw COM, kontrolek ActiveX, aplikacji serwerw WWW i wielowarstwowych aplikacji SQL oraz aplikacji uproszczonego klienta (thin client). Nie od rzeczy bdzie rwnie przypomnienie znaczcych usprawnie samego rodowiska projektowego gwnie implementacji pakietw, konfigurowalnej dla poszczeglnych projektw palety komponentw, usprawnienia edytora (CodeInsight), debuggera itp. Sama metodologia projektowania aplikacji pozostaa jednak w duej czci taka sama, jak w Delphi 1. By rok 1997. Bardzo duo dziao si na rynku narzdzi do tworzenia aplikacji. Najpierw Microsoft zaprezentowa swj Visual Basic 5 z ulepszonym i efektywniejszym kompilatorem, dobr obsug COM/ActiveX i wieloma innymi nowociami. Delphi udao si natomiast zaj znaczc pozycj rynkow i zdystansowa zarwno PowerBuildera, jak i Forte. Niewtpliw strat dla cyklu rozwojowego Delphi byo przejcie do Microsoftu naczelnego architekta Andersa Hejlsberga. Prace jednak nie ustay nowym szefem zespou zosta Chuck Jazdzewski, dotychczas wspkonstruktor Delphi.

Delphi 4
Wrd nowoci kolejnej wersji Delphi wymieni naley midzy innymi kolejne usprawnienia samego rodowiska IDE, czynice prac programisty jeszcze atwiejsz; w pierwszym rzdzie Eksplorator Moduw, uwypuklajcy modularno-hierarchiczn struktur kodu, oraz udoskonalenia edytora w postaci kompletacji definicji klas i usprawnionego nawigowania. Zmienio si rwnie nieco zachowanie okien i paskw narzdziowych zyskay one ciekaw wasno dokowalnoci (docking) usprawniony zosta rwnie zintegrowany debugger. Programici baz danych zyskali take narzdzie umoliwiajce tworzenie wielowarstwowych aplikacji typu klient-serwer na podstawie tak nowoczesnych technologii, jak MIDAS, DCOM, MTS i CORBA. By rok 1998. Delphi umacniao sw pozycj rynkow, zyskujc coraz wiksz popularno. Dysponowao wszak niezaprzeczalnym atutem, nieobecnym u konkurencji implementacj specyfikacji CORBA, ktra bya technologicznym hitem tamtych czasw. Wypuszczenie na rynek Delphi 4 poprzedzone byo wchoniciem przez Borland firmy Visigenic, wczesnego lidera w zakresie technologii CORBA. Wkrtce Borland zmieni sw nazw na Inprise, co odzwierciedla miao strategi zorientowan na oprogramowanie dla przedsibiorstw (enterprise), gwnie na podstawie technologii CORBA. By jednak zamierzenie to mogo zosta uwieczone sukcesem, tworzenie aplikacji z wykorzystaniem tej technologii powinno by tak proste, jak wykorzystanie technologii COM i tworzenie aplikacji internetowych. Z rnych przyczyn nie dokonano jednak w Delphi 4 penej integracji technologii CORBA, pozostawiajc t kwesti jako element oglnych planw rozwojowych przyszego oprogramowania.

28

Delphi 5
Delphi 5 to duy krok naprzd pod wieloma wzgldami. Po pierwsze, kontynuowano (rozpoczte w Delphi 4) zabiegi majce na celu uproszczenie tych czynnoci zwizanych z IDE i debuggerem, ktre uwaane byy za uciliwe i czasochonne aby uytkownik mg skoncentrowa si na tym co zamierza napisa, bez zbytniego troszczenia si o to jak to napisa. Po wtre, wprowadzono wiele nowych elementw majcych uproci tworzenie aplikacji zwizanych z Internetem m.in. Active Server Object Wizard, komponenty InternetExpress z obsug jzyka XML oraz nowe elementy MIDAS-a, czynice go przydatnym dla platformy internetowej. Wreszcie powicono mnstwo czasu i wiele wysiku, by wyposay now wersj Delphi w to, co najwaniejsze: stabilno. Niczym dobre wino, dobre oprogramowanie musi dojrze co z pewnoci rozumia Borland, nie spieszc si z now wersj Delphi i czekajc, a bdzie ona faktycznie gotowa do wypuszczenia na rynek. Bya druga poowa 1999 roku. Delphi kontynuowao penetracj rynku korporacyjnego, podczas gdy Visual Basic nie przestawa walczy o klientw indywidualnych. Firma powrcia do swej pierwotnej nazwy (Borland), chocia nie sensu stricto po wielu perturbacjach na szczeblu zarzdzania nastpi podzia firmy na sekcje narzdzi i oprogramowania middleware; odszed Del Yoham, za jego nastpca Dale Fuller z powrotem przestawi Borland na tory oprogramowania dla projektantw aplikacji.

Delphi 6
Podstawow kwesti zwizan z Delphi 6 jest jego zgodno z Kyliksem narzdziem RAD dla Linuksa. W tym celu Borland opracowa now bibliotek komponentw Component Library For Cross Platform, oznaczan akronimem CLX, zawierajc midzy innymi komponenty VisualCLX suce do projektowania interfejsu uytkownika, bazodanowe komponenty DataCLX i komponenty NetCLX dla aplikacji internetowych. Aplikacje ograniczajce si do biblioteki CLX i przenonych elementw biblioteki RTL mog by bez trudu przenoszone pomidzy Windows i Linuksem. Nowe komponenty i sterowniki dbExpress stanowi kolejny krok w stron zgodnoci z Linuksem; s jednoczenie realn alternatyw dla wyranie starzejcego si ju mechanizmu BDE. Nie mniej wany aspekt unowoczenie w ostatniej wersji Delphi zwizany jest z jzykiem XML, a dokadniej z wyran tendencj do zwikszania jego roli w aplikacjach przemysowych i biznesowych. Jzyk ten uatwia aplikacjom pokonywanie barier zwizanych z rnicami pomidzy rodowiskami projektowymi, architekturami baz danych, platformami systemowymi, jak rwnie pomidzy poszczeglnymi komputerami w Internecie. Analizujc repertuar komponentw Delphi, natrafimy na obecno XML-a midzy innymi w komponentach bazodanowych, komponentach sucych do tworzenia aplikacji internetowych oraz komponentach usug sieciowych typu SOAP. Caoci dopeniaj zwyczajowe ju (w kontekcie dotychczasowego rozwoju Delphi) udoskonalenia w zakresie VCL, IDE, debuggera, biblioteki RTL i samego Object Pascala.

rodowisko zintegrowane Delphi 6


Aby unikn ewentualnych nieporozumie terminologicznych w dalszej czci lektury, przedstawiamy na rysunku 1.2 rodowisko zintegrowane (IDE) Delphi 6 z wyszczeglnieniem jego najwaniejszych elementw: okna gwnego, palety komponentw, paskw narzdziowych, projektanta formularzy, edytora kodu, inspektora obiektw, hierarchii obiektw i eksploratora kodu.

29

Rysunek 1.2. Podstawowe elementy rodowiska zintegrowanego Delphi 6

Okno gwne
Okno gwne Delphi 6 peni swoist rol centrum sterowania caego rodowiska w terminologii aplikacji Windows jest ono gwnym oknem aplikacji. Skada si z trzech podstawowych czci: menu gwnego, paskw narzdziowych oraz palety komponentw.

Menu gwne
Jak w kadej typowej aplikacji Windows, menu gwne Delphi 6 umoliwia wykonywanie podstawowych operacji, jak np. otwarcie istniejcego lub zainicjowanie nowego projektu, moduu, formularza lub innego pliku, zapisanie wprowadzonych zmian itp. Tutaj take znajduj si polecenia umoliwiajce przenoszenie danych za pomoc schowka do innych aplikacji oraz uruchamianie aplikacji towarzyszcych, jak np. 32-bitowy Turbo Debugger lub kontroler wersji. Wikszo polece menu gwnego posiada swe odpowiedniki w postaci przyciskw na paskach narzdziowych.

Paski narzdziowe Delphi


Dziki paskom narzdziowym moliwe jest wykonywanie niektrych operacji poprzez jednokrotne kliknicie odpowiedniego przycisku, zamiast bardziej kopotliwego wybierania polecenia menu. Zazwyczaj z kadym ze wspomnianych przyciskw skojarzona jest podpowied (tooltip), zawierajca krtki opis funkcji zwizanej z danym przyciskiem. Delphi 6 posiada pi nastpujcych paskw narzdziowych (nie liczc palety komponentw): Debug, Desktop, Standard, View i Custom. Na rysunku 1.2 widzimy ich domyln posta, ktr mona atwo zmienia za pomoc polecenia Customize z menu kontekstowego kadego z paskw. Okno dialogowe zwizane z tym poleceniem zostao przedstawione na rysunku 1.3 dodawanie nowych przyciskw odbywa si poprzez przeciganie ich z listy Commands na docelowy pasek, za usunicie przycisku nastpuje poprzez wycignicie go poza obszar paska.

30

Rysunek 1.3. Okno dialogowe konfiguracji paskw narzdziowych

Moliwoci dostosowawcze paskw narzdziowych nie ograniczaj si jednak do zmiany zestawu przyciskw. Paski te jak rwnie paleta komponentw i menu gwne mog by przemieszczane w obrbie okna gwnego. Naley w tym celu chwyci mysz widoczny z lewej strony uchwyt i przecign pasek w docelowe pooenie. Gdy przemiecimy pasek narzdziowy poza granice okna gwnego, zaobserwujemy inny ciekawy efekt pasek zostanie wwczas wydokowany z okna gwnego i funkcjonuje jako niezalene okienko narzdziowe. Przykad takich pywajcych paskw narzdziowych przedstawia rysunek 1.4

Rysunek 1.4. Pywajce paski narzdziowe

Paleta komponentw
Paleta komponentw jest odmian paska narzdziowego o podwjnej wysokoci; jest ona podzielona na strony, na ktrych reprezentowane s wszystkie zainstalowane w IDE komponenty VCL oraz kontrolki ActiveX. Zestaw i kolejno komponentw na poszczeglnych stronach mona zmienia za pomoc polecenia Properties z menu kontekstowego palety lub za pomoc polecenia Component/Configure Palette z menu gwnego.

31

Projektant formularzy
Formularze odpowiedzialne s za ostateczny wygld aplikacji. Pocztkowo okno formularza jest puste; twrca aplikacji, umieszczajc w nim poszczeglne komponenty, postpuje podobnie jak artysta malarz, urzeczywistniajcy na ptnie swe natchnienie. Rozmieszczenie i rozmiar komponentw mona korygowa za pomoc myszy, a do ewentualnej modyfikacji ich wygldu i zachowania su: inspektor obiektw oraz edytor kodu.

Inspektor obiektw
Inspektor obiektw umoliwia modyfikacj waciwoci komponentw oraz zmian ich sposobu reagowania na zdarzenia. Waciwociami (properties) obiektu s dane okrelajce na przykad jego wysoko, szeroko, kolor i czcionk, czyli midzy innymi jego reprezentacj graficzn na ekranie. Zdarzenie (event) zwizane jest natomiast z wystpieniem pewnej sytuacji, na przykad naciniciem klawisza, przesuniciem myszy, czy te naciniciem jednego z jej przyciskw: reakcja obiektu na poszczeglne zdarzenia opisana jest szczegowo za pomoc wydzielonej czci kodu, zwanej procedur obsugi zdarze (event procedure). Inspektor obiektw podzielony jest na dwie czci, zwizane z waciwociami i zdarzeniami, za przeczanie si midzy nimi zorganizowane jest podobnie, jak wybieranie zakadek notatnika aby wybra zdarzenia lub waciwoci, trzeba klikn odpowiedni zakadk (tab). Zestaw wywietlanych w danej chwili waciwoci lub zdarze odpowiada aktualnie wybranemu formularzowi lub komponentowi (w oknie projektanta formularzy). Poczwszy od Delphi 5 moliwe jest wywietlanie waciwoci (zdarze) na jeden z dwu sposobw: w alfabetycznie uporzdkowanym monolicie, bd te w podziale na kategorie. Wygld okna inspektora obiektw w kadym z tych dwu przypadkw przedstawia rysunek 1.5.

Rysunek 1.5. Waciwoci formularza wywietlane alfabetycznie oraz w podziale na kategorie

32

Jedn z bardzo wygodnych cech inspektora obiektw i jednoczenie przejawem jego integracji z reszt rodowiska jest natychmiastowy dostp do tekstu pomocy zwizanego z wybran waciwoci lub zdarzeniem: wystarczy tylko nacisn F1.

Edytor kodu
Edytor kodu jest tym miejscem IDE, gdzie odbywa si proces programowania w cisym tego sowa znaczeniu. Kod aplikacji wpisywany rcznie bd generowany automatycznie podczas wstawiania nowych komponentw oraz modyfikacji ich waciwoci i zdarze opisuje szczegowo zachowanie si wszystkich komponentw aplikacji. Poszczeglne moduy kodu dostpne s poprzez szereg zakadek (tabs), umieszczonych w grnej czci okna edytora. Kadorazowe wczenie do aplikacji nowego formularza spowoduje automatyczne wygenerowanie nowego moduu kodu. Moliwe jest rwnie niezalene dodawanie moduw kodu nie reprezentujcych formularzy. Menu kontekstowe edytora kodu udostpnia szerok gam polece zwizanych z edycj, jak np. zamykanie plikw, ustawianie zakadek (bookmarks) i nawigowanie wrd symboli.
Wskazwka

Moliwe jest jednoczesne otwarcie kilku okien edytora kodu za pomoc polecenia View|New Edit Window menu gwnego.

Eksplorator kodu
Eksplorator kodu uwidacznia w przejrzystej, hierarchicznej postaci wewntrzn struktur moduu, ktrego kod wywietlany jest aktualnie w oknie edytora kodu zawarto obydwu tych okien (eksploratora i edytora) powizana jest ze sob w cisy sposb: kady z elementw odzwierciedlony jest w relacji jeden do jednego. Oprcz wygodnej nawigacji wrd elementw moduu, moliwe jest take dodawanie nowych elementw oraz zmiana nazw elementw istniejcych. Z kadym elementem zawartym w oknie eksploratora kodu zwizana jest grupa polece, dostpnych za pomoc menu kontekstowego (uruchamianego klikniciem prawym przyciskiem myszy). Wiele szczegw zwizanych z funkcjonowaniem eksploratora kodu jak sortowanie i filtrowanie elementw okreli mona za pomoc ustawie na karcie Explorer opcji rodowiska (dostpnych za porednictwem polecenia Tools|Environment Options menu gwnego).

Hierarchia obiektw
Okno odzwierciedlajce hierarchiczn zaleno obiektw (Object TreeView) jest nowoci Delphi 6. Daje obraz zestawu komponentw formularza, moduu danych lub ramki, w hierarchicznym ukadzie przedstawiajcym zaleno rodzicielski potomny. Moemy atwo zmienia t zaleno przecigajc (w obrbie okna) ikony reprezentujce komponenty, moemy te przenosi komponenty z palety bezporednio do okna Object TreeView (z moliwoci wyboru elementu rodzicielskiego).

Kod rdowy projektu


Rwnolegle do konstruowania nowego formularza czyli umieszczania na nim kolejnych komponentw odbywa si automatyczne generowanie kodu rdowego odzwierciedlajcego biecy wygld formularza. Najatwiej mona to zaobserwowa w momencie tworzenia nowego projektu. W tym celu wybierz z menu gwnego polecenie File|New|Application, a ujrzysz pusty formularz oraz szkielet odpowiadajcego mu moduu w oknie edytora kodu, zgodnie z poniszym wydrukiem:

33

Wydruk 1.1. Szkielet kodu nowego formularza


unit Unit1;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type TForm1 = class(TForm) private { Private declarations } public { Public declarations } end;

var Form1: TForm1;

implementation

{$R *.DFM}

end.

Zauwa, e kod odpowiadajcy formularzowi ma posta moduu; jest to regu w Delphi kademu formularzowi odpowiada jeden modu, lecz nie zawsze jest odwrotnie, poniewa mog istnie moduy nie reprezentujce formularzy. Zwr uwag na deklaracj formularza jako typu:

type TForm1 = class(TForm) private { Private declarations } public { Public declarations } end;

Jak atwo zauway, formularz jest obiektem o typie pochodnym w stosunku do typu TForm. Stosowne komentarze wskazuj miejsce do wpisania wasnych deklaracji prywatnych i publicznych (wicej szczegw na temat waciwoci obiektw znajdziesz w rozdziale 2.). Kolejnym wanym szczegem w przedstawionym wydruku jest dyrektywa
{$R *.DFM}

Suy ona do poczenia moduu z reprezentujcym formularz plikiem zasobu o rozszerzeniu .DFM (od Delphi ForM). Gwiazdka zastpujca nazw pliku nie spenia tutaj roli szablonu (wildcard), lecz symbolizuje nazw moduu. I tak, dla moduu UNIT1.PAS napis *.DFM oznacza to samo, co UNIT1.DFM.

34

Wskazwka

Poczwszy od Delphi 5, moliwe jest przechowywanie formularzy (w plikach .DFM) w postaci tekstowej (zamiast, jak dotychczas, w postaci binarnej). Domylnie kady nowo tworzony formularz zapisywany jest w postaci tekstowej, lecz moemy to atwo zmieni, usuwajc zaznaczenie opcji New Forms As Text na karcie Preferences opcji rodowiska (Environment Options). Mimo i tekstowa reprezentacja formularza jest nieco mniej efektywna od binarnej pod wzgldem rozmiaru i szybkoci odczytu (zapisu), kilka wanych wzgldw przemawia jednak za jej stosowaniem. Po pierwsze, niewielkie zmiany w formularzu mog by dokonywane za pomoc zwykego edytora tekstowego, bez koniecznoci uruchamiania Delphi. Po drugie, dane z uszkodzonego pliku tekstowego daj si odzyska znacznie atwiej ni z uszkodzonego pliku binarnego. Wreszcie tekstowe pliki formularzy s atwiejsze w zarzdzaniu dla systemw kontroli wersji. Naley jednak pamita o tym, i wersje wczeniejsze od Delphi 5 akceptuj wycznie binarn posta plikw .DFM.

W odrnieniu od wikszoci tradycyjnych aplikacji tworzonych w Turbo Pascalu, prawie cay kod aplikacji zawarty jest w moduach, natomiast program gwny zredukowany zosta do roli organizacyjnej. Kod programu gwnego przechowywany jest w pliku o rozszerzeniu .DPR (od Delphi PRoject), pod nazw odpowiadajc nazwie projektu. Oto przykadowa posta programu gwnego Delphi:
program Project1;

uses Forms, Unit1 in 'Unit1.pas' {Form1}; {$R *.RES}

begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.

Powyszy kod rezyduje w pliku o nazwie PROJECT1.DPR. Oczywicie, tre kadego moduu jest automatycznie uaktualniana w miar wprowadzania zmian do odnonych formularzy. Tak samo, w miar przybywania nowych moduw, uaktualniana jest dyrektywa uses w programie gwnym. atw orientacj w repertuarze poszczeglnych moduw umoliwia okno Menedera Projektu, wywietlane za pomoc polecenia View|Project Manager. Edycj zawartoci pliku .DPR umoliwia polecenie Project|View Source menu gwnego.
Notatka

Kademu formularzowi odpowiada jeden modu kodu, lecz mog istnie moduy nie reprezentujce formularzy. Poza tym do rzadkoci naley modyfikowanie kodu programu gwnego.

Prosta aplikacja
Do dziewiczego jeszcze formularza dodaj z palety przycisk Button (ten z napisem OK); kod moduu przeobrazi si wwczas do nastpujcej postaci:
type TForm1 = class(TForm) Button1: TButton;

35

private { Private declarations } public { Public declarations } end;

Jak atwo zauway, przycisk sta si polem obiektu formularza. Nazwa tego pola Button1 ma sens jedynie w kontekcie formularza typu TForm1 (i jego typw pochodnych), tak wic odwoujc si do niej poza zasigiem deklaracji TForm1, musisz uy odwoania kwalifikowanego Form1.Button1 (Form1 jest nazw zmiennej typu TForm1, a pojcie zasigu i jego implikacje zostan wyjanione w rozdziale 2.). Przycisk dodany wanie do formularza nie jest obiektem niezmiennym; za pomoc inspektora obiektw mona modyfikowa zarwno jego waciwoci, jak i jego sposb reakcji na zdarzenia. Aby si o tym przekona, uczy przycisk aktywnym obiektem formularza (kliknij go) i otwrz okno inspektora obiektw (poprzez wybranie z menu gwnego opcji View|Object Inspector lub nacinicie klawisza F11). Nastpnie odszukaj pozycj odpowiadajc waciwoci Width i zmie jej standardow warto 75 na 100; po naciniciu klawisza Enter przycisk wyduy si. Nastpnie przejd na stron Events inspektora obiektw i znajd pozycj OnClick. Po dwukrotnym klikniciu pustego pola zwizanego z t pozycj Delphi automatycznie wprowadzi do moduu UNIT1.PAS szkielet procedury obsugujcej zdarzenie kliknicia:

procedure TForm1.Button1Click(Sender: TObject); begin

end;

Jest to zaledwie szkielet procedury nie wypenilimy jej jeszcze treci i kliknicie przycisku (w uruchomionej aplikacji) nie wywoa adnych skutkw. Aby przycisk oywi, ka mu dwukrotnie zwikszy sw wysoko po kadym klikniciu; uzyskasz to przez wpisanie do szkieletu procedury nastpujcej instrukcji:
Button1.Height := Button1.Height * 2

Po skompilowaniu i uruchomieniu aplikacji przekonasz si, e wszystko dziaa zgodnie z Twoimi oczekiwaniami.

Notatka

Delphi sprawuje cakowit kontrol nad generowanymi procedurami zdarzeniowymi i ich zwizkiem z odnonymi komponentami. W procesie kompilacji usuwane s procedury jaowe, to znaczy takie, ktrych tre ogranicza si do dyrektyw begin i end. Jest to czci szeroko zakrojonych optymalizacji, ktre kompilator jest w stanie wykona w celu zmniejszenia rozmiaru kodu oraz polepszenia jego efektywnoci. Wynika std jednoczenie wniosek, e niecelowe jest rczne usuwanie z moduw kodu procedur generowanych automatycznie i pniej nie wykorzystywanych kompilator sam wykona t czynno, poza tym obecnie puste procedury mog by przecie w przyszoci modyfikowane.

Gdy przycisk na formularzu stanie si ju nadmiernie duy, moesz zakoczy prac aplikacji i powrci do IDE. W tym miejscu warto nadmieni, e opisana metoda programowania procedur zdarzeniowych obiektu nie jest jedyna. Jeeli dane zdarzenie jest zdarzeniem domylnym obiektu, wystarczy klikn go dwukrotnie, co spowoduje udostpnienie odpowiedniej procedury zdarzeniowej. To, ktre zdarzenie obiektu jest jego zdarzeniem domylnym, zaley od konkretnej klasy i tak dla przycisku TButton jest to zdarzenie OnClick, lecz ju np. dla komponentu zegarowego TTimer jest to zdarzenie OnTimer.

36

Jeszcze o zdarzeniach...
Jeeli miae okazj tworzy aplikacje w tradycyjnym rodowisku Windows, z pewnoci docenisz moliwoci, jakie oferuje Delphi w dziedzinie obsugi zdarze i programowania sterowanego zdarzeniami. Programowanie tradycyjne wymagao od programisty wykonywania wielu czynnoci na piechot podstawow form komunikacji wewntrz aplikacji Windows s komunikaty, a ich klasyfikowanie, testowanie kontekstu (uchwytw, okien, parametrw) spoczywao cakowicie na barkach aplikacji. Komunikatom Windows powicilimy rozdzia 3., tam wic moesz znale wicej informacji na ten temat. W Delphi podstawow form komunikacji s zdarzenia (events); s one wanie przechwyconymi i odpowiednio opracowanymi komunikatami. Na przykad zdarzenie OnMouseDown dotyczce formularza typu TForm, jest w rzeczywistoci obudowanym komunikatem WM_xBUTTONDOWN. Zdarzenie to niesie ze sob informacj o swoich szczegach w tym wypadku o nacinitym przycisku (lewy, rodkowy, prawy). Podobnie ma si rzecz ze zdarzeniem OnKeyDown generowanym w odpowiedzi na nacinicie klawisza kod nacinitego klawisza i stan klawiszy Shift jest integraln czci zdarzenia, co najlepiej wida w szablonie odpowiadajcej mu procedury zdarzeniowej:
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin

end;

Delphi, obudowujc komunikat stosownym zdarzeniem, wykonujeca czarn robot dokonuje analizy komunikatu i odczytuje jego parametry. Czsto zdarza si tak, e dane zdarzenie komponowane jest z kilku komunikatw (dotyczy to m.in. zdarzenia OnMouseDown); zalety Delphi staj si wtedy jeszcze bardziej odczuwalne.

Niekontraktowy charakter obsugi zdarze


Najwiksz bodaj korzyci wynikajc z mechanizmu zdarze Delphi jest co, co mona by nazwa programowaniem niekontraktowym (contract-free programming). Okrelenie to oznacza zwolnienie programisty z obowizku oprogramowywania jakichkolwiek zdarze, innymi sowy ingerencja w domylne szablony procedur zdarzeniowych jest cakowicie dobrowolna i podyktowana zazwyczaj chci zmiany niektrych aspektw zachowania si komponentu. Nie mia tego komfortu programista tworzcy niegdy aplikacje dla Windows w sposb klasyczny, czyli poprzez obsug komunikatw. By on bowiem odpowiedzialny za drobiazgow obsug komunikatw, w szczeglnoci za wywoywanie odziedziczonych procedur ich obsugi oraz decyzj, czy po obsueniu danego komunikatu przekaza sterowanie z powrotem do Windows, czy te dany komunikat cakowicie wygasi. Delphi, przejmujc kopotliw obsug komunikatw i konwertujc je na zdarzenia dostpne dla programisty, pozbawia go jednoczenie pewnej elastycznoci. Mimo caej porcznoci mechanizmu zdarze, niektrych zada nie da si zrealizowa wycznie za ich pomoc na przykad zdarzenie OnResize suy jedynie do powiadomienia aplikacji, i nastpuje zmiana rozmiarw okna; procedura obsugi tego zdarzenia nie daje adnych rodkw do tego, by zmian tak uniemoliwi. Z tego wanie wzgldu Delphi, udostpniajc programicie mechanizm zdarze, nie uniemoliwia mu jednoczenie bezporedniego operowania komunikatami. Wymaga to oczywicie pewnej odpowiedzialnoci i oczywicie odpowiedniej znajomoci systemu operacyjnego, chocia i pod tym wzgldem Delphi jest na swj sposb pomocne, bowiem procedury obsugi komunikatw maj posta metod obiektu, do ktrego komunikaty te s adresowane. Szczegami bezporedniego operowania systemem komunikatw zajmiemy si w rozdziale 3. Mechanizm zdarze uwalnia nas wic od pewnych uciliwoci, jednoczenie nie pozbawiajc moliwych korzyci; to troch tak, jakby zje ciastko i mie je nadal. Co wspaniaego

37

Prototypowanie kodu
Gdy analizuje si Delphi z punktu widzenia pocztkujcego uytkownika, nie sposb oprze si wraeniu, i celowo wyposaono je w rodki znacznie uatwiajce stworzenie pierwszego projektu i oglnie czynice nauk efektywniejsz. rodki te su oczywicie take programistom zaawansowanym, umoliwiajc im szybsze i efektywniejsze tworzenie skomplikowanych nieraz projektw. A to wszystko za spraw tego elementu rodowiska, ktry w wielu przypadkach bywa prawdziw zmor programistw mianowicie interfejsu uytkownika. Ten aspekt funkcjonowania interfejsu uytkownika, ktry wykorzystujc oglne zasady okrelajce ksztat gotowego programu uatwia doprowadzenie kodu rdowego do ostatecznej postaci, nazywany bywa czsto prototypowaniem kodu. Zauwaye zapewne, w jaki sposb generator kodu rdowego dopisuje do moduu kolejne procedury zdarzeniowe: automatycznie generowany jest jedynie szkielet, ktry potem uzupeniany jest przez programist, stosownie do potrzeb. Tworzone w ten sposb szablony procedur stanowi wanie co na ksztat prototypw przyszego kodu. Zadaniem programisty jest przeksztacenie tych prototypw w podan posta docelow, pocztkowo bowiem za efektownym interfejsem graficznym nie kryj si adne konkretne funkcje: poszczeglne obiekty s guche, gdy procedury okrelajce ich zachowanie, stanowice zaplecze (back-end) aplikacji, s w tej fazie jedynie prototypami. Jednak na co komu nie dziaajca aplikacja, w ktrej pod wystrzaow form nie kryje si adna tre? C, na przykad, wart jest program komunikacyjny, ktry dysponujc efektownymi okienkami i listami wyboru, nie dokonuje de facto adnych pocze? Kady, kto w tradycyjnych rodowiskach programistycznych posugiwa si technik prototypowania we wasnym wykonaniu, z pewnoci doceni ten element automatyzmu Delphi. Wysoki stopie owego automatyzmu nie odbija si przy tym ujemnie na wygldzie graficznym aplikacji (interfejsie uytkownika). Delphi oferuje bowiem ca gam komponentw i kontrolek realizujcych podstawowe funkcje dialogowe oraz prezentacyjne i zapewniajcych aplikacji profesjonalny wygld, wcale nie gorszy o ile nie lepszy od tego, ktry stworzony byby za pomoc tradycyjnych narzdzi.

Rozbudowa zestawu komponentw i konfigurowalne rodowisko


Oprcz moliwoci tworzenia wasnych komponentw od zera, Delphi umoliwia wprowadzanie nowych komponentw poprzez modyfikacj komponentw istniejcych z wykorzystaniem mechanizmw dziedziczenia pl, metod i waciwoci obiektw. To jedna z korzystnych konsekwencji obiektowej natury Delphi. Problematyce tworzenia komponentw powicimy kocowe rozdziay niniejszego tomu. Delphi oferuje ponadto moliwo doczania do rodowiska zintegrowanego zewntrznych programw (ekspertw experts), poprzez umieszczenie odpowiadajcych im polece w menu gwnym. Zadanie to wykonuje narzdzie Delphi o nazwie Expert Interface. Na przykad programem-ekspertem jest Database Form Wizard, reprezentowany przez jedno z polece menu Database. Szczegom tworzenia programwekspertw i integrowania ich ze rodowiskiem IDE powicimy jeden z rozdziaw drugiego tomu niniejszej ksiki.

Top 10 gorca dziesitka IDE


Jednym z warunkw efektywnego wykorzystania narzdzia jest poznanie wszystkich jego moliwoci. Poniej przedstawiamy 10 wybranych elementw IDE, ktre programista powinien pozna i nauczy si efektywnie z nich korzysta.

1. Uzupenianie klas
Nic chyba nie irytuje programistw bardziej ni konieczno wpisywania oczywistego kodu czy nie odnosisz czsto wraenia, i gwnym ograniczajcym Ci czynnikiem jest szybko Twoich palcw? By moe kiedy pojawi si inteligentna klawiatura odgadujca Twoje intencje, pki co, Delphi oferuje Ci podobny mechanizm zwany uzupenianiem klas (Class Completion) wystarczy, i napiszesz deklaracj jednego z

38

elementw klasy i naciniesz magiczn kombinacj klawiszy Shift+Ctrl+C, a Delphi dokona automatycznie odpowiednich wpisw w czci implementacyjnej. I tak, na przykad, gdy zadeklarujesz dowoln metod i naciniesz wspomnian kombinacj klawiszy, spowodujesz wygenerowanie szkieletu definicji tej metody. Jeeli z kolei zadeklarujesz waciwo (property) bazujc na polu obiektu i metodzie dostpowej, nacinicie wspomnianej kombinacji spowoduje wygenerowanie zarwno deklaracji stosownego pola, jak i deklaracji oraz definicji stosownej metody dostpowej1.

2. Udostpnianie deklaracji symbolu


W gszczu setek czy nawet tysicy symboli w kodzie rdowym trudno pamita znaczenie i struktur (typ, rozmiar) kadego z nich. Nieocenion wydaje si wwczas moliwo szybkiego dotarcia do deklaracji wybranego symbolu co Delphi potrafi byskawicznie uczyni, gdy klikniesz identyfikator symbolu przy nacinitym klawiszu Ctrl, wykorzystujc informacj symboliczn tworzon przez kompilator (pod warunkiem oczywicie, i dla moduu zawierajcego deklaracj symbolu informacja taka jest dostpna). To jeszcze nie wszystko Delphi zapamituje indagowane w ten sposb symbole, tworzc co na ksztat stosu historii, po ktrego elementach mona atwo si porusza za pomoc klawiszy strzaek pionowych, przy nacinitym klawiszu Ctrl.

3. Od deklaracji do definicji
Delphi umoliwia take szybkie przeczanie si pomidzy deklaracj symbolu a jego definicj (i vice versa) naley wwczas umieci kursor edytora na identyfikatorze symbolu i nacisn kombinacj klawiszy Shift+Ctrl+<strzaka pionowa>.

4. Dokowanie
IDE umoliwia poczenie kilku niezalenych okien w taki sposb, by funkcjonoway jako poszczeglne panele pojedynczego okna. Gdy sprawimy, e jedno okno stanie si czci innego okna to znaczy, e je zadokowalimy lub zakotwiczylimy. Podczas przecigania okna zdolnego do zadokowania widoczny jest jego kontur, ktry wyranie zmienia swj ksztat w momencie, gdy okno znajdzie si w tzw. docelowej pozycji dokowania (dock site) upuszczenie okna w tym momencie spowoduje faktyczne jego zadokowanie. Edytor kodu umoliwia dokowanie okien przy lewej, dolnej lub grnej krawdzi swego okna. Odmian dokowania jest poczenie kilku okien w pojedyncze okno o tzw. strukturze notatnikowej, czyli podzielone na strony reprezentowane przez poszczeglne zakadki; efekt taki uzyskuje si poprzez upuszczenie przeciganego okna wewntrz okna docelowego. Za pomoc menu kontekstowego dowolnej z zakadek mona wybra ich pooenie (przy dowolnej z czterech krawdzi okna). Jeeli chcemy, by dane okno nie posiadao zdolnoci dokowania, naley wyczy opcj Dockable w jego menu kontekstowym lub trzyma nacinity klawisz Ctrl w czasie jego (okna) przecigania. Poczwszy od Delphi 5, ukad okien w rodowisku IDE moe by zapisany pod wskazan nazw temu celowi suy pasek narzdziowy Desktops.

1 Aby mechanizm uzupeniania klas mg rozrni, czy identyfikator wystpujcy w ramach klauzuli read (write) jest nazw pola, czy te nazw metody dostpowej, obowizuj pewne dodatkowe reguy np. nazwa pola powinna rozpoczyna si od litery F (przyp. tum.).

39

5. Object Browser
W pocztkowych wersjach Delphi do Delphi 4 wcznie przegldarka obiektw (Object Browser) bya w powszechnej opinii uytkownikw narzdziem do nieciekawym. Sytuacja ta zmienia si w Delphi 5 zgodnie ze sw nazw Object Browser umoliwia odtd wgld w hierarchi obiektw, czego przykad przedstawia rysunek 1.6; moliwa jest nawigacja pomidzy zmiennymi globalnymi, klasami i moduami, moliwy jest rwnie wgld w struktur dziedziczenia, w odwoania do symboli i w zakresy deklaracji. Object Browser uruchamia si za pomoc polecenia View|Browser menu gwnego.

Rysunek 1.6. Object Browser w nowym wcieleniu

6. GUID
To jedno z niepozornych, aczkolwiek bardzo uytecznych narzdzi: naciskajc kombinacj klawiszy Ctrl+Shift+G spowodujemy wygenerowanie (w oknie edytora kodu) unikatowego symbolu GUID. Pozwala to zaoszczdzi czas przy definiowaniu nowych interfejsw.

7. Wyrnianie skadni plikw C++


Analogicznie do wyrniania skadni plikw *.PAS, moliwe jest wyrnianie elementw syntaktycznych jzyka C++ w plikach *.CPP i *.H. Realizacja tego nie bya specjalnie skomplikowana, poniewa edytory Delphi i C++Buildera zostay zbudowane na podstawie tego samego kodu rdowego.

8. Znaczniki To Do
Podczas tworzenia projektu metod od ogu do szczegw naturaln tendencj jest zostawianie pewnych rzeczy do wykonania na pniej. Duym uatwieniem w tym wzgldzie s tzw. znaczniki To Do (dos. do

40

zrobienia) dla kompilatora s one zwykymi komentarzami, zaczynaj si jednak od frazy TODO i z punktu widzenia edytora kodu zawieraj werbaln informacj o zadaniu do wykonania, jak rwnie adnotacje o autorze tej informacji oraz o priorytecie i kategorii odnonego zadania. Przykadow list znacznikw TODO przedstawia rysunek 1.7.

Rysunek 1.7. Lista znacznikw TODO

9. Meneder projektw
Meneder projektw pozwala sprawnie zorganizowa prac nad duym projektem, zwaszcza skadajcym si z wielu moduw. Na szczegln uwag zasuguje moliwo przecigania pomidzy projektami poszczeglnych elementw, jak rwnie ich wymiana za pomoc schowka. Mimo to meneder projektw jest narzdziem niedocenianym zadziwiajce, jak wielu programistw zapomina wrcz o jego istnieniu. Okno menedera projektw otwiera si za pomoc opcji View|Project Manager menu gwnego.

10. Code Insight


Trudno pamita szczegy dotyczce pl, metod i waciwoci obiektw, trudno te wdrowa do deklaracji klasy kadorazowo, gdy chcemy odwoa si do jednego z jej obiektw. W zwizku z tym Delphi udostpnia swoisty mechanizm podpowiedzi (o nazwie Code Insight) gdy postawimy kropk po identyfikatorze obiektu (lub rekordu), po krtkiej chwili ujrzymy podpowied zawierajc deklaracje wszystkich waciwoci, pl, metod i zdarze odnoszcych si do tego obiektu; poszczeglne elementy w ramach podpowiedzi mog by posortowane wedug nazw lub wedug zakresw deklaracji (wyboru dokonuje si za pomoc menu kontekstowego). Gdy okno podpowiedzi zamknite zostanie przedwczenie, mona je otworzy ponownie za pomoc kombinacji Ctrl+<spacja>. Podobnie rzecz si ma z parametrami funkcji, procedury lub metody: wywietlenie podpowiedzi zawierajcej deklaracj nastpuje wkrtce po napisaniu nawiasu otwierajcego list parametrw. Przedwczenie zamknite okno mona otworzy ponownie za pomoc kombinacji Ctrl+Shift+<spacja>.

Podsumowanie
W niniejszym rozdziale przedstawilimy ogln charakterystyk Delphi i niektre szczegy jego kolejnych wersji take w kontekcie oglnie pojtego programowania w Windows. Kolejne rozdziay obfitowa bd w rnorodne szczegy techniczne, wskazane jest zatem przed dalsz lektur nabycie pewnej wprawy w posugiwaniu si IDE i operowaniu jego poszczeglnymi mechanizmami.

41

Rozdzia 2.

Jzyk Object Pascal


Pozostawmy na chwil wizualne aspekty Delphi i przyjrzyjmy si bliej stanowicemu podstaw Delphi jzykowi Object Pascal. Poniewa niniejsza ksika przeznaczona jest raczej dla zaawansowanych czytelnikw, z jednej strony ograniczylimy si jedynie do zestawienia najwaniejszych cech tego jzyka, z drugiej natomiast wprowadzilimy pewne porwnania jego elementw z innymi jzykami wysokiego poziomu jak C++, Visual Basic i Java przy zaoeniu, e Czytelnik posiada o nich podstawow wiedz. Obecna wersja Object Pascala rni si znacznie od tej z Delphi 1 czy Delphi 2, zalecamy wic uwane przestudiowanie treci niniejszego rozdziau nie da si bowiem w peni wykorzysta Delphi bez dogbnej znajomoci jzyka, na bazie ktrego zostao zbudowane.

Notatka

Ilekro wspominamy w niniejszym rozdziale o jzyku C, mamy na myli elementy wsplne dla C i C++. Gdy mowa jest o elementach specyficznych dla C++ zaznaczamy to wyranie.

Komentarze
Komentarz jest najprostszym elementem jzyka programowania stanowi swobodny tekst majcy znaczenie jedynie dla czytelnoci programu; przez kompilator jest on cakowicie ignorowany. Object Pascal dopuszcza trzy rodzaje ogranicznikw komentarza: nawiasy klamrowe { .. }, znane z Turbo Pascala, ograniczniki typu nawias-gwiazdka (* ... *), rwnie wystpujce w Turbo Pascalu, podwjny ukonik // (ang. double slash), zapoyczony z jzyka C++. Oto przykady poprawnych komentarzy w Object Pascalu:
{ to jest komentarz jzyka Object Pascal, podstawy Delphi 6 }

47

(* to rwnie jest komentarz, tylko z innymi ogranicznikami *)

//Ten komentarz musi zmieci si w jednej linii

// // // // //

Ten komentarz zosta, dla odmiany, podzielony pomidzy kilka linii, z ktrych kada traktowana jest przez kompilator jako niezaleny komentarz, cho przecie dla uytkownika nie ma to adnego znaczenia.

Za koniec komentarza rozpoczynajcego si od podwjnego ukonika przyjmuje si koniec linii.

Ostrzeenie

Komentarze tej samej postaci nie mog by zagniedane, gdy jest to sprzeczne z reguami skadniowymi jzyka. Na przykad w poniszym przykadzie

{ prba zagniedenia komentarza } }

pocztkiem komentarza jest oczywicie pierwszy z nawiasw otwierajcych, lecz kocem pierwszy, nie drugi nawias zamykajcy. Nie ma jednak adnych przeszkd, aby zagnieda komentarze rnych typw, na przykad: (* Poniszy komentarz {nadmiar} wskazuje bdn instrukcj *) { ponisze komentarze w formie (* tekst do usunicia } W komentarzu rozpoczynajcym si od podwjnego ukonika znaki { } (* *) mog oczywicie wystpowa bez adnych ogranicze, gdy kocem komentarza nie jest aden wyrniony znak, lecz koniec linii. *) ograniczaj

Nowoci w zakresie procedur i funkcji


Poniewa procedury i funkcje stanowi najbardziej uniwersalny element wszystkich jzykw algorytmicznych, zrezygnujemy w tym miejscu z ich wyczerpujcego opisu (ktry Czytelnik znale moe m.in. w dokumentacji Delphi), koncentrujc si na tych ich cechach, ktre odrniaj Object Pascal od Turbo Pascala oraz tych, ktre pojawiy si w pniejszych wersjach Delphi (poczwszy od Delphi 3).

Puste nagwki wywoa


Wyjtkowo, opisywana w tym miejscu konstrukcja obecna jest w Object Pascalu ju od Delphi 2, mimo to jest jednak na tyle mao popularna, i zasuguje przynajmniej na krtk wzmiank. Ot, wywoujc funkcj lub procedur nie posiadajc parametrw, moemy uy pary nawiasw na wzr jzyka C lub Java, na przykad:
Form1.Show();

48

... R := CurrentDispersion();

co oczywicie jest rwnowane


Form1.Show; ... R := CurrentDispersion;

Nie jest to moe adna rewelacja jzykowa, lecz z pewnoci drobne uatwienie ycia programistom, ktrzy oprcz Object Pascala uywaj rwnie jzykw wczeniej wymienionych, w ktrych uycie wspomnianych nawiasw jest obowizkowe.

Przecianie
W Delphi 4 wprowadzono mechanizm przeciania (overloading) procedur i funkcji, umoliwiajcy zdefiniowanie caej rodziny procedur (funkcji), posiadajcych t sam nazw, lecz rnicych si postaci listy parametrw wywoania, na przykad:
function Divide(X, Y: Real): Real; overload; begin Result := X/Y; end; function Divide(X, Y: Integer): Integer; overload; begin Result := X div Y; end;

Informacj o przecieniu procedury funkcji jest klauzula overload (jak w powyszym przykadzie). Kompilator, analizujc posta wywoania przecionej procedury (funkcji), automatycznie wybierze waciwy jej egzemplarz (zwany czsto jej aspektem), aby jednak zadanie to byo wykonalne, poszczeglne aspekty faktycznie musz rni si od siebie. Nie jest tak chociaby w poniszym przykadzie:
procedure Cap(S: string); overload; ... procedure Cap(var Str: string); overload; ...

Wywoanie
Cap(S);

pasuje bowiem do obydwu aspektw kompilator uzna drugi z nich za prb redefinicji pierwszego i zasygnalizuje bd. Przecianie procedur i funkcji jest chyba najbardziej oczekiwan nowoci Object Pascala od czasu Delphi 1 i, jakkolwiek bardzo podane i uyteczne, stanowi jedno z odstpstw od rygorystycznej kontroli typw danych (tak charakterystycznej dla pocztkw Pascala). Mamy wszak do czynienia z rnymi procedurami (funkcjami), kryjcymi si pod t sam nazw przypomina to identycznie nazwane procedury (funkcje) rezydujce w rnych moduach. Naley wic korzysta z przeciania rozsdnie, a w adnym wypadku nie naley go naduywa. Zagadnieniem podobnym do przeciania procedur i funkcji jest przecianie metod, ktrym zajmiemy si w dalszej czci niniejszego rozdziau.

Domylne parametry
To kolejna nowo wprowadzona w Delphi 4, umoliwiajca uproszczenie wywoa procedur i funkcji poprzez pominicie jednego lub wicej kocowych parametrw listy. W treci procedury (funkcji) parametry te posiada bd wartoci domylne, ustalone w jej definicji. Oto przykad deklaracji procedury z jednym parametrem domylnym:
procedure HasDefVal( S: String; I: Integer = 0);

Poniewa drugiemu z parametrw definicja przyporzdkowuje domyln warto 0, wic wywoanie


HasDefVal('Hello');

49

rwnowane jest wywoaniu


HasDefVal('Hello', 0);

Parametry z wartociami domylnymi musz wystpi w kocowej czci listy nie mog by przemieszane z pozostaymi parametrami, tak wic ponisza deklaracja
Procedure NoProperDefs( X: Integer = 1; Y : Real);

jest bdna, poprawna jest natomiast deklaracja


Procedure ProperDefs( X: Integer = 1; Y : Real = 0.0);

Parametry z deklarowan wartoci domyln nie mog by przekazywane przez referencj (var), lecz jedynie przez warto lub przez sta (const); wie si to z oczywistym wymogiem, aby parametr aktualny wywoania by wyraeniem o dajcej si ustali wartoci. Wymg ten narzuca rwnie ograniczenie na typ parametru, ktremu przypisuje si warto domyln nie mog w tej roli wystpi rekordy, zmienne wariantowe, pliki, tablice i obiekty. Ograniczeniu podlega take sama warto domylna przypisywana parametrowi moe ona by wyraeniem typu porzdkowego, wskanikowego lub zbiorowego. Pewnego komentarza wymaga uycie parametrw domylnych w poczeniu z przecianiem procedur i funkcji. Naley uwaa, aby nie uniemoliwi kompilatorowi jednoznacznego zidentyfikowania waciwego aspektu procedury (funkcji) przecianej rozrnienie takie nie jest moliwe chociaby w poniszym przykadzie:
procedure Confused(I: Integer); overload; ... procedure Confused(I: Integer; J: Integer = 0); overload; ...

var X: Integer; begin ... Confused(X);

//

Ta instrukcja spowoduje bd kompilacji

Mechanizm parametrw domylnych oddaje nieocenione usugi przy unowoczenianiu istniejcego oprogramowania. Zamy na przykad, i nastpujc procedur
Procedure MyMessage( Msg:String);

wywietlajc komunikat w rodkowej linii okna, chcielibymy wzbogaci w moliwo jawnego wskazania linii (poprzez drugi parametr). Nawet jeeli przyjmiemy, i podanie 0 jako numeru linii oznacza bdzie tradycyjne wywietlanie w rodkowej linii, to i tak nie zwolni nas to z modyfikacji wszystkich wywoa procedury MyMessage() w kodzie programu. cilej byoby tak, gdyby nie mechanizm domylnych parametrw, bowiem poczwszy od Delphi 4 moemy dopuci wywoanie procedury MyMessage() z jednym parametrem, przyjmujc w takiej sytuacji, i opuszczony, domylny drugi parametr ma warto 0:
Procedure MyMessage ( Msg : String; Line: byte = 0 );

Wwczas wywoanie
MyMessage('Hello', 1)

spowoduje wywietlenie komunikatu w pierwszej linii okna, natomiast efektem wywoania


MyMessage('Hello')

bdzie wywietlenie komunikatu w linii rodkowej. I nie trzeba przy tym niczego zmienia, poza oczywicie sam procedur MyMessage().

Zmienne
W jzyku C i w Javie moliwe jest deklarowanie zmiennych dopiero w momencie, gdy faktycznie okazuj si potrzebne, na przykad:
void foo(void) { int x = 1; x++; int y = 2; float f; // ... i tak dalej

50

Natomiast w jzyku Pascal wszystkie deklaracje zmiennych musz by zlokalizowane przed blokiem kodu danej procedury, funkcji lub programu gwnego:
Procedure Foo; var x, y : integer; f : double; begin x := 1; Inc(x); y := 2; (* itd. *) end;

Moe si to wydawa nieco krpujce, lecz w rzeczywistoci prowadzi do programu bardziej czytelnego i mniej podatnego na bdy co jest charakterystyczne dla Pascala, stawiajcego raczej na bezpieczestwo aplikacji ni hodowanie okrelonym konwencjom.

Notatka

Object Pascal i Visual Basic, w przeciwiestwie do Javy i C, niewraliwe s na wielko liter w nazwach elementw skadniowych. Wraliwo taka zwiksza co prawda elastyczno jzyka, lecz niepomiernie zwiksza rwnie prawdopodobiestwo popenienia trudnych do wykrycia bdw. W Pascalu raczej trudno odczu brak takiej elastycznoci, za to programista ma do du swobod stylistyczn w pisowni nazw, na przykad nazwa

prostesortowaniemetodprzesiewaniazograniczeniami

staje si bardziej czytelna, gdy jest zapisana w tzw. notacji wielbdziej

ProsteSortowanieMetodPrzesiewaniaZOgraniczeniami

Deklaracje zmiennych tego samego typu mog by czone, na przykad deklaracj


var Zmienna1 : integer; Zmienna2 : integer;

mona skrci do postaci


var Zmienna1, Zmienna2 : integer;

Jak wida, po licie zmiennych nastpuje dwukropek i nazwa typu. Nadawanie zmiennym wartoci odbywa si w innym miejscu ni ich deklarowanie, mianowicie w treci programu. Nowoci, ktra pojawia si w Delphi 2 jest moliwo inicjowania zmiennych ju podczas ich deklaracji, na przykad:
var i P s d : : : : integer = 10; Pointer = NIL; String = 'Napis domylny'; Double = 3.1415926 ;

Jest to jednak dopuszczalne wycznie dla zmiennych globalnych; kompilator nie zezwoli na inicjowanie w ten sposb zmiennych lokalnych w procedurach i funkcjach.

Wskazwka

51

Kompilator dokonuje rwnie automatycznej inicjalizacji wszystkich zmiennych globalnych bez deklarowanej wartoci pocztkowej, zerujc zajmowan przez nie pami; tak wic wszystkie zmienne cakowitoliczbowe otrzymuj warto 0, zmiennoprzecinkowe warto 0.0, acuchy staj si acuchami pustymi, wskaniki otrzymuj warto NIL itp.

Stae
Stae (ang. constants) s synonimami konkretnych wartoci wystpujcych w programie. Deklaracja staych poprzedzona jest sowem kluczowym const i skada si z jednego lub wicej przypisa wartoci nazwom synonimicznym, na przykad:
const DniWTygodniu = 7; Stanowisk = 7; TaboretyNaStanowisku = 4; TaboretyOgolem = TaboretyNaStanowisku * Stanowisk; Komunikat = 'Przerwa niadaniowa';

Zasadnicz rnic midzy Object Pascalem a jzykiem C w zakresie deklaracji staych jest to, e Pascal nie wymaga deklarowania typw staych; kompilator sam ustala typ staej na podstawie przypisywanej wartoci. Ponadto, stae synonimiczne typw skalarnych nie zajmuj dodatkowego miejsca w pamici programu, gdy istniej jedynie w czasie jego kompilacji. Wic na przykad nastpujce deklaracje jzyka C:
const float AdecimalNumber = 3.14 const int i = 10 const char *ErrorString = "Uwaga, niebezpieczestwo";

posiadaj nastpujce odpowiedniki w Pascalu:


const AdecimalNumber = 3.14; i = 10; ErrorString = 'Uwaga, niebezpieczestwo';

Kompilator, ustalajc typ staej, wybiera typy o moliwie najmniejszej liczebnoci. Typ staej cakowitoliczbowej ustalany jest w nastpujcy sposb:

Tabela 2.1 Ustalane przez kompilator typy staych cakowitoliczbowych

Warto od 2^63 do 2.147.483.649 od 2.147.483.648 do 32.769 od 32.768 do 129 od 128 do 1 od 0 do 127 od 128 do 255 od 256 do 32.767 od 32.768 do 65.535 od 65.536 do 2.147.483.647 od 2.147.483.648 do 4.294.967.295 od 4.294.967.296 do 2^631

Typ
Int64 Longint Smallint Shortint 0 .. 127 Byte 0 .. 32.767 Word 0 .. 2.147.483.647 Cardinal Int64

52

Ponadto stae o wartociach rzeczywistych s staymi typu Extended; stae acuchowe, zalenie od ustawienia przecznika kompilacji $H, s albo acuchami typu ShortString, albo acuchami typu AnsiString; dla zbiorw (sets) liczbowych i znakowych rozmiar zajtej pamici wynika bezporednio z ich postaci.

Aby uzyska wiksz kontrol nad danymi, programista moe jawnie wskaza typ deklarowanej staej, na przykad:
const ADecimalNumber : Double = 3.14; i : integer = 10; ErrorString : string = 'Uwaga, niebezpieczestwo';

Staa ADecimalNumber jest teraz sta typu Double, bez jawnego wskazania typu byaby natomiast sta typu Extended. Jawne wskazywanie typw w deklaracjach staych zasuguje na nieco wicej uwagi. Programista znajcy Turbo Pascal rozpozna w tych konstrukcjach zmienne inicjowane, ktre zostay nazwane staymi przez nieporozumienie, gdy w rzeczywistoci zachowuj si w programie jak zwyke zmienne; mg si o tym przekona kady, kto chcia uy tak zdefiniowanej staej jako np. indeksu granicznego w deklaracji tablicy. Tak byo we wszystkich wersjach Turbo Pascala i w Delphi 1, natomiast wersja Delphi 2 przyniosa pewn nowo w tej materii: ot przy wczonym przeczniku {$J} wszystko jest po staremu, jednak jego wyczenie spowoduje, i kompilator zabroni modyfikowania tak deklarowanych staych. Zdecydowanie zaleca si t drug ewentualno stae pozostaj wwczas naprawd staymi, za zmiennym mona nadawa pocztkowe wartoci za pomoc dyrektywy var. W wyraeniach przypisywanych staym (oraz przy inicjowaniu zmiennych) Object Pascal zezwala na wykorzystanie nastpujcych funkcji wbudowanych: Abs(), Chr(), Hi(), High(), Length(), Lo(), Low(), Odd(), Ord(), Pred(), Round(), SizeOf(), Succ(), Swap() i Trunc() na przykad w taki sposb:
type A = array [ 1 .. 2 ] of Integer; const W : Word = SizeOf(byte) var i : integer = 8; j : SmallInt = Ord('a'); L : Longint = Trunc(3.14159); x : ShortInt = Round(2.71828); B1 : byte = High(A); B2 : byte = Low(A); C : Char = Chr(46);

Dopuszczalne jest take rzutowanie typw, na przykad


const BigOne = Int64(1);

Wskazwka

Ku rozczarowaniu wielu programistw, Object Pascal nie posiada preprocesora podobnego do tego z jzyka C. Nie mona wic definiowa makroinstrukcji i std brak mechanizmu odpowiadajcego sowu kluczowemu #define jzyka C (dyrektywa $define ma znaczenie zupenie inne definiuje tzw. symbole warunkowe kompilacji). Jednak, poczwszy od Delphi 6, mona wykorzysta dyrektywy $IF i $ELSEIF umoliwiajce uycie definiowanych staych na rwni z symbolami kompilacji warunkowej.

53

Operatory
Operatory s symbolami jzyka sucymi mwic najoglniej do manipulowania danymi. Istniej operatory arytmetyczne dodawania, odejmowania, mnoenia i dzielenia wartoci liczbowych, operator przypisania, wyboru elementu z tablicy itp. W niniejszym podrozdziale rozpatrzymy wikszo operatorw Object Pascala i przedstawimy ich odpowiedniki w jzykach C, Visual Basic i Java.

Operator przypisania
Operator przypisania suy do przypisania zmiennej wartoci; jest to bodaj najprostszy, lecz jednoczenie jeden z najwaniejszych operatorw jzyka. Oto jeden z przykadw jego zastosowania:
Number1 := 5;

Powysza instrukcja przypisuje zmiennej Number1 warto 5.

Operatory porwnania
Operatory porwnania w Delphi i w Visual Basicu s niemale identyczne. Su do stwierdzenia rwnoci lub nierwnoci dwch wartoci albo ich porwnania pod wzgldem relacji mniejszoci. W tabeli 2.2 przedstawione zostay cznie operatory porwnania i operatory logiczne, w tym miejscu chcemy jedynie zwrci uwag na istotn rnic midzy operatorem porwnania (= w Delphi, == w C i Javie) a operatorem przypisania (:= w Delphi, = w C i Javie). Operator badajcy nierwno dwch wielkoci, w jzyku C majcy sugestywn posta !=, w Pascalu ma posta
<>, na przykad:
if x <> y Then Cokolwiek

Operatory logiczne
Operatory logiczne realizuj (w ograniczonym zakresie) operacje wynikajce z algebry Boolea (std czsto nazywane bywaj operatorami boolowskimi ang. Boolean operators). Ich typowym zastosowaniem jest jednoczesne testowanie kilku warunkw, na przykad:
if (warunek1) and (warunek2) Then

Cokolwiek
While (warunek1) or (warunek2) do

Cokolwiek

Operatory logiczne obecne s w kadym jzyku programowania, chocia ich posta jest rnorodna. Tabela 2.2 przedstawia operatory porwnania oraz operatory logiczne w Pascalu, C, Javie i Visual Basicu.

Tabela 2.2. Operatory przypisania, porwnania i operatory logiczne Operator Pascal Java i C Visual Basic Przypisania Rwnoci Nierwnoci Mniejszoci Wikszoci Niewikszoci Niemniejszoci Logiczne i
:= = <> < > <= >= and = == != < > <= >= && = Is <> < > <= >= And

(dla obiektw) innych typw)

(dla

54

Logiczne lub Zaprzeczenie

or not

|| !

Or Not

Operatory arytmetyczne
Tabela 2.3 prezentuje operatory arytmetyczne Pascala, C, Javy i Visual Basica.

Tabela 2.3. Operatory arytmetyczne Operator Dodawania Odejmowania Mnoenia Dzielenia rzeczywistego Dzielenia cakowitego Reszty z dzielenia (modulo) Potgowania

Pascal
+ * / div mod

Java i C
+ * / / %

Visual Basic
+ * / \ Mod ^

brak

brak

Jak wynika z tabeli, Pascal i Visual Basic rozrniaj dzielenie liczb cakowitych (wynik jest liczb cakowit) od dzielenia liczb rzeczywistych (wynik jest liczb rzeczywist); Java i C nie czyni takiego rozrnienia.

Ostrzeenie

Wykonujc dzielenie, zawsze uywaj operatorw stosownych do operandw i oczekiwanego wyniku. Kompilator Object Pascala nie zezwoli na dzielenie cakowite operandw, z ktrych co najmniej jeden nie jest liczb cakowit. Rwnie powszechnym bdem jest prba przypisania zmiennej cakowitej wyniku dzielenia rzeczywistego (operator /), co ilustruje poniszy przykad: Var i : integer; r : real; begin i r i := := 4/3 3.4 div 2.3; // tu wystpi bd kompilacji // ta linia rwnie jest bdna // ta linia jest poprawna // ta linia rwnie jest poprawna

:= Trunc(4/3);

r := 3.4 / 2.3; end;

Jako ciekawostk odnotowa naley fakt, i wiele jzykw nie wykonuje dzielenia cakowitego i w zwizku z tym posiada jeden, uniwersalny operator dzielenia. Dzielenie dwch liczb cakowitych przebiega wic nastpujco: konwersja na typ zmiennoprzecinkowy, wykonanie dzielenia zmiennoprzecinkowego oraz konwersja wyniku (po zaokrgleniu lub obciciu rnie bywa) na typ cakowity. Jest to dziaanie kosztowne oraz nieefektywne w sytuacji, gdy procesor posiada instrukcje dzielenia cakowitego (posiadaj je wszystkie procesory 8086).

55

Operatory bitowe
Operatory bitowe su do operowania na poszczeglnych bitach wartoci binarnej reprezentujcej dane wyraenie. Operacje te zaliczy mona do jednej z dwch kategorii: logiczne operacje na bitach oraz przesuwanie bitw. Tabela 2.4 przedstawia operatory bitowe dla czterech rozpatrywanych tu jzykw. Tabela 2.4. Operatory bitowe Operator Koniunkcja Zaprzeczenie Alternatywa Dysjunkcja Przesunicie w lewo Przesunicie w prawo Pascal
and not or xor shl shr

Java i C
& | ^ << >>

Visual Basic
And Not Or Xor

nie istnieje nie istnieje

Operatory zwikszania i zmniejszania


Realizuj one zoptymalizowan operacj zwikszania (increment) lub zmniejszania (decrement) zmiennej typu porzdkowego. Operatory te wystpuj w dwch odmianach. Pierwsza z nich powoduje zmian wartoci zmiennej o 1 (w gr lub w d):
Inc(zmienna); Dec(zmienna);

i jest przez kompilator przekadana na pojedyncz instrukcj INC lub DEC kodu maszynowego. Operatory w postaci dwuargumentowej
Inc(zmienna, dystans); Dec(zmienna, dystans);

powoduj zmniejszenie albo zwikszenie zmiennej o warto zadan jawnie w postaci drugiego argumentu; operacja jest realizowana przez kompilator w postaci rozkazu ADD albo SUB.

Notatka

Kompilator Delphi w wersji 2 i nastpnych jest na tyle inteligentny, e sam rozpoznaje operacj zmniejszania/zwikszania zmiennej za pomoc zwykej operacji dodawania lub odejmowania, tak wic przekad instrukcji x := x + 1 nie rni si od przekadu instrukcji Inc(x)1, dlatego gwn korzyci wynikajc z uycia omawianych operatorw jest raczej wygoda programisty.

Zestawienie operatorw zwikszania i zmniejszania dla omawianych jzykw przedstawia tabela 2.5. Tabela 2.5. Operatory zwikszania i zmniejszania Operator Pascal Java i C Zwikszania Zmniejszania
Inc() Dec() ++

Visual Basic nie istnieje nie istnieje

Z wyjtkiem sytuacji, gdy x jest waciwoci klasy (przyp. tum.)

56

Operatory wykonaj i przypisz


Object Pascal, w przeciwiestwie do C i Javy, nie posiada operatorw oznaczajcych (mwic oglnie) wykonanie na zmiennej pewnej operacji i stanowicych pewne uoglnienie operatorw inc() i dec() nowa warto zmiennej musi by zapisana explicite po prawej stronie operatora przypisania, tak wic zupenie naturalna w C instrukcja
x += 5;

w Pascalu musi by zapisana jako


x := x + 5;

Typy jzyka Object Pascal


Jedn z najkorzystniejszych cech Object Pascala jest tzw. bezpieczestwo typw (ang. type safety). Oznacza to, e kompilator prowadzi rygorystyczn kontrol typw zmiennych biorcych udzia w operacjach i bdcych parametrami wywoa procedur i funkcji. Jakiekolwiek odstpstwo od cile zdefiniowanych regu powoduje bd kompilacji. Jednoczenie uytkownicy Pascala wolni s od tych wspaniaych ostrzee kompilatora o podejrzanych konstrukcjach z uyciem wskanikw, ktre to ostrzeenia s czym powszednim w jzyku C; s to jednak tylko ostrzeenia, niezdolne powstrzyma prb przysowiowego zatykania okrgej dziury kwadratowym korkiem Aby, zbawienna skdind, rygorystyczna kontrola typw pascalowskich nie bya dla uytkownikw zbyt krpujca, wprowadzono rne moliwoci jej obejcia amorficzne wskaniki (typ Pointer), nakadanie si zmiennych (dyrektywa absolute), typy wariantowe, amorficzne parametry procedur i funkcji itp. Jak w przypadku wszelkich mechanizmw tego typu, ich uyteczno uzaleniona jest od ich rozsdnego uywania.

Porwnanie typw
Wikszo typw Object Pascala posiada swe odpowiedniki w C, Javie i Visual Basicu. Zestawienie widoczne w tabeli 2.6 moe by niezwykle uyteczne w przypadku wykorzystywania w ktrym z tych jzykw bibliotek DLL stworzonych w innym jzyku.

Tabela 2.6. Porwnanie typw Pascala, Javy, C i Visual Basica Typ zmiennej Pascal Java cakowity 8-bitowy ze znakiem cakowity 8-bitowy bez znaku cakowity 16bitowy ze znakiem cakowity 16bitowy bez znaku cakowity 32bitowy ze znakiem cakowity 32bitowy bez znaku cakowity 64bitowy ze znakiem zmiennoprzecinkow y 4-bajtowy
ShortInt Byte SmallInt Word Integer, LongInt Cardinal, LongWord Int64 Single byte

C/C++
char BYTE, unsigned short short unsigned short int, long unsigned long __int64 float

Visual Basic nie istnieje


Byte

nie istnieje
short

Short

nie istnieje
int

nie istnieje
Integer, Long

nie istnieje
long float

nie istnieje nie istnieje


Single

57

zmiennoprzecinkow y 6-bajtowy zmiennoprzecinkow y 8-bajtowy zmiennoprzecinkow y 10-bajtowy staoprzecinkowy 64-bitowy data/czas 8-bajtowy wariantowy bajtowy znak 1-bajtowy znak 2-bajtowy acuch znakw o ustalonej maksymalnej dugoci dynamiczny acuch znakw 1bajtowych acuch znakw jednobajtowych z zerowym ogranicznikiem acuch znakw dwubajtowych z zerowym ogranicznikiem dynamiczny acuch znakw dwubajtowych boolowski bajtowy boolowski bajtowy boolowski bajtowy 12416-

Real48 Double Extended Currency TDateTime Variant, Olevariant, TVarData Char WideChar ShortString

nie istnieje
double

nie istnieje
double long double

nie istnieje
Double

nie istnieje nie istnieje nie istnieje nie istnieje

nie istnieje
Currency Date Variant

nie istnieje nie istnieje


VARIANT** Variant, Olevariant char WCHAR

(domylny) nie istnieje


Char

nie istnieje
char

nie istnieje

nie istnieje

nie istnieje

AnsiString

nie istnieje

AnsiString

String

PChar

nie istnieje

char *

nie istnieje

PWideChar

nie istnieje

LPCWSTR

nie istnieje

WideString

String**

WideString

nie istnieje

Boolean, ByteBool WordBool BOOL, LongBool

boolean

(dowolny bajtowy) (dowolny bajtowy)


BOOL

1- nie istnieje 2- Boolean nie istnieje

nie istnieje nie istnieje

oznacza tu klas C++ Buildera emulujc odnon klas Object Pascala


** oznacza powszechnie uywan klas lub typ, nie za rodzimy typ jzyka Wskazwka

Podczas przenoszenia aplikacji z Delphi 1 bd wiadom tego, e typy Integer i Cardinal, 16-bitowe w Delphi 1, s ju 32-bitowe w Delphi 2 oraz w nastpnych wersjach. W Delphi 4 zmienio si te znaczenie typu Cardinal: w Delphi 2 i 3 jego zakres tosamy by z nieujemn poow typu integer (bit znaku by po prostu ignorowany), natomiast poczwszy od Delphi 4 jest on penoprawn 4-bajtow liczb cakowit bez znaku o zakresie 0 4294967296.

58

Ostrzeenie

W Delphi 4 zmienio si rwnie znaczenie identyfikatora Real. W Delphi 1, 2 i 3 oznacza on specyficzny dla Turbo Pascala 6-bajtowy format liczby zmiennoprzecinkowej, obsugiwany cakowicie w sposb programowy i nie majcy odpowiednika w formatach danych (ko)procesorw. Poczwszy od Delphi 4, identyfikator Real jest synonimem typu Double; wspomnianemu formatowi 6-bajtowemu odpowiada natomiast identyfikator Real48. Moliwe jest jednak przywrcenie dawnego znaczenia identyfikatora Real poprzez uycie dyrektywy kompilatora {$REALCOMPATIBILITY ON}

Znaki
W Delphi istniej trzy typy reprezentujce pojedynczy znak:
AnsiChar to powszechny w wikszoci dotychczasowych jzykw 1-bajtowy znak ANSI, WideChar dwubajtowy znak ze zbioru znakw Unicode, Char w obecnej wersji Delphi jest to typ identyczny z AnsiChar, lecz Borland zastrzeg sobie prawo ewentualnego utosamienia go z typem WideChar w przyszych wersjach.

Fakt, e znak niekoniecznie jest teraz jednobajtowy, stanowi przesank nieco ostroniejszego kodowania, a przy ustalaniu rozmiaru struktur zawierajcych znaki, wskazane jest korzystanie z funkcji SizeOf().

Wskazwka

Funkcja SizeOf() zwraca rozmiar (w bajtach) zmiennej lub typu.

Mnogo acuchw
acuchem (string) nazywamy cig znakw i oczywicie reprezentujcy go obiekt jzyka programowania. To interesujce, e rnorako implementacji acuchw w rnych jzykach programowania jest znacznie wiksza ni w jakimkolwiek innym aspekcie jzyka. W Object Pascalu obsuga acuchw znakw zrealizowana zostaa w postaci nastpujcych typw:
AnsiString podstawowy typ acuchw danych w Object Pascalu. Jest cigiem (o potencjalnie nieograniczonej dugoci) znakw AnsiChar. Zgodny jest z typem reprezentujcym cig jednobajtowych znakw zakoczony bajtem zerowym. ShortString to stary pascalowy znajomy acuch znakw AnsiChar o ustalonej z gry maksymalnej

dugoci, nie przekraczajcej 255 znakw. Podstawowy typ acuchowy w Delphi 1.


WideString stanowi cig znakw WideChar i, podobnie jak AnsiString, nie ma limitowanej dugoci i

zakoczony jest znakiem zerowym.


PChar to wskanik do cigu znakw typu Char zakoczonego bajtem zerowym, na wzr typw char* lub lpstr w jzyku C. PAnsiChar to wskanik do zakoczonego znakiem zerowym cigu znakw typu AnsiChar. PWideChar jest to wskanik do zakoczonego znakiem zerowym cigu znakw typu WideChar.

Zmienna deklarowana jako String jest zmienn typu AnsiString lub ShortString, zalenie od ustawienia przecznika kompilacji $H:
var {$H-} S1 : String {$H+} // zmienna S1 jest typu ShortString

59

S2 : String

// zmienna S2 jest typu AnsiString

Zmienna deklarowana jako String z wyspecyfikowan maksymaln dugoci (nie wiksz ni 255) jest jednak zawsze typu ShortString:
var {$H-} S1[63] : String {$H+} S2[63] : String // zmienna S1 jest typu ShortString // zmienna S2 jest typu ShortString

Typ AnsiString
Typ AnsiString, zwany rwnie potocznie long string lub po polsku dugi acuch pojawi si po raz pierwszy w Delphi 2. Uosabia on t sam atwo obsugi, jak miay klasyczne acuchy pascalowe i jest jednoczenie wolny od bardzo dotkliwego ograniczenia dugoci do 255 znakw dugo acucha typu AnsiString jest praktycznie nieograniczona. Obsuga dugich acuchw wie si z do wyrafinowan, niewidoczn dla uytkownika gospodark pamici operacyjn, polegajc na jej przydziale stosownie do potrzeb i odzyskiwaniu (ang. garbage collection) wtedy, gdy nie jest ju potrzebna (za chwil zajmiemy si tym interesujcym zagadnieniem w szerszym kontekcie). Uwalnia to programist od rcznej obsugi pamici, tak uciliwej w C++ i wersji 7.0 Borland Pascala (typ PChar). Struktur acucha AnsiString w pamici operacyjnej przedstawia rysunek 2.1.

Rysunek 2.1. acuch AnsiString reprezentujcy napis DDG

Ostrzeenie

Wewntrzny format dugich acuchw Delphi nie zosta udokumentowany firma Borland pozostawia sobie w ten sposb moliwo jego modyfikacji w przyszoci. Aplikacje bazujce na tym formacie nios wic ze sob ryzyko ewentualnej niezgodnoci z przyszymi wersjami Delphi. Z podobnym problemem zetknli si swego czasu uytkownicy Delphi 1 zakadajcy, i bieca dugo acucha reprezentowana jest przez jego pocztkowy bajt.

Jeeli wic mimo wszystko prezentujemy tutaj wewntrzn struktur dugiego acucha, to czynimy to wycznie w celu pogldowego wytumaczenia zasad jego funkcjonowania.

Jak wida na rysunku 2.1, dugi acuch reprezentowany jest w zmiennej jako wskanik do cigu znakw. Systemowe procedury zarzdzajce obsug acuchw gwarantuj ponadto, e jest on zakoczony bajtem zerowym. Powoduje to, e dugie acuchy mog by uywane jako parametry wywoania procedur i funkcji Win32 API, wymagajcych acuchw z zerowym ogranicznikiem. Najbardziej nieoczywistym elementem rysunku jest natomiast z pewnoci licznik odwoa. Ot, w celu zminimalizowania zuycia pamici, Object Pascal stara si zapamitywa w pojedynczym egzemplarzu zawarto dwch (lub wicej) zmiennych acuchowych o (aktualnie) identycznej zawartoci, kontrolujc jedynie odwoania do tego egzemplarza za pomoc wspomnianego licznika. Tak wic przypisanie zmiennej acuchowej zawartoci innej zmiennej nie spowoduje fizycznego kopiowania, lecz tylko powielenie wskanika (fizyczn reprezentacj zmiennych acuchowych s bowiem wskaniki) i zwikszenie licznika odwoa. Modyfikacja ktrej z tych zmiennych spowoduje jednak zerwanie jej dotychczasowego zwizku ze wspomnianym egzemplarzem, zmniejszenie licznika odwoa z nim zwizanego i utworzenie nowego, niezalenego egzemplarza o zmienionej zawartoci. Poniszy przykad z pewnoci dostatecznie wyjania t koncepcj:
var S1, S2 : AnsiString; begin

60

S1 := 'Taki sobie napis ... ' { licznik odwoa dugiego acucha wskazywanego przez S1 jest rwny 1 } S2 := S1; { S1 i S2 wskazuj na ten sam dugi acuch, ktrego licznik odwoa jest teraz rwny 2 } S2 := S2 + ' i jeszcze co ... '; { Nastpio utworzenie niezalenego egzemplarza dla zmiennej S2, S1 i S2 wskazuj teraz na dwa rne obszary pamici. Licznik odwoa acucha wskazywanego przez S1 znowu jest rwny 1 }

Zmienne typu AnsiString jako przykad zmiennych o kontrolowanym czasie ycia Pojcie czasu ycia wynika z pojcia zakresu widocznoci deklaracji zmiennej. W Pascalu zmienne globalne yj wic przez cay czas realizacji programu, zmienne lokalne procedur i funkcji jedynie w czasie realizacji tyche procedur i funkcji. Ten oczywisty poniekd fakt nie powodowa adnych szczeglnych implikacji a do pojawienia si Delphi 2 i acuchw AnsiString, na potrzeby ktrych, w tle, realizowany jest skomplikowany scenariusz gospodarowania pamici.

W Turbo Pascalu, po zakoczeniu realizacji programu, zwalniana bya caa przydzielona mu pami i tym samym unicestwiane byy wszystkie zmienne globalne. Podobne unicestwienie zmiennych lokalnych procedur i funkcji sprowadzao si po prostu do zdjcia ich ze stosu. Rozwamy jednak poniszy przykad:
procedure MyProc; var X: Pointer; begin GetMem(X,10000); ... // tutaj procedura wykorzystuje do czegokolwiek // obszar wskazywany przez X ... FreeMem(X,10000); end;

Obszar wskazywany przez wskanik X istnieje tylko w czasie realizacji procedury MyProc i nie ma poza ni adnego znaczenia z tego prostego wzgldu, i jest na zewntrz niej niedostpny. Zamy teraz, i programista zapomnia o kocowej instrukcji FreeMem. Konsekwencj tego faktu byaby po prostu strata 10000 bajtw pamici przy kadym wywoaniu procedury. Std wany wniosek, i do zniwelowania skutkw przydziau pamici nie wystarczy proste zdjcie zmiennej X ze stosu.

Ten prosty przykad wyjania istot problemu, ktry pojawi si w momencie wprowadzenia do Pascala zmiennych, na potrzeby ktrych gospodarka pamici nie posiada mwic najprociej odzwierciedlenia w kodzie rdowym programu, lecz wykonywana jest w tle. Konkretnie w przypadku zmiennych typu AnsiString, w zwizku z zakoczeniem procedury nie wystarczy ju zwyke zdjcie ze stosu

61

zmiennej, zawierajcej li tylko wskanik do danych zasadniczych; z drugiej strony wykluczone s jakiekolwiek jawne instrukcje zwalniajce, gdy gospodarka pamici odbywa si tu bez zwizku z kodem rdowym programu. Std wniosek, i integraln czci procesu niejawnego gospodarowania pamici powinno by jej automatyczne zwalnianie w przypadku zakoczenia czasu ycia odnonej zmiennej. Proces taki rzeczywicie ma miejsce, a zmienne, ktrych on dotyczy, nosz nazw zmiennych z kontrolowanym czasem ycia (lifetime memory-managed). Opisywane w niniejszym punkcie zmienne typu AnsiString i WideString s wanie takimi zmiennymi inne przykady zmiennych tej kategorii przedstawimy w dalszej czci rozdziau i w rozdziaach nastpnych. Dokadniej proces obsugi zmiennych o kontrolowanym czasie ycia daje si pokrtce opisa nastpujco: zmienne globalne inicjowane s automatycznie podczas wykonywania sekcji initialization moduu, w ktrym zostay zdefiniowane (lub podczas rozpoczynania programu, o ile modu takiej sekcji nie posiada). Wszystkie czynnoci zwizane z zakoczeniem czasu ycia zmiennych odbywaj si rwnie automatycznie w czasie koczenia sekcji finalization tego moduu (lub podczas koczenia programu, jeli modu sekcji finalization nie posiada). W stosunku do zmiennych lokalnych procedur i funkcji odpowiednie dziaania nastpuj podczas wejcia do procedury i bezporednio przed wyjciem z niej. Skutek jest taki, jak gdyby tre procedury zanurzona zostaa w dodatkowym bloku tryfinally (oczywicie tylko pojciowo), co ilustruje nastpujcy przykad:
// rzeczywista posta procedury procedure Foo; var S: AnsiString; begin ... // ciao procedury wykorzystujce S ... end;

Koncepcyjnie wyglda to natomiast tak:


procedure Foo; var S: AnsiString; begin S := ''; // inicjalizacja try ... // ciao procedury wykorzystujce S ... finally // zwolnij zasoby przydzielone do S end; end;

Nie jest to zreszt nic nadzwyczajnego. Wrmy na chwil do procedury MyProc jeeli programista chciaby unikn zagubienia owych 10000 bajtw pamici na skutek wystpienia wyjtku, powinien sw procedur sformuowa mniej wicej tak:

62

procedure MyProc; var X: Pointer; begin X := NIL; try GetMem(X,10000); ... // tutaj procedura wykorzystuje do czegokolwiek // obszar wskazywany przez X ... finally if X <>NIL Then FreeMem(X,10000); end; end;

Analogia widoczna jest a nadto dobrze.

Operacje na acuchach
Do poczenia (konkatenacji) dwch acuchw suy operator + lub funkcja Concat() ta ostatnia zachowana zostaa ze wzgldw kompatybilnoci i naley jej raczej unika. Oto przykady czenia acuchw Object Pascala:
Var S, S2 : AnsiString; begin S := 'Szafa'; S2 := ' grajca'; S := S + S2; { Szafa grajca } end.

Var S, S2 : AnsiString; begin S := 'Szafa'; S2 := ' grajca'; S := Concat(S,S2); { Szafa grajca } end.

Wskazwka

Ogranicznikiem literaw acuchowych jest w Pascalu pojedynczy apostrof.

Notatka

Funkcja Concat() stanowi przykad magicznych funkcji kompilatora owa magia polega na tym, i funkcji tej nie da si napisa w Pascalu. Takich funkcji i procedur jest w Pascalu wicej e wspomnimy tylko o najpopularniejszych Readln(), Writeln(), New(), SizeOf(). Ich lista jest zamknita i cile

63

okrelona przy ich przekadzie na kod wynikowy kompilator posuguje si specjalnie zdefiniowanymi na t okazj moduami (ang. helper functions) zawartymi w bibliotece RTL i w module System.

Niezalenie od magicznych funkcji oraz procedur istnieje do pokany zestaw pascalowych podprogramw operujcych na acuchach; s one w wikszoci zlokalizowane w module SysUtils, a ich wykaz mona odnale w systemie pomocy pod hasem String-handling routines (Pascalstyle). Dodatkowo, kilka uytecznych podprogramw operujcych na acuchach moesz znale w module StrUtils, znajdujcym si na doczonym do ksiki krku CD-ROM w katalogu \Source\Utils.

Dugo acucha i alokacja pamici


Bezporednio po zadeklarowaniu, zmienna acuchowa nie posiada przydzielonej pamici i nie reprezentuje adnego napisu. Przypisanie jej jakiego napisu spowoduje przydzia pamici lub ustalenie wskazania na obszar ju przydzielony. Ilustruje to poniszy przykad:
Var S1, S2 : AnsiString; begin S1 := 'Pierwszy'; { zmiennej S1 przydzielono obszar pamici o wielkoci co najmniej 9 bajtw, zawierajcy obecnie napis 'Pierwszy' zakoczony bajtem zerowym } S2 := 'Drugi'; { zmiennej S2 przydzielono obszar pamici o wielkoci co najmniej 6 bajtw, zawierajcy obecnie napis 'Drugi' zakoczony bajtem zerowym } S1 := S2; { zmienne S1 i S2 wskazuj teraz na ten sam acuch. Obszar poprzednio wskazywany przez zmienn S2 nie jest ju do niczego potrzebny i moe zosta ponownie wykorzystany }

Mechanizm zarzdzania pamici na potrzeby dugich acuchw jest bardzo wyrafinowany, w jednym wszake przypadku nie zapewnia on dostatecznej dugoci acucha: wtedy, gdy odwoujesz si do niego na wzr tablicowy, uywajc indeksu explicite:
Var S : AnsiString; begin S[1] := 'a';

W powyszym przykadzie zmiennej S nie zostaa jeszcze przydzielona pami, jej zawartoci jest pusty wskanik, a wic odwoanie do S[1] gwarantuje wystpienie bdu wykonania. Jeeli jednak odwoanie powysze poprzedzone zostanie przypisaniem acuchowi S napisu co najmniej jednoznakowego, bd nie wystpi:
Var S, T begin S := S[1] T := T[1] : AnsiString; 'b'; := 'a' 'Ala ma kota'; := 'U';

Obecnie treci zmiennej S jest jednoznakowy napis 'a', natomiast treci zmiennej T jest zdanie 'Ula ma
kota'.

Istnieje jednak prostszy sposb na wymuszenie przydziau pamici dla dugiego acucha suy do tego funkcja SetLength():
Var S : AnsiString; begin Setlength(S,1); S[1] := 'a'

Wywoanie SetLength(S, 1) spowoduje przydzielenie zmiennej S obszaru pamici o wielkoci wystarczajcej przynajmniej na przechowanie napisu jednoznakowego.

64

Dugie acuchy a funkcje Win32 API


Procedury interfejsu Win32 API, jak i wielu innych interfejsw programisty, wymagaj jako parametrw rnorodnych tekstw, np. nazw katalogw, plikw. Wymagan ich postaci jest zakoczony bajtem zerowym cig znakw, a ich przekazanie do procedury nastpuje za pomoc wskanika; do rzadkoci nale rodowiska wymagajce innej postaci parametrw tekstowych. Z opisanych wasnoci dugich acuchw Object Pascala wynika ich znakomita przydatno do tego celu. Potrzebny jest jedynie drobny zabieg kosmetyczny kompilator nie zaakceptuje zmiennej typu String jako parametru aktualnego w miejscu, gdzie wymagany jest wskanik do cigu znakw (PChar), dlatego konieczne jest w tym przypadku rzutowanie typu (zjawiskiem tym zajmiemy si w dalszej czci rozdziau). Z natury dugiego acucha wynika, e takie rzutowanie jest operacj sensown, poza tym jest ono dozwolone przez reguy syntaktyczne kompilatora. Oto prosty przykad funkcji pobierajcej nazw biecego katalogu maksymalna dugo nazwy katalogu w systemach Windows 95 i Windows NT wynosi 260 znakw:
{$H+} var S : String; begin SetLength(S,260); GetWindowsDirectory(PChar(S), 260);

To jednak jeszcze nie koniec: jak wynika z rysunku 2.1, dugiemu acuchowi towarzysz dodatkowe informacje organizacyjne. Procedury Win32 API, traktujc parametry jako cigi znakw zakoczone bajtem zerowym (i nic ponadto), nie s owych dodatkw wiadome. Z tego te wzgldu, po wykonaniu procedury GetWindowsDirectory() w powyszym przykadzie, zmienna S nie jest jeszcze rasowym dugim acuchem programista musi sam uaktualni informacje dodatkowe. Da si to atwo wykona za pomoc wspomnianej funkcji SetLength() lub nieco wygodniejszej funkcji RealizeLength() znajdujcej si w module StrUtils:
procedure RealizeLength ( var S : String ); begin SetLength( S, StrLen(PChar(S)) ); end;

Oto kompletna posta procedury dostarczajcej nazw biecego katalogu:


Procedure GetCurrentDir ( var S : String ); begin SetLength(S,260); // przydzia pamici GetWindowsDirectory(PChar(S), 260); // pobranie treci RealizeLength(S); // uaktualnienie informacji organizacyjnych end;

Ostrzeenie

Poniewa dugi acuch podlega procesowi odzyskiwania pamici (garbage collection), naley zachowa ostrono podczas jego rzutowania na typ PChar naley mianowicie upewni si, i zmienna stanowica argument rzutowania nie zakoczya jeszcze swego ycia. W kontekcie przedstawionych wczeniej informacji na temat zmiennych o kontrolowanym czasie ycia, wymaganie takie wydaje si zupenie oczywiste.

Dugie acuchy a przenoszenie aplikacji


Moesz w ogle zrezygnowa z dugich acuchw, konsekwentnie uywajc przecznika $H i wtedy typ String z Delphi 1 zachowuje swoje znaczenie w nastpnych wersjach Delphi. Jeeli jednak chcesz wykorzysta zalety dugich acuchw, musisz nieco zmodyfikowa kod aplikacji. W Delphi 1 typ String reprezentowa klasyczny acuch znakw poprzedzony bajtem oznaczajcym jego dugo, w wersjach nastpnych, przy ustawionym przeczniku $H+, typ String reprezentuje dugi acuch, majcy diametralnie rn struktur. W zwizku z tym: Wszystkie odwoania do typu PString naley zamieni na String (reprezentacj zmiennych tego ostatniego s bowiem wskazania na acuchy).

65

Naley pamita, e dugo acucha nie jest ju reprezentowana przez jego zerowy element! Tak wic prba odczytania (zapisania) S[0] jako dugoci acucha S jest bezsensowna zamiast tego naley uy funkcji Length() (SetLength()). Funkcje StrPas() i StrPCopy(), stanowice w Delphi 1 pomost midzy klasycznymi acuchami oraz acuchami z zerowym ogranicznikiem, nie s ju do niczego potrzebne. Przepisania cigu znakw typu PChar do typu String dokonuje si obecnie za pomoc zwykego operatora przypisania
StrVar := PCharVar;

zamiast niegdysiejszego
StrVar := StrPas(PCharVar);

Typ ShortString
Klasyczne pascalowe acuchy dostpne s nadal pod postaci typu ShortString, funkcjonujcego niezalenie od ustawienia przecznika $H. Dla uproszczenia bdziemy w typ nazywa krtkim acuchem. Pod wzgldem strukturalnym krtki acuch jest tablic jednobajtowych znakw, indeksowan poczwszy od 1 i poprzedzon bajtem zawierajcym biec dugo (bajt ten uwaany jest take za zerowy element wspomnianej tablicy). Ilustruje to rysunek 2.2.

Rysunek 2.2. acuch ShortString reprezentujcy napis DDG Zarzdzanie krtkimi acuchami nie wie si z dynamicznym gospodarowaniem pamici pami dla zmiennej przydzielana jest od razu zgodnie z jej zadeklarowan dugoci, zmienne typu ShortString nie s wic zmiennymi o kontrolowanym czasie ycia. Operacje na krtkich acuchach s wic bardzo efektywne, jednak dotkliwe jest ograniczenie ich maksymalnej dugoci do 255 znakw. Oto przykad przypisania wartoci krtkiemu acuchowi:
var S : ShortString; begin S := 'Krtki napis';

Dla oszczdnoci pamici, moemy ograniczy maksymaln dugo krtkiego acucha, wskazujc j (w deklaracji) w nawiasach kwadratowych, na przykad:
var Nazwisko: string[20];

Zgodnie z powysz deklaracj zmienna Nazwisko zajmuje w pamici 21 bajtw. Gdybymy zadeklarowali j jako
var Nazwisko: ShortString;

zajmowaaby 256 bajtw. Przypisywanie krtkiemu acuchowi napisu zbyt dugiego w stosunku do zadeklarowanej dugoci jest bezpieczne dla aplikacji z zastrzeeniem, i kocwka napisu zostaje utracona. W wyniku wykonania poniszej sekwencji
var Napis: String[8]; Napis := 'Zbyt dugi napis';

zmienna Napis zawiera bdzie napis Zbyt du.

66

Nie jest natomiast bezpieczne odwoywanie si do poszczeglnych pozycji acucha poza zadeklarowan dugoci ponisza sekwencja z duym prawdopodobiestwem spowoduje bd wykonania, w kadym razie jej skutki s nieokrelone (chyba e ustawiono przecznik $R+, wtedy wykryte zostanie przekroczenie dopuszczalnego zakresu):
var Napis: String[8]; i: integer; i := 10; Napis[i] := '*';

Notabene gdy napiszemy to bardziej bezporednio


var Napis: String[8]; Napis[10] := '*';

bd zostanie wykryty ju na etapie kompilacji na takie prymitywne sztuczki kompilator jest bowiem za mdry.

Wskazwka

Mimo i ustawienie przecznika $R+ moe przyczyni si do wykrycia wielu bdw zwizanych m.in. z przekroczeniem zadeklarowanego zakresu tablicy lub acucha, zwizane z tym testy generalnie spowalniaj wykonanie aplikacji, wic po (wystarczajcym) przetestowaniu aplikacji, naley w przecznik wyczy.

W przeciwiestwie do dugich acuchw, krtkie acuchy zupenie nie nadaj si na parametry wywoa wikszoci funkcji Win32 API, nie posiadaj bowiem zerowego ogranicznika. Ich przeksztacenie do postaci z zerowym ogranicznikiem nie sprowadza si do zwykego rzutowania typw, lecz wymaga pewnych dodatkowych operacji. Zajmuje si tym ponisza funkcja z moduu StrUtils:
Function ShortStringAsPChar ( var S : ShortString ) : PChar; { Bieca zawarto zmiennej S musi by krtsza ni maksymalna zadeklarowana dugo, inaczej ostatni znak zostanie zignorowany. } begin if Length(S) = High(S) Then Dec (S[0]); S[Ord(Length(S)) + 1] := #0; Result := @S[1]; end;

Oto jeszcze jeden przykad przeksztacenia dugiego acucha w acuch z zerowym ogranicznikiem:
Function ShortToASCIIZ ( var S : ShortString ) : PChar; // A. Grayski var k: byte; begin k := Length(S); if k > High(S) // to na wypadek, gdyby S[0] zawierao zbyt du warto then k := High(S); Move(S[1], S[0], k); S[k] := #0; ShortToASCIIZ := @S[0]; end;

Procedura ShortToASCIIZ nie wymaga ograniczenia dugoci acucha wejciowego, powoduje jednak zniszczenie jego zawartoci. Moemy jednak t zawarto odzyska, dokonujc prostego wywoania

67

S := StrPas(X);

gdzie X jest wskanikiem o wartoci zwrconej przez funkcj ShortToASCIIZ (wskanik ten straci oczywicie sw wano):
var S: ShortString; X: PChar; begin ... S := 'Jaki napis'; X := ShortToASCIIZ(S) ... JakasFunkcjaAPI(X); ... S := StrPas(X); // teraz wskanik X nie moe ju zosta uyty

Typ WideString
acuchy typu WideString s odpowiednikami acuchw AnsiString w wiecie dwubajtowych znakw WideChar. S one rwnie dynamicznie alokowane, a ich zmienne s zmiennymi o kontrolowanym czasie ycia. Ponadto typy AnsiString oraz WideString s ze sob zgodne w sensie przypisania, istniej jednak trzy podstawowe rnice pomidzy nimi: acuchy WideString skadaj si z dwubajtowych znakw WideChar, co czyni je odpowiednimi do przechowywania sw utworzonych nad alfabetem kompatybilnym z kodem Unicode. Pami dla acuchw WideString alokowana jest za pomoc funkcji API SysAllocStrLen(), co czyni je kompatybilnymi z charakterystycznymi dla OLE acuchami typu BSTR. W odniesieniu do acuchw WideString nie jest prowadzona oszczdnociowa polityka wspdzielenia egzemplarzy na podstawie licznika odwoa. Oznacza to, e acuchy WideString, identyczne pod wzgldem zawartoci, lecz zwizane z rnymi zmiennymi zawsze istniej w postaci oddzielnych egzemplarzy. Oznacza to rwnie, i kada operacja przypisania pomidzy dwiema zmiennymi typu WideString realizowana jest przez rzeczywiste kopiowanie zawartoci acucha. Czyni to oczywicie acuchy WideString mniej efektywnymi od acuchw AnsiString pod wzgldem szybkoci operowania oraz wykorzystania pamici. Jak wspomniano wczeniej, typy AnsiString oraz WideString s ze sob zgodne w sensie przypisania wszystkie niezbdne konwersje wykonywane s automatycznie. Poniszy fragment programu jest wic poprawny w Object Pascalu:
var W: WideString; S: AnsiString; begin W := 'Margaritaville'; S := W; S := 'Come Monday'; W := S; end;

acuchy WideString mog take wystpowa jako parametry standardowych funkcji operujcych na acuchach Concat(), Copy(), Insert(), Pos(), SetLength(), Length() itp. mog by rwnie argumentami operatorw +, = i <>, na przykad:
var W1, W2 : WideString; P: Integer; begin W1 := 'Enfield'; W2 := 'field'; if W1 <> W2 Then P := Pos(W1, W2);

68

end;

Dopuszczalne jest rwnie odwoywanie si do poszczeglnych znakw acucha WideChar, na przykad:


var W: WideString; C: WideChar; begin W := 'Ebony and Ivory living in perfect harmony'; C := W[Length(W)]; // C zawiera ostatni znak acucha W end;

acuchy z zerowym ogranicznikiem


acuchy takie, zwane w oryginale null terminated strings, stanowi cig znakw z wyrnionym (np. przez wskanik) pierwszym znakiem. Wszystkie nastpne znaki a do znaku o kodzie zero stanowi zawarto acucha, znak zerowy nie jest ju jego czci i peni rol ogranicznika. Std prosty wniosek, e repertuar znakw reprezentowalnych w acuchach omawianego typu jest zuboony o znak o kodzie zero. W poprzednich wersjach Pascala, do Delphi 1 wcznie, znak reprezentowany by przez jeden bajt pamici, std te istnia jedynie jeden typ acuchw z zerowym ogranicznikiem typ PChar. Typ ten istnieje nadal w nastpnych wersjach Delphi, ze wzgldw kompatybilnoci oraz na potrzeby interfejsu Win32 API. Ze wzgldu jednake na trzy typy znakw (Char, WideChar i AnsiChar) w wprowadzono w Delphi 2 dodatkowe typy omawianych acuchw: PWideChar i PAnsiChar. Jak atwo wywnioskowa z powyszego opisu, acuch z zerowym ogranicznikiem reprezentowany jest przez wskanik do pierwszego znaku (rysunek 2.3), a wymienione trzy typy PChar, PWideChar i PAnsiChar s wedug terminologii jzyka Pascal typami wskanikowymi (pointers).

Rysunek 2.3. Napis DDG reprezentowany w postaci acucha z zerowym ogranicznikiem W odrnieniu od acuchw typu AnsiString, opisywanym tu acuchom nie towarzyszy aden mechanizm wspomagajcy zarzdzanie pamici operacyjn jej przydzielanie i zwalnianie odbywa si w sposb jawny. Podstawow funkcj dokonujc przydziau pamici dla acuchw z zerowym ogranicznikiem jest funkcja StrAlloc(), moliwe jest jednak wykorzystanie w tym celu take podstawowych funkcji Object Pascala, w rodzaju AllocMem(), GetMem(), StrNew() , a nawet VirtualAlloc(). Naley jednak zaznaczy, e sposb zwalniania przydzielonej pamici musi by zgodny ze sposobem jej przydzielania; wzajemn odpowiednio niektrych funkcji w tym wzgldzie przedstawia tabela 2.7. Tabela 2.7. Funkcje przydziau i zwalniania pamici operacyjnej na potrzeby acuchw z zerowym ogranicznikiem Funkcja przydzielajca Funkcja zwalniajca
AllocMem() GlobalAlloc() GetMem() New() StrAlloc() StrNew() VirtualAlloc() FreeMem() GlobalFree() FreeMem() Dispose() StrDispose() StrDispose() VirtualFree()

Cho naruszenie regu powyszej tabeli nie zawsze jest bdem (dowiadczony programista wie przecie, e np.
StrAlloc() oraz StrNew() korzystaj z procedury GetMem()), to jednak ich przestrzeganie zmniejsza ryzyko

popenienia bdu.

69

Poniszy przykad ilustruje wykorzystanie acuchw z zerowym ogranicznikiem.


Var P1, P2 : PChar; S1, S2 : AnsiString; begin P1 := StrAlloc(64 * Sizeof(Char)); { P1 wskazuje na 63 znakowy acuch } StrPCopy (P1, 'Delphi 6 '); { Do acucha P1 zostaje wpisana konkretna zawarto } S1 := 'vademecum profesjonalisty'; { Do acucha S1 zostaje wpisana konkretna zawarto } P2 := StrNew(PChar(S1)); { P2 wskazuje na kopi S1 } StrCat(P1, P2); { konkatenacja P1 i P2 } S2 := P1 { S2 zawiera napis 'Delphi 6 vademecum profesjonalisty' } StrDispose(P1); StrDispose(P2); { zwolnienie przydzielonej pamici} end.

Zwr uwag, e rozmiar przydzielanej pamici zostaje obliczony za pomoc konstrukcji SizeOf(Char) jest to prost konsekwencj faktu, e znak typu Char by moe nie bdzie ju jednobajtowy w nastpnych wersjach Delphi. Funkcja StrCat() wykonuje konkatenacj dwch acuchw typu PChar nie mona w tym celu wykorzysta operatora +, jak to miao miejsce w przypadku acuchw AnsiString, WideString i ShortString. Funkcja StrNew() tworzy kopi acucha podanego jako parametr. Poniewa funkcje operujce na acuchach z zerowym ogranicznikiem nie posiadaj adnej informacji o wielkoci pamici przydzielanej na ich potrzeby, odpowiedzialno za przydzielenie wystarczajco duego obszaru spoczywa cakowicie na programicie. Najczstszym bdem jest przydzielanie zbyt maego obszaru w poniszym przykadzie funkcja StrCat() usiuje przypisa 13-znakowy napis Witaj wiecie do acucha zdolnego pomieci napis co najwyej 6znakowy:
var P1, P2 : PChar; begin P1 := StrNew('Witaj '); P2 := StrNew('wiecie!'); StrCat (P1, P2 ); // tu nastpuje wyjcie poza przydzielon pami ......

Wskazwka

Opis funkcji oraz procedur operujcych na acuchach z zerowym ogranicznikiem znale mona w systemie pomocy pod hasem String-handling routines (null-terminated). Wiele uytecznych funkcji zawiera rwnie modu STRUTILS w katalogu \SOURCE\UTIL na zaczonym krku CD-ROM.

Typy wariantowe
Typ Variant jest w Pascalu zupen nowoci; konsekwencj wspomnianego wczeniej bezpieczestwa typw jest absolutne ustalenie typu kadej zmiennej ju na etapie kompilacji. Takie podejcie nie da si jednak pogodzi ze standardami programowania w Windows, szczeglnie w aspekcie mechanizmu OLE, o czym bdzie mowa w dalszej czci ksiki. Wprowadzono wic moliwo dynamicznego przepoczwarzania si zmiennej,

70

czyli zmiany jej typu stosownie do kontekstu wykonywanego programu2. Najwaniejsz przesank powstania typu wariantowego bya niewtpliwie konieczno przekazywania w jednolity sposb danych rnych typw w ramach mechanizmu automatyzacji OLE. Nieprzypadkowo wic delphicka implementacja typu Variant jest niemal identyczna ze stosowan w OLE, cho jej uyteczno wykracza daleko poza w kontekst oferujc wyjtkowo silne narzdzie programistyczne. W chwili obecnej Object Pascal jest jedynym cakowicie kompilowalnym jzykiem implementujcym zmienne wariantowe zarwno w aspekcie dynamicznej zmiany typu w czasie wykonywania programu, jak i pod ktem penoprawnego typu danych w skadniowej i semantycznej konwencji kompilatora. W Delphi 3 wprowadzono dodatkowo inny typ wariantowy OLEvariant. Od swego pierwowzoru (Variant) rni si zakresem reprezentowalnych typw, ograniczonym do typw wykorzystywanych przez automatyzacj OLE. W niniejszym rozdziale skoncentrujemy si gwnie na typie Variant, a typ OLEvariant bdziemy przywoywa tylko w kontekcie porwna ze swym pierwowzorem.

Dynamiczna zmiana typu


Podstawow cech zmiennych typu Variant jest moliwo dynamicznej zmiany ich typu w czasie wykonania programu i wynikajca std niemono okrelenia tego typu na etapie kompilacji. Oto fragment programu, pod kadym wzgldem poprawnego w Object Pascalu:
var V: Variant; begin // Zmienna V zawiera acuch znakw V := 'Delphi 6 jest wspaniae'; // Zmienna V zawiera liczb cakowit V := 1; // Zmienna V zawiera liczb zmiennoprzecinkow V := 123.34; // Zmienna V zawiera warto boolowsk V := TRUE; // Zmienna V zawiera wskazanie na obiekt OLE V := CreateOLEobject('Word.Basic');

Zmienna typu Variant moe podczas wykonywania programu przechowywa wartoci cakowite, zmiennoprzecinkowe, acuchy znakw, wartoci boolowskie, znaczniki daty/czasu, kwoty pienine (Currency) i obiekty automatyzacji OLE. Moe ona rwnie reprezentowa tablic heterogeniczn, tj. tak, ktrej rozmiary i typy elementw ulegaj dynamicznej zmianie, w szczeglnoci tablic, ktrej elementy s wskanikami do innych tablic wariantowych.

Wewntrzna struktura zmiennej wariantowej


Wewntrzna implementacja zmiennej typu Variant oparta jest na nastpujcej strukturze3:
Type TVarType = Word; PVarData = ^TVarData; {$EXTERNALSYM PVarData} TVarData = packed record VType: TVarType; case Integer of

Zjawisko dynamicznej zmiany typu znane byo ju np. w popularnym jzyku Clipper w wersji 87 nie istniaa nawet moliwo deklarowania typu zmiennej, wszystkie zmienne miay charakter wariantowy. W Visual Basicu domylnym typem (nie zadeklarowanej) zmiennej jest wanie typ Variant. W Turbo Pascalu namiastk zmiennych wariantowych stanowiy parametry amorficzne (ang. untyped parameters) oraz rzutowanie typw (ang. typecasting) konstrukcje zaczerpnite z jzyka Ada. O mechanizmie zmiennych wariantowych myleli ju w latach szedziesitych (!) twrcy jednego z pierwszych jzykw wysokiego poziomu jzyka Algol 60 lecz szybko uznali oni, e moc obliczeniowa wspczesnych komputerw nie byaby w stanie zapewni wystarczajcej efektywnoci jzykowi implementujcemu zmienne wariantowe (przyp. tum.). Nie naley myli ze sob dwch bytw o zblionym nazewnictwie, lecz rnych znaczeniach: rekordu z czci wariantow (jakim jest rekord TVarData) oraz zmiennych wariantowych. Cz wariantowa rekordu jest pascalowym odpowiednikiem unii jzyka C, w ramach ktrej kilka rnych danych zajmuje ten sam obszar pamici. Jedynym zwizkiem wspomnianego rekordu i zmiennej wariantowej jest prezentowana struktura.
3

71

0: (Reserved1: Word; case Integer of 0: (Reserved2, Reserved3: Word; case Integer of varSmallInt: (VSmallInt: SmallInt); varInteger: (VInteger: Integer); varSingle: (VSingle: Single); varDouble: (VDouble: Double); varCurrency: (VCurrency: Currency); varDate: (VDate: TDateTime); varOleStr: (VOleStr: PWideChar); varDispatch: (VDispatch: Pointer); varError: (VError: LongWord); varBoolean: (VBoolean: WordBool); varUnknown: (VUnknown: Pointer); varShortInt: (VShortInt: ShortInt); varByte: (VByte: Byte); varWord: (VWord: Word); varLongWord: (VLongWord: LongWord); varInt64: (VInt64: Int64); varString: (VString: Pointer); varAny: (VAny: Pointer); varArray: (VArray: PVarArray); varByRef: (VPointer: Pointer); ); 1: (VLongs: array[0..2] of LongInt); ); 2: (VWords: array [0..6] of Word); 3: (VBytes: array [0..13] of Byte); end;

Jak atwo policzy, zmienna wariantowa zajmuje 16 bajtw pamici. Pierwsze dwa bajty (VType) okrelaj aktualny typ zawartoci zmiennej:
varEmpty varNull varSmallint varInteger varSingle varDouble varCurrency varDate varOleStr varDispatch varError varBoolean varVariant varUnknown //varDecimal varShortInt varByte varWord varLongWord varInt64 //varWord64 = = = = = = = = = = = = = = = = = = = = = $0000; $0001; $0002; $0003; $0004; $0005; $0006; $0007; $0008; $0009; $000A; $000B; $000C; $000D; $000E; $0010; $0011; $0012; $0013; $0014; $0015; { { { { { { { { { { { { { { { { { { { { { { vt_empty vt_null vt_i2 vt_i4 vt_r4 vt_r8 vt_cy vt_date vt_bstr vt_dispatch vt_error vt_bool vt_variant vt_unknown vt_decimal undefined $0f vt_i1 vt_ui1 vt_ui2 vt_ui4 vt_i8 vt_ui8 } } } } } } } } } } } } } } } {nie obsugiwane} } {nie obsugiwane} } } } } } } {nie obsugiwane}

{ rozszerzajc interpretacj typu Variant, naley zmodyfikowa zmienn varLast oraz tablice BaseTypeMap i OpTypeMap w module Variants} varStrArg = $0048; { vt_clsid } varString = $0100; { acuch pascalowy, niezgodny z OLE } varAny = $0101; { typ "any" CORBA } varTypeMask = $0FFF; varArray = $2000; varByRef = $4000;

Jak atwo zauway, nie istnieje moliwo reprezentowania w zmiennej wariantowej wskanikw ani obiektw.
Wskazwka

Delphi 6 umoliwia uytkownikowi rozszerzenie powyszej interpretacji i wykorzystywanie zmiennych wariantowych do reprezentowania wartoci samodzielnie zdefiniowanych typw. Wymaga to jednak ingerencji w kod rdowy biblioteki RTL, konkretnie w modu Variants. Naley zmieni trzy elementy: zmienn globaln varLast zawierajc numer ostatniego zdefiniowanego wariantu (obecnie varInt64), tablic

72

BaseTypeMap okrelajce list zdefiniowanych wariantw oraz tablic OpTypeMap okrelajc konwersj pomidzy poszczeglnymi wariantami (przyp. tum.).

Kompilator dopuszcza samodzielne mapowanie zmiennej wariantowej przez struktur TVarData, co umoliwia bezporednie odwoywanie si do pl tej ostatniej, na przykad:
Var V: Variant; begin TVarData(V).VType := varInteger; TVarData(V).VInteger := 2; end;

Powysza konstrukcja jest rwnowana prostszej konstrukcji


V := 2;

Nie to jest jednak najwaniejsze; jak zobaczymy za chwil, bezporednie operowanie polami struktury
TVarData niesie ze sob niebezpieczestwo dezorganizacji zarzdzania pamici.

Zmienne wariantowe a kontrolowany czas ycia


Zmienna wariantowa moe reprezentowa acuch AnsiString pole VType ma wwczas warto varString, natomiast pole VString zawiera wskanik do tego acucha. Kompilator uwzgldnia oczywicie fakt, i ten acuch jest zmienn o kontrolowanym czasie ycia; wobec moliwoci reprezentowania przez zmienn wariantow innych wielkoci tej kategorii, generalnie same zmienne wariantowe s zmiennymi o kontrolowanym czasie ycia. Spjrzmy na poniszy fragment:
procedure ShowVariant(S: String); var V: Variant; begin V := S; ShowMessage(V); end;

Wszystko odbywa si tu automatycznie programista nie musi si martwi o zarzdzanie pamici na potrzeby acucha reprezentowanego przez zmienn V. Delphi, realizujc powysz sekwencj, nadaje pocztkowo zmiennej wariantowej warto nieokrelon (Unassigned). Nastpnie przypisuje polu VType identyfikator varString, natomiast do pola VString kopiuje wskanik do acucha reprezentowanego przez S, jednoczenie zwikszajc jego licznik odwoa. Kiedy zmienna V zakoczy swj czas ycia (to znaczy gdy zakoczy si wykonywanie procedury ShowVariant), acuch ten jest traktowany tak, jak gdyby by reprezentowany przez zwyk zmienn acuchow jego licznik odwoa jest zmniejszany o 1, a jeeli osignie przez to warto zero, zwalniana jest caa pami przydzielona acuchowi. Mona to przedstawi pogldowo jako zanurzenie caej procedury ShowVariant() w wyimaginowanym bloku tryfinally:
procedure ShowVariant(S: String); var V: Variant; begin V := Unassigned; try V := S; ShowMessage(V); finally // zwolnij zasoby przydzielone do zmiennej wariantowej end; end;

Z podobnym, cho troch bardziej zoonym przypadkiem automatycznego zwalniania zasobw, mamy do czynienia w sytuacji zmiany aktualnego typu zmiennej wariantowej z acuchowego na inny, na przykad:
Procedure ChangeVariant(S:String; I:Integer); var V: Variant; begin

73

V := S; ShowMessage(V); V := I; end;

To, co dzieje si podczas realizacji powyszej sekwencji, mona by zapisa nastpujco (instrukcje wyrnione kursyw niekoniecznie s poprawnymi konstrukcjami Object Pascala):
Procedure ChangeVariant(S:String; I:Integer); var V: Variant; begin V := Unassigned; try // skojarz zmienn V z acuchem S V.Vtype := varString; V.VString := S; // zwiksz licznik odwoa acucha S Inc(S.RefCount); ShowMessage(V); // zmniejsz licznik odwoa acucha S Dec(S.RefCount) jeli S.RefCount = 0 to zwolnij pami przydzielon dla acucha S V.VType := varInteger; V.VInteger := I; finally zwolnij zasoby przydzielone dla zmiennej V end; end;

Powyszy schemat mgby stanowi inspiracj do wykonania caego scenariusza za pomoc bezporedniego operowania na polach struktury TVarData:
Procedure ChangeVariant(S:String; I:Integer); var V: Variant; begin V := S; ShowMessage(V); TVarData(V).VType := varInteger; TVarData(V).VInteger := I; end;

I tu wanie kryje si puapka: w powyszym kodzie nie istnieje bowiem miejsce, w ktrym kompilator mgby stwierdzi, i zerwany zostaje zwizek zmiennej V z acuchem S (struktura TVarData nie jest traktowana w aden szczeglny sposb). W efekcie licznik odwoa acucha S nie zostanie zmniejszony i zarzdzanie pamici na jego potrzeby ulegnie pewnemu zachwianiu. Wniosek naley unika operowania wprost na strukturze TVarData.

Zmienne wariantowe a rzutowanie typw


Kada zmienna o typie reprezentowalnym przez typ Variant moe by w sposb jawny obsadzona w jego roli, na przykad:
var X : Integer; ... ShowMessage(Variant(X));

Rwnie warto kadego ze wspomnianych typw moe by w sposb jawny przypisana zmiennej wariantowej, przykadowo
V V V V := := := := 2; 1.6; 'Hello'; TRUE;

I vice versa zmienna wariantowa moe by obsadzana w roli dopuszczalnych typw, na przykad:
V := 1.6; S := String(V); I := Integer(V); B := Boolean(V); // // // // S zawiera warto '1.6' I zawiera warto 2 jako zaokrglenie 1,6 do najbliszej liczby cakowitej B zawiera warto TRUE

74

D := Double(V);

// D zawiera warto 1.6

lecz i to nie jest konieczne, poniewa powysze konstrukcje mona by rwnie dobrze zapisa jako:
V := 1.6; S := V; // S zawiera warto '1.6' I := V; // I zawiera warto 2 jako zaokrglenie 1,6 // do najbliszej liczby cakowitej B := V; // B zawiera warto TRUE D := V; // D zawiera warto 1.6

Zmienne wariantowe w wyraeniach


Zmienne zadeklarowane jako Variant mog by argumentami nastpujcych operatorw: +, , =, *, /, div, mod, shl, shr, and, or, xor, not, :=, <>, <, >, <= i >=. Znaczenie danego operatora moe by uzalenione od biecej zawartoci zmiennych wariantowych stanowicych jego argumenty np. operator + moe oznacza dodanie dwch liczb albo konkatenacj acuchw. Jeeli argumenty operacji rni si pod wzgldem typu, Delphi przeprowadza konwersj na wsplny typ, ktrym jest typ silniejszy ranking siy poszczeglnych typw przedstawia si nastpujco:
double integer string

Moe to niekiedy prowadzi do zaskakujcych rezultatw (zaskakujcych, jeli nie zna si powyszej reguy). Spjrzmy na poniszy przykad:
var V1, V2, V3 : Variant; begin V1 := '100'; // acuch V2 := '50'; // acuch V3 := 200; // liczba cakowita V1 := V1 + V2 + V3; end;

Po wykonaniu powyszej sekwencji wartoci zmiennej V1 jest nie 350 (jak mogoby si niektrym wydawa), lecz 10250.0. Istotnie: pierwsza operacja V1 + V2 jest konkatenacj acuchw, a jej wynikiem jest '10050'. Kolejna operacja jest dodawaniem acucha '10050' do liczby 200 zgodnie z przedstawionym rankingiem acuch '10050' konwertowany jest na liczb cakowit 10050, ta za dodawana jest do liczby (cakowitej) 200, co daje w wyniku (uwaga!) liczb rzeczywist4 (double) 10250.0. Oczywicie nie kada operacja na zmiennych wariantowych jest wykonalna. W poniszej sekwencji
var V1, V2: Variant; begin V1 := 77; V2 :='Hello'; V1 := V1 / V2; end;

Delphi sprbuje skonwertowa zawarto zmiennej V2 na posta liczbow (integer lub double), co oczywicie jest niewykonalne; w efekcie otrzymamy wyjtek EVariantError z komunikatem Invalid variant type conversion5. Niekiedy celowe moe okaza si jawne konwertowanie zawartoci zmiennej wariantowej na wskazany typ. Operacja ta sprawia, e kod wynikowy jest bardziej zwizy i poprawia efektywno jego wykonywania ponisza sekwencja

Mogoby si wydawa, i wsplnym typem powinien by w tym przypadku integer i wynik powinien by liczb cakowit. Nieprzypadkowo jednak wsplnym typem acucha i liczby cakowitej jest double, nie integer w przeciwnym razie niewykonalne byyby tak oczywiste obliczenia, jak np. dodanie acucha '3.7' do liczby 2 (wynik takiego dodawania to liczba 5.7) (przyp. tum.). Sytuacja nie zmieniaby si, gdyby zamiast operatora / wystpi operator +; wbrew pozorom Delphi nie zinterpretowaoby prezentowanej operacji jako konkatenacji acuchw, bowiem wsplnym typem acucha i liczby cakowitej jest double, nie string (przyp. tum.).
5

75

V4 := V1 * V2 / V3;

jest mniej efektywna od


V4 := Integer(V1) * Double(V2) / Integer(V3);

Naley take zauway, i w drugim przypadku mamy do czynienia z zaokrgleniami zawartoci V1 i V3. Jest rzecz oczywist, e zmienne wariantowe s wyranym odstpstwem od zasady bezpieczestwa typw. To prawda, jednak bez nich wykorzystanie mechanizmu OLE byoby jedynie iluzj. Zreszt, s one rwnie uyteczne w zastosowaniach o wiele bardziej banalnych, na przykad:
Var V1, V2 : Variant; L : Word; ............. if L < 0 Then begin V1 := 'brakuje '; V2 := L; end Else if L > 0 then begin V1 := 'nadmiar '; V2 := L; end else begin V1 := 'nie brakuje '; V2 := 'adnych'; end; V1 := V1 + V2 + ' pozycji';

Po wykonaniu powyszego fragmentu, zmienna V1 zawiera acuch stanowicy czytelny raport na temat ewentualnych brakw czy nadmiaru w wykazie.

Wartoci UNASSIGNED i NULL


Spord wielu moliwych wartoci, jakie przyjmowa moe zmienna wariantowa, dwie zasuguj na obszerniejsze omwienie. Pierwsz z nich jest UNASSIGNED, oznaczajca, e zmienna wariantowa nie reprezentuje aktualnie adnej wartoci; jest ona nadawana przez Delphi automatycznie kadej zmiennej wariantowej rozpoczynajcej swj czas ycia. Odpowiada jej warto varEmpty pola VType. Drug ze wspomnianych wartoci jest NULL, oznaczajca warto pust i reprezentowana w polu VType przez warto varNull. Rozrnienie pomidzy tymi dwiema wartociami jest istotne midzy innymi w sytuacji, gdy tabela bazy danych zawiera pole typu Variant szerzej zajmiemy si tym zagadnieniem w dalszej czci niniejszego tomu. Inn wan cech odrniajc wartoci UNASSIGNED i NULL jest rezultat uycia zmiennej wariantowej w wyraeniu: prba uycia jako operandu zmiennej wariantowej o wartoci UNASSIGNED spowoduje wyjtek, natomiast warto NULL posiada wasno propagacji warto wyraenia, ktrego chocia jeden operand ma warto 6 NULL, rwna jest NULL .
Ostrzeenie

Mimo wielkiej uytecznoci zmiennych wariantowych wykorzystuje je biblioteka VCL, korzystaj z nich kontrolki ActiveX ich elastyczno stanowi jednoczenie puapk dla wygodnego programisty. Wraenie, e deklarowanie zmiennych jako Variant wszdzie, gdzie tylko si da, uatwi mu ycie, jest zudne; gdy przyjdzie do testowania programu, prawdopodobne trudnoci w znalezieniu przyczyny ewentualnego bdu stanowi bd zbyt wysok cen za wygodnictwo (i, by moe, faszywie pojt elastyczno kodu). Ponadto, ze wzgldu na znacznie bardziej skomplikowany sposb obsugi zmiennych wariantowych, wydua si kod programu i spada jego oglna efektywno. Zalecamy wic rozsdnie uywa zmiennych wariantowych.

Chyba e drugi operand posiada warto UNASSIGNED (przyp. tum.).

76

Tablice wariantowe
Wspominalimy przed chwil, i jedn z wartoci reprezentowanych przez zmienn wariantow moe by wskazanie na (by moe heterogeniczn) tablic. Poniszy fragment programu
var V: variant; I, J : Integer; begin J := 1 I := V[J]; ...

jest syntaktycznie poprawny i kompiluje si bezbdnie, ale prba jego wykonania skoczy si niepowodzeniem, poniewa zmienna V nie reprezentuje aktualnie adnej tablicy. W celu utworzenia tablicy wariantowej mona skorzysta z jednej z dwu przeznaczonych do tego funkcji Object Pascala: VarArrayCreate() lub VarArrayOf(). Funkcja VarArrayCreate() Funkcja VarArrayCreate() deklarowana jest w module System w taki oto sposb:
Function Variant; VarArrayCreate( const Bounds: array of Integer; VarType: Integer):

Funkcja ta tworzy tablic wariantow na podstawie zadanych par indeksw granicznych i wskazanego typu elementw. Pary indeksw granicznych s zawartoci tablicy przekazanej jako pierwszy parametr (notabene parametr ten stanowi przykad tablicy otwartej tablicami otwartymi zajmiemy si w dalszej czci rozdziau), natomiast drugi parametr identyfikuje typ elementw tablicy (w konwencji wartoci wpisywanych w pole VType struktury TVarData). Oto prosty przykad utworzenia jednowymiarowej tablicy o czterech elementach typu Integer:
Var V: Variant; begin V := VarArrayCreate( [1 , 4], varInteger ); ... V[1] := 1; V[2] := 3; V[3] := 5; V[4] := 7; ...

Z kolei poniszy przykad ilustruje tworzenie macierzy jednostkowej o wymiarze 1010, zawierajcej elementy typu Double; na przektnej macierzy wpisywane s jedynki, poza przektn zera:
// A.Grayski Const VDim = 10; Var V: Variant; i, j : Integer begin V := VarArrayCreate( [1 , VDim, 1, VDim], varDouble ); For i := 1 to VDim do begin For j := 1 to VDim do begin V[i, j] := (I div J) * (J div I); // 1, gdy i=j, 0 gdy i<>j end; end; end;

Oczywicie nic nie stoi na przeszkodzie, aby elementy tablicy same byy zmiennymi wariantowymi, w szczeglnoci zawieray wskazanie na tablice wariantowe! Umoliwia to tworzenie tablic wyszego rzdu, posiadajcych ciekaw cech nieortogonalnoci. Tablic nazywamy ortogonaln, jeli da si ona przedstawi jako wektor zmiennych jednakowego typu i tak, np. tablica array [ 1 .. 10 ] of real jest 10-elementowym wektorem zmiennych typu real, macierz array [ 1 .. 5, 2 .. 20 ] of integer moe by rozpatrywana bd jako 5-elementowy wektor tablic array [ 2 .. 20 ] of integer, bd 19-elementowy wektor tablic array [ 1 .. 5 ] or integer. Oglnie rzecz biorc, kada zwyka tablica pascalowa jest tablic ortogonaln, lecz tablice wariantowe wcale nie musz posiada tej cechy. Oto prosty przykad ponisza sekwencja tworzy trjktn tablic stanowic grny trjkt macierzy 1010 elementw typu Double:
// A.Grayski

77

var V : Variant; i : integer; const VDim = 10; begin V := VarArrayCreate ( [1 , VDim], varVariant); for i := 1 to VDim do begin V[i] := VarArrayCreate ( [1 , VDim-i+1], varDouble); end;

Funkcja VarArrayOf() Funkcja VarArrayOf() deklarowana jest w module System nastpujco:


Function VarArrayOf(const Values: array of Variant): Variant;

i jak atwo si domyli suy do zgrupowania w jednowymiarow tablic wariantow wartoci stanowicych kolejne elementy wektora podanego jako parametr. Po wykonaniu poniszej instrukcji
V := VarArrayOf( [ 1, 'Delphi', 2.2] );

V[1] zawiera liczb cakowit 1, V[2] zawiera acuch 'Delphi', natomiast V[3] jest liczb rzeczywist o wartoci 2.2. Tablica utworzona w ten sposb moe wic by tablic heterogeniczn, tj. posiadajc elementy rnych typw.

Procedury i funkcje wspomagajce zarzdzanie tablicami wariantowymi Oprcz opisanych funkcji VarArrayCreate() i VarArrayOf() Object Pascal oferuje kilka rwnie uytecznych podprogramw zwizanych z tablicami wariantowymi. Zdefiniowane s one w module System oto ich nagwki, nastpnie krtki opis:
function VarIsArray (const A: Variant): Boolean; function VarArrayDimCount (const A: Variant): Integer; function VarArrayLowBound (const A: Variant; Dim: Integer): Integer; function VarArrayHighBound (const A: Variant; Dim: Integer): Integer; procedure VarArrayRedim (var A : Variant; HighBound: Integer); function VarArrayRef (const A: Variant): Variant; function VarArrayLock (const A: Variant): Pointer; procedure VarArrayUnlock (const A: Variant);

Funkcja VarIsArray() dokonuje prostego sprawdzenia, czy przekazany parametr jest tablic wariantow:
function VarIsArray(const A: Variant): Boolean; begin Result := TVarData(A).VType and varArray <> 0; end;

Funkcja VarArrayDimCount() zwraca liczb wymiarw tablicy, natomiast doln oraz grn granic kadego wymiaru pozna mona dziki funkcjom VarArrayLowBound() i VarArrayHighBound(). Procedura VarArrayRedim() umoliwia zmian grnej granicy najwyszego w hierarchii wymiaru tablicy to ten wymiar, ktry identyfikowany jest przez ostatni (skrajny, prawy) indeks. Istniejce elementy tablicy zostaj zachowane, ewentualne nowe elementy otrzymuj wartoci o reprezentacji zerowej. Funkcja VarArrayRef() otrzymujc tablic wariantow, tworzy zmienn wariantow zawierajc wskazanie na t tablic. Ta dziwna na pozr czynno podyktowana zostaa potrzebami wynikajcymi z uytkowania serwerw automatyzacji OLE, ktre wymagaj tablicy wariantowej w takiej wanie postaci.
function VarArrayRef(const A: Variant): Variant; begin if TVarData(A).VType and varArray = 0 then Error(reVarNotArray); _VarClear(Result); TVarData(Result).VType := TVarData(A).VType or varByRef; if TVarData(A).VType and varByRef <> 0 then

78

TVarData(Result).VPointer := TVarData(A).VPointer else TVarData(Result).VPointer := @TVarData(A).VArray; end;

Jeeli wic, na przykad, VA oznacza tablic wariantow, to wywoanie dowolnej funkcji API zwizanej z serwerem powinno mie posta
Server.PassvariantArray(VarArrayRef(VA));

Istnienie funkcji VarArrayLock() i VarArrayUnlock() podyktowane jest pewnym subtelnym aspektem efektywnociowym, ktry postaramy si zilustrowa na prostym przykadzie. Zamy, i chcemy skopiowa zawarto wektora bajtw skadajcego si z 10000 elementw do nowo utworzonej tablicy wariantowej, na przykad w tak oczywisty sposb:
var V: Variant; A: Array [ 1 .. 10000] of byte; .... V := VarArrayCreate([1, 10000], VarByte); for i := 1 to 10000 do V[i] := A[i];

Nastpuje tutaj wykonanie kolejno 10000 przypisa, z ktrych kade jest do kosztowne, wymaga bowiem wykonania dosy skomplikowanych kontroli i oblicze, wynikajcych m.in. ze zoonej struktury samej tablicy V. Okazuje si, i moliwa byaby znaczna redukcja tych operacji, gdyby zaoy, i w trakcie owych 10000 przypisa tablica nie zmienia swej struktury ani pooenia w pamici. Tak wanie rol peni funkcja VarArrayLock() zablokowuje tablic w tym sensie, i do czasu jej odblokowania za pomoc funkcji VarArrayUnlock() niedopuszczalne jest wywoanie w stosunku do niej funkcji VarArrayRedim(). Operowanie na zablokowanej tablicy wariantowej redukuje znacznie liczb wykonywanych weryfikacji i tym samym zwiksza ogln efektywno programu. Dodatkow uyteczn informacj niesie wynik funkcji VarArrayLock(): jest on wskanikiem do fizycznego wektora elementw w pamici operacyjnej, dziki czemu moliwe jest wykonywanie pewnych operacji na skrty w tym konkretnym przypadku kopiowanie elementw moe zosta wykonane przez jedno wywoanie procedury Move():
var V: Variant; A: Array [ 1 .. 10000] of byte; P: Pointer; .... V := VarArrayCreate([1, 10000], VarByte); P := VarArrayLock(V); try Move(A, P^, 10000); finally VarArrayUnlock(V); end;

Wykorzystujc funkcj VarArrayLock() w stosunku do wielowymiarowej tablicy wariantowej, musimy pamita, i fizyczna tablica wskazywana przez wynik tej funkcji posiada struktur wymiarw odwrcon w stosunku do tablicy oryginalnej innymi sowy, w poniszym przykadzie
V := VarArrayCreate([1, 100, 2, 50, 6, 30], VarByte); P := VarArrayLock(V);

wskanik P powinien by traktowany tak, jak gdyby wskazywa na tablic postaci


array [ 6 .. 30, 2 .. 50, 1 .. 100 ] of byte

Inne podprogramy zwizane ze zmiennymi wariantowymi


Procedura VarClear() dokonuje wyczyszczenia zmiennej wariantowej poprzez wpisanie w jej pole VType wartoci varEmpty. Procedura VarCopy() kopiuje zawarto zmiennej wariantowej:
procedure VarCopy(var Dest: Variant; const Source: Variant);

Procedura VarCast() dokonuje konwersji zawartoci zmiennej wariantowej na wskazany typ, zapisujc wynik w innej zmiennej wariantowej:
procedure VarCast(var Dest: Variant; const Source: Variant; VarType: Integer);

Funkcja VarType() zwraca typ zmiennej wariantowej, a dokadniej zawarto pola VType struktury TVarData naoonej na t zmienn:
function VarType(const V: Variant): Integer;

79

asm MOVZX end;

EAX,[EAX].TVarData.VType

Funkcja VarAsType() jest bliniacz siostr procedury VarCast(), zwraca bowiem zmienn wariantow stanowic rezultat konwersji argumentu na zadany typ:
function VarAsType(const V: Variant; VarType: Integer): Variant; begin _VarCast(Result, V, VarType); end;

Funkcja VarIsEmpty() dokonuje sprawdzenia, czy zmienna wariantowa jest zainicjowana:


function VarIsEmpty(const V: Variant): Boolean; begin with TVarData(V) do Result := (VType = varEmpty) or ((VType = varDispatch) or (VType = varUnknown)) and (VDispatch = nil); end;

Podobne zadanie spenia funkcja VarIsNull(), sprawdzajca, czy zmienna wariantowa reprezentuje warto
NULL:
function VarIsNull(const V: Variant): Boolean; begin Result := TVarData(V).VType = varNull; end;

Funkcja VarToStr() tworzy znakow reprezentacj zmiennej wariantowej, przy czym wartoci varNull odpowiada acuch pusty:
function VarToStr(const V: Variant): string; begin if TVarData(V).VType <> varNull then Result := V else Result := ''; end;

Zwr uwag na ciekawy fakt, i zasadnicza konwersja dokonywana jest tu automatycznie przez podprogramy biblioteki RTL, uruchamiane w wyniku pojedynczej instrukcji przypisania
Result := V;

Wreszcie, funkcje VarFromDateTime() i VarToDateTime() dokonuj konwersji pomidzy zmiennymi wariantowymi a wskazaniami daty/czasu:
function VarFromDateTime(DateTime: TDateTime): Variant; begin _VarClear(Result); TVarData(Result).VType := varDate; TVarData(Result).VDate := DateTime; end;

function VarToDateTime(const V: Variant): TDateTime; var Temp: TVarData; begin Temp.VType := varEmpty; _VarCast(Variant(Temp), V, varDate); Result := Temp.VDate; end;

Typ OLEvariant
Typ ten jest niemal identyczny z typem Variant rnica sprowadza si do niemonoci reprezentowania przez jego zmienne typw niekompatybilnych z mechanizmem automatyzacji OLE. Wyjtkiem jest typ AnsiString, reprezentowany przez warto varString w polu VType przypisanie go do zmiennej typu OleVariant spowoduje uprzedni jego konwersj na typ BSTR, w wyniku czego pole VType posiada bdzie warto varOleStr, za pole VOleStr wskazywa bdzie na acuch typu BSTR (czyli acuch znakw WideChar zakoczony zerowym ogranicznikiem).

80

Typ Currency
Ten typ pojawi si po raz pierwszy w Delphi 2 i z zaoenia przeznaczony jest do przechowywania liczb rzeczywistych reprezentujcych wielkoci, co do ktrych wymagana jest bezwzgldna dokadno gwnie kwot pieninych. Wewntrzn jego reprezentacj jest 64-bitowa liczba cakowita ze znakiem, zawierajca warto 10000 razy wiksz ni warto faktycznie reprezentowana na przykad dla liczby 4,67 warto ta rwna jest 46700. Jest to wic typ rzeczywisty staoprzecinkowy notabene jedyny tego rodzaju typ w Object Pascalu zapewniajcy dokadno czterech cyfr dziesitnych i maksymaln warto bezwzgldn (263 1)/ 10000 = 922337203685477.5807. Przy przenoszeniu aplikacji z Delphi 1 wskazane jest przeanalizowanie danych i przeprogramowanie na posta typu Currency danych finansowych reprezentowanych dotychczas przez typy zmiennoprzecinkowe Real, Single, Double i Extended.

Typy definiowane przez uytkownika


Liczby cakowite, zmiennoprzecinkowe, acuchy itp. nie czyni jeszcze z jzyka narzdzia do rozwizywania rzeczywistych problemw programistycznych. Repertuar ten musi zosta poszerzony o typy zdefiniowane przez uytkownika. W jzyku Object Pascal typy definiowane przez uytkownika maj posta tablic (arrays), rekordw (records) i obiektw (objects), a ich definicje rozpoczynaj si od sowa kluczowego Type.

Tablice
Tablice stanowi uporzdkowany cig (a waciwie wektor) zmiennych tego samego typu. Typem elementu tablicy moe by dowolny typ, rwnie zdefiniowany przez uytkownika. Ponisza deklaracja definiuje tablic omiu liczb cakowitych:
Type Int8Arr = array [ 0 .. 7 ] of integer;

Od tej chwili typ Int8Arr staje si penoprawnym typem danych, a wic jest moliwe definiowanie zmiennych tego typu:
Var A : Int8Arr;

Powysza deklaracja rwnowana jest nastpujcej:


Var A : array [ 0 .. 7 ] of integer;

i odpowiada takiej oto deklaracji jzyka C:


int A[8]

oraz poniszej deklaracji Visual Basica


Dim A[8] of integer

W przedstawionej deklaracji poszczeglne elementy tablicy identyfikowane s kolejnymi liczbami, poczwszy od zera A[0], A[1] itd. lecz minimalna warto indeksu tablicy moe mie w Pascalu dowoln warto. Konieczno indeksowania tablicy poczwszy wanie od zera pokutuje jeszcze w C++; Visual Basic pozby si tego brzemienia w wersji 4.0. Przypumy na przykad, e chcemy pozna liczb pitkw przypadajcych trzynastego dnia miesica w kadym roku dwudziestego stulecia i przechowa t informacj w tablicy ponisza deklaracja wydaje si wwczas najodpowiedniejsza:
Type TFeralne = array [ 1901 .. 2000 ] of byte;

Indeksy tablicy odpowiadaj tutaj wprost bezwzgldnym numerom kolejnych lat. Doln i grn warto graniczn indeksu tablicy wymiarowej zwracaj funkcje Low() i High() ponisza sekwencja wypenia zerami tablic typu Double:
for i := Low(X) to High(X) do X[i] := 0.0;

81

Wskazwka

Na szczegln uwag gdy chodzi o indeksowanie zasuguj tablice znakowe (array [] of Char); deklarowane z zerow wartoci dolnego indeksu, staj si kompatybilne z typem PChar (byo tak ju w wersji 7.0 Turbo Pascala). Wskazane jest zatem ich deklarowanie z zerow graniczn wartoci indeksu, jeeli nie sprzeciwiaj si temu inne wzgldy projektowe.

Repertuar tablic w Object Pascalu nie ogranicza si do tablic jednowymiarowych. Moliwe jest deklarowanie tablic o wikszej liczbie wymiarw; deklaracje poszczeglnych par indeksw granicznych oddzielone s od siebie przecinkami, na przykad:
var G: array [ 1 .. 3, 4 .. 656, 10 .. 10 ] of byte;

Rwnie naturalne s odwoania do poszczeglnych elementw takiej tablicy wielowymiarowej, przykadowo


K := G [2, 5, 4] + F/ G[ 1, 1, -10] * (5 + G [ 1, 1, 1]);

Tablice dynamiczne
Tablice dynamiczne pojawiy si po raz pierwszy w Delphi 4. Deklaracja tablicy dynamicznej definiuje liczb jej wymiarw i typ elementw, jednak nie definiuje a priori indeksw granicznych rozpitoci poszczeglnych wymiarw okrelone zostan dopiero w trakcie wykonywania programu. Zajmijmy si na pocztek jednowymiarowymi tablicami dynamicznymi, za chwil natomiast uoglnimy rozwaania na tablice wielowymiarowe. Oto przykadowa deklaracja jednowymiarowej tablicy dynamicznej:
var A: array of string;

Deklaracja ta definiuje zmienn A jako wektor acuchw tekstowych, nie okrelajc jednake rozmiaru tego wektora. Okrelenie tego rozmiaru, i jednoczenie przydzielenie odpowiedniej iloci pamici, wykonywane jest przez funkcj SetLength():
Readln(N); ... SetLength(A, N);

Ostatnia z powyszych instrukcji ustala rozmiar tablicy A na N elementw (biorc oczywicie pod uwag biec warto zmiennej N). Doln wartoci graniczn indeksu tablicy dynamicznej jest zawsze zero, tote indeksami granicznymi wektora
A bd wartoci 0 oraz N-1; innymi sowy, jeli w czasie wykonania instrukcji SetLength() wartoci N

byo (powiedzmy) 5, to od tej pory tablic A wykorzystywa mona na rwni ze statyczn tablic zadeklarowan jako
array [ 0 .. 4 ] of string;

na przykad w taki sposb:


A[1] := 'Jestem ju penoprawn tablic'; .... Writeln(A[1], A[2]); .... Delete(A[3], 1, Length(A[4]))

Opniona deklaracja wielkoci wymiaru (wymiarw) tablicy dynamicznej nie jest jednake jedyn istotn cech odrniajc j od tablic statycznych. Jej specyfika wie si rwnie z dynamicznym przydziaem pamici, dokonujcym si dopiero w momencie wywoania procedury SetLength(); fizyczn reprezentacj zmiennej okrelajcej tablic dynamiczn (w tym wypadku zmiennej A) jest wskanik. Tablice dynamiczne nale ponadto do zmiennych o kontrolowanym czasie ycia. Oznacza to, e po zakoczeniu czasu ycia tablicy dynamicznej (ktra jest np. zmienn lokaln funkcji/procedury) przydzielona do niej pami jest automatycznie zwalniana (ang. garbage-collected). Moemy te wymusi wczeniejsze wykonanie tej czynnoci, podstawiajc pod zmienn tablicow warto NIL:
A := NIL; // zwolnienie pamici przydzielonej dla tablicy dynamicznej A

Jest to zalecane szczeglnie w odniesieniu do duych tablic dynamicznych, ktrych zawarto przestaa ju by potrzebna. Innym mechanizmem charakterystycznym dla tablic dynamicznych jest oszczdno gospodarowania pamici na podstawie licznika odwoa (podobnie jak w przypadku acuchw AnsiString). Dwie tablice dynamiczne o identycznej zawartoci maj w rzeczywistoci wspln reprezentacj pamiciow, a fakt jej wspdzielenia jest

82

odzwierciedlany przez warto licznika odwoa rwn 2. Z tego faktu wynika pewna niespodzianka. Przyjrzyjmy si poniszemu fragmentowi
var A1, A2 : array of Integer; begin SetLength(A1, 4); A2 := A1; A1[0] := 1; A2[0] := 26; ...

i zgadnijmy, co kryje si pod elementem A1[0]? Poprawna odpowied brzmi: 26. Ot przypisanie A2 := A1 jest de facto utosamieniem tablic A1 i A2, a wspomniana instrukcja dokonuje tylko przepisania wskanika oraz zwikszenia licznika odwoa. Kada zmiana w obrbie tablicy A1 skutkowa bdzie identyczn zmian w obrbie tablicy A2 i vice versa ergo: przypisanie A2[0] := 26 ustala warto elementu A1[0] na 26. Moliwe jest jednake faktyczne powielenie tablicy dynamicznej do tego celu suy funkcja standardowa
Copy(). Po wykonaniu poniszej sekwencji
var A1, A2 : array of Integer; begin SetLength(A1, 4); A2 := Copy(A1); A1[0] := 1; A2[0] := 26;

warto A1[0] bdzie rwna 17. Moliwe jest powielenie jedynie wybranego fragmentu tablicy rdowej. Instrukcja
A2 := Copy (A1, 2,2);

wycina z tablicy A1 elementy A1[2] i A1[3], tworzc z nich zawarto tablicy A2 identycznie do poniszej sekwencji:
SetLength(A2,2); A2[0] := A1[2]; A2[1] := A1[3];

Wielowymiarowe tablice dynamiczne deklaruje si poprzez zagniedanie klauzuli array of; oto przykad tablicy dwuwymiarowej:
var B: array of array of Integer;

Wywoanie procedury SetLength() musi oczywicie uwzgldnia liczb wymiarw, na przykad:


SetLength(B, 5, 7)

nadaje tablicy dynamicznej B struktur


array [ 0 .. 4, 0 .. 6 ] of integer;

Naley w tym miejscu zaznaczy, i moliwe jest tworzenie nieortogonalnych tablic dynamicznych (pojcie ortogonalnoci tablicy wyjanione zostao przy opisie tablic wariantowych). Jest taka moliwo, gdy macierz moe by rozpatrywana jako wektor wektorw, ktre w przypadku tablicy dynamicznej (i tablic wariantowych) nie musz by identyczne. Poniszy przykad przedstawia tworzenie trjktnej macierzy acuchw:
var A : array of array of string; I, J : Integer; begin SetLength(A, 10); for I := Low(A) to High(A) do begin SetLength(A[I], I); for J := Low(A[I]) to High(A[I]) do A[I,J] := IntToStr(I) + ',' + IntToStr(J) + ' '; end; end;

Podobny efekt da o sobie zna w momencie, gdy autorzy Delphi 1 zadecydowali, i wszelkie obiekty reprezentowane bd w programie przez wskaniki do fizycznej reprezentacji. Od tej pory zwyka instrukcja przypisania pomidzy zmiennymi obiektowymi powoduje jedynie powielenie wskanika do istniejcego obiektu, za fizyczne powielenie reprezentacji wykonywane jest w sposb jawny przez metod Assign() (przyp. tum.).

83

Rekordy
Rekord w przeciwiestwie do tablicy nie ma charakteru struktury jednorodnej, lecz stanowi agregat potencjalnie rnych typw. Odpowiednikiem pascalowego rekordu s: struktura jzyka C definiowana za pomoc sowa kluczowego struct oraz typ definiowany (user-defined type) Visual Basica. Oto przykad rekordu w jzyku Object Pascal oraz jego odpowiedniki w C i Visual Basicu:
{ Pascal } Type MyRec = Record i : integer; d : double end;

/* C */ typedef struct { int i; double d; } MyRec;

' Visual Basic Type MyRec i As Integer d As Double End Type

Skadowe rekordu nazywane s jego polami (fields), a odwoania do nich maj posta odwoa kwalifikowanych po nazwie zmiennej nastpuje kropka rozdzielajca i nazwa pola:
var N : MyRec; begin N.i := 23; N.d := 3.4; end;

Aby unikn mudnego powtarzania nazwy zmiennej (w odwoaniach kwalifikowanych), mona uy tzw. instrukcji wicej with, powodujcej, e odwoania do pl rekordu dotycz konkretnej zmiennej. Oto poprzedni przykad po zastosowaniu instrukcji wicej:
var N : MyRec; begin with N do begin i := 23; d := 3.4; end; end;

Rekord pascalowy moe posiada tzw. cz zmienn, zwan rwnie czci wariantow (uwaga: nie myli ze zmiennymi typu Variant!). Interpretacja czci zmiennej rekordu moe odbywa si na jeden ze zdefiniowanych z gry sposobw. Znawcy jzyka C natychmiast rozpoznaj w tym odpowiednik unii (union). Oto przykad rekordu z czci zmienn oraz jego odpowiednik w C++:
Type TVariantRecord = record NullStrField : PChar; IntField : Integer; Case Integer of 0 : (D: Double); 1 : (I: Integer); 2 : (C: Char); End; struct TUnionStruct { char * StrField; int IntField; union { double D; int I; char C;

84

}; };

Zgodnie z powysz definicj, pola D, I oraz C zajmuj ten sam obszar pamici. Cz zmienna rekordu musi wystpi na jego kocu. Nie ma przeciwwskaza, by w czci zmiennej pojawio si pole bdce rekordem zawierajcym take cz zmienn.
Wskazwka

Reguy Object Pascala zabraniaj definiowania w zmiennej czci rekordu pl bdcych zmiennymi o kontrolowanym czasie ycia.

Zbiory
Zbiory (sets) s konstrukcj unikatow, waciw jedynie Pascalowi (chocia C++Builder implementuje klasszablon Set emulujc zbiory pascalowe). Zbiory oferuj wyjtkowo efektywny mechanizm reprezentowania kolekcji zoonych z elementw typw porzdkowych, znakowych lub wyliczeniowych. Zbiory deklaruje si za pomoc klauzuli set of, na przykad
type TCharSet = set of Char;

definiuje zbir znakw typu Char. Oto inny przykad zbioru, zawierajcego dni tygodnia:
Type TWeekDays = (Ni, Pn, Wt, Sr, Cz, Pt, So); // typ wyliczeniowy WeekDaysSet = set of TWeekDays; // zbir oparty na typie wyliczeniowym

I jeszcze jeden rodzaj deklaracji deklaracje zbiorw opartych na typie okrojonym:


DigitsSet = set of 0 .. 9; Litery = 'A' .. 'z';

Liczba elementw zbioru nie moe przekracza 256, natomiast numery porzdkowe jego elementw (Ord()) nie mog wykracza poza przedzia 0 255. Z tego wzgldu ponisze deklaracje s bdne:
TShortIntSet = Set of ShortInt; // Ord(Low(ShortInt)) < 0 TIntSet = set of Word; // wicej ni 255 elementw

TStringSet = set of String; // "String" nie jest typem porzdkowym

Kady kandydat na element zbioru reprezentowany jest przez pojedynczy bit: jedynka oznacza przynaleno do zbioru, zero brak elementu w zbiorze. Rozmiar zmiennej zbiorowej zaleny jest wic od licznoci (mocy) typu, na bazie ktrego zbir zdefiniowano nie przekracza on wic nigdy 32 bajtw. W szczeglnoci, zbiory oparte na typach o mocy nie przekraczajcej 32 elementw cechuj si szczegln efektywnoci ich zmienne nie przekraczaj rozmiaru czterech bajtw, mog wic by w caoci adowane do rejestrw procesora. Stae oznaczajce zbiory zapisuje si w nawiasach prostoktnych jako ogranicznikach, na przykad:
Var Robocze, Parzyste, Wolne, Happy: WeekDaysSet; ... Robocze := [ Pn .. Pt ]; Parzyste := [ Wt, Cz, So ]; Wolne := [ So, Ni ]; Happy := [];

Konstrukcja [] oznacza zbir pusty.

85

Operatory zbiorowe
Ide typu zbiorowego jest odwzorowanie algebry zbiorw, co znajduje odzwierciedlenie w zestawie waciwych temu typowi operatorw. Relacje przynalenoci do zbioru i zawierania zbiorw Obecno danego elementu w zbiorze testowana jest za pomoc operatora in. Oto proste przykady:
var C: Char; I: Integer; const Digits = [ '0' .. '9' ]; .... if not (C in Digits) Then SignalError(); // czy C nie jest cyfr?

if i in [ 1 .. 127, 255 ] Then begin .... end;

Do testowania relacji zawierania zbiorw suy operator <=. Zbir A zawiera si w zbiorze B (co oznaczamy A <= B), jeeli kady element zbioru A jest jednoczenie elementem zbioru B (niekoniecznie na odwrt).
var X1, X2 : WeekDaysSet; ... if X1 <= X2 Then .....

Suma i rnica zbiorw Sum zbiorw A i B oznaczan A + B jest zbir tych elementw, ktre nale do przynajmniej jednego z nich. Rnic zbiorw A i B, oznaczan A B, tworz wszystkie te elementy, ktre nale do zbioru A i jednoczenie nie nale do zbioru B. Oto przykady:
Var A, B, C: set of Char .... A := B + ['0']; ... C := A - B + [' ', '+', ''];

Szczeglnym przykadem tworzenia sumy zbiorw moe by doczanie elementu do zbioru:


var C : Char; ObtainedChars : Set of Char; .... ObtainedChars := []; ...... ObtainedChars := ObtainedChars + C;

i analogicznie tworzc rnic moemy wyczy ze zbioru pojedynczy element:


var C: Char; ReservedChars : Set of Char; ReservedChars := [#0 .. #255]; .... ReservedChars := ReservedChars - C;

Poczwszy od wersji 7.0 Turbo Pascala dostpne s procedury Include() i Exclude() dokonujce doczania elementu do zbioru i wykluczania z niego elementu:
Include(ObtainedChars, C); // to samo co: // ObtainedChars := ObtainedChars + C;

oraz

86

Exclude(ReservedChars, C); // to samo co: // ReservedChars := ReservedChars - C;

Wskazwka

Naley uywa procedur Include() i Exclude() wszdzie tam, gdzie jest to moliwe. S one bowiem realizowane w sposb niezwykle efektywny za pomoc pojedynczej (!) instrukcji procesora, natomiast realizacja sumy (rnicy) zbiorw wymaga 13 + 6n instrukcji (n oznacza tu liczb bitw zajmowanych przez zbir).

Iloczyn zbiorw Iloczyn zbiorw A i B oznaczany A * B tworz te elementy, ktre nale jednoczenie do obydwu zbiorw. Oto przykadowy test, czy dwa zbiory posiadaj wsplne elementy:
var A, B : set of integer; ... if A * B <> [] Then .....

Obiekty
Obiekty stanowi zasadniczy trzon Delphi oraz jzyka Object Pascal w szczeglnoci obiektami s wszystkie komponenty wizualne. Obiekty podobne s do rekordw, mog jednak dodatkowo zawiera jako skadowe procedury i funkcje. Ta uproszczona definicja nie oddaje w peni sensu obiektu pascalowego (obiektom powicony jest obszerny fragment w dalszej czci tego rozdziau), jednak w tym miejscu interesuj nas jedynie podstawy skadniowe obiektu jako jednego z elementw jzyka Object Pascal. Oglna posta definicji obiektu jest nastpujca:
Type TPochodny = class(TMacierzysty) JakiesPole : integer; Procedure JakasProcedura; End;

Cho obiekty C++ rni si od obiektw Delphi, to ich deklaracja jest troch podobna:
class TPochodny : public TMacierzysty { int JakiesPole; void jakasProcedura() }

Procedury oraz funkcje stanowice skadowe obiektu nazywane s jego metodami (methods). Wewntrz deklaracji typu obiektowego znajduje si jedynie nagwek metody, ktry musi by rozwinity w tekcie programu; kompletna definicja metody zawiera oprcz jej nazwy nazw typu obiektowego, ktrego skadow stanowi:
Procedure TPochodny.JakasProcedura; begin { tre metody } end;

Kropka stanowica separator odwoania kwalifikowanego podobna jest do operatora :: jzyka C oraz operatora . (kropki) Visual Basica. Mimo i wszystkie trzy jzyki posiadaj obsug klas, jedynie Object Pascal i C++ umoliwiaj definiowanie nowych klas w sposb cakowicie zgodny z kanonami programowania obiektowego (powrcimy do tej kwestii w dalszej czci rozdziau).

Notatka

Obiekty jzyka C++ zostay zrealizowane w zupenie inny sposb ni obiekty jzyka Object Pascal; ich czenie w ramach jednej aplikacji moliwe jest tylko w wyniku zastosowania specjalnych zabiegw (wicej

87

szczegw znale mona w rozdziale 13. Zaawansowane techniki programistyczne ksiki Delphi 4. Vademecum profesjonalisty). Wyjtkiem od tej zasady s obiekty C++Buildera deklarowane z uyciem dyrektywy __declspec(delphiclass). S one jednak niekompatybilne z regularnymi obiektami C++.

Wskaniki
Wskaniki (pointers) stanowi wskazanie na zmienn, znajdujc si w pamici. Przykadem typu wskanikowego jest poznany ju typ PChar, stanowicy wskazanie na cig znakw (bd na pierwszy znak cigu, zalenie od kontekstu). Kady typ posiada swj odpowiednik wskanikowy (regu t naley stosowa rekurencyjnie istniej wskaniki do wskanikw). W Pascalu wystpuje rwnie tzw. wskanik amorficzny (untyped pointer) stanowicy wskazanie na obszar pamici operacyjnej bez zwizku z konkretnym typem i deklarowany jako Pointer. Poniewa stanowi on jednak odstpstwo od zasady bezpieczestwa typw, nie powinien by naduywany.
Wskaniki s bardzo silnym narzdziem programistycznym. Niezwykle uyteczne w rku dowiadczonego programisty, mog rwnoczenie okaza si skrajnie niebezpieczne (dla aplikacji), gdy s niewaciwie uywane.

Wskaniki typowane (typed pointers) definiowane s za pomoc operatora ^ w poczeniu z typem podstawowym, na ktry wskazuj. Nie dotyczy to oczywicie wskanikw typu Pointer, gdy nie wskazuj one na aden typ. Oto kilka przykadw definicji typw wskanikowych:
Type PInt = ^Integer { typ PInt jest typem wskazujcym na typ Integer } Foo = record Nazwisko : string; Wiek : byte; end; PFoo = ^Foo; { Typ PFoo wskazuje na typ Foo } var P: Pointer P2: PFoo;

{wskanik amorficzny} {wskanik rekordu typu Foo}

Notatka

Znaczenie operatora ^ w Object Pascalu podobne jest do znaczenia operatora * w C. Odpowiednikiem wskanika amorficznego (pointer) jest w C typ void *.

Naley zaznaczy, e wartoci wskanika jest adres pamici zajmowanej przez odnon zmienn. Ewentualna alokacja wskazywanej przez wskanik pamici ley cakowicie w gestii programisty. Moliwe jest uwolnienie wskanika od jakiegokolwiek wskazania; wartoci nie wskazujc na nic jest w Pascalu warto NIL (w Delphi 2 i nastpnych dodatkowo NULL). Reprezentacj takiego pustego wskanika nie s binarne zera. Odwoanie si do wskazywanej zmiennej nastpuje przez uycie operatora ^ (zwanego operatorem dereferencji). Najlepiej wyjani to na przykadach:
Program PtrTest; Type MyRec = record I : Integer; S : String; R : Real; End; PMyRec = ^MyRec; Var Rec : PMyRec; begin

88

New(Rec);

{tworzy dynamiczny rekord typu MyRec, zmienna Rec zawiera wskazanie na niego}

Rec^.I := 10; Rec^.S := 'Co dla odmiany ... ' Rec^.R := 6. 384; ...... Dispose(Rec) ; {zwolnienie zajtej pamici} end;

Wskazwka

Jak alokowa i zwalnia pami?

Jeeli przydzielasz pami dla zmiennej cile okrelonego typu, zawsze uywaj procedury New(). Gwarantuje to przydzielenie pamici w iloci odpowiedniej do typu struktury. Nie zapomnij o zwolnieniu tak przydzielonej pamici za pomoc procedury Dispose().

Zamiast procedur New() i Dispose() mona oczywicie wykorzystywa procedury GetMem() i 8 FreeMem() , lecz jest to mniej bezpieczne. Niekiedy jednak nie da si zastosowa procedury New() i uycie GetMem() jest konieczne typowym przykadem jest alokacja pamici dla cigu znakw PChar wykonywana przez funkcj StrNew(). Nieco bezpieczniejsz od GetMem() jest funkcja AllocMem() wykonuje ona dodatkowo zerowanie przydzielonego obszaru pamici.

Odwoanie si do pamici poza przydzielonym obszarem jest bdem i najczciej koczy si wyjtkiem (Access Violation), jednak na szczcie zasada bezpieczestwa typw redukuje znacznie prawdopodobiestwo takiego zjawiska; jego niebezpieczestwo wzrasta jednak, gdy uywamy wskanikw amorficznych.

Z typami wskanikowymi wie si bardzo ciekawa osobliwo Turbo Pascala dotyczca zgodnoci typw (mogca przyprawi o bl gowy programistw przyzwyczajonych do jzyka C): ot identyczna deklaracja dwch typw nie jest jeszcze gwarancj ich zgodnoci (w sensie regu kompilatora). Oto typowy przykad:
var a : ^integer; b : ^integer; begin ..... a := b;

//tu kompilator zgosi bd

Dla programistw wychowanych na jzyku C stanowi to nie lada zaskoczenie wszak zgodnie z deklaracj
int *a; int *b;

zmienne a i b s tego samego typu. Przyczyn owej niezgodnoci s zaostrzone reguy zgodnoci typw w Pascalu kompilator nie wchodzi w szczegy definicji typw i ewentualna identyczno dwch rnych deklaracji nie ma dla niego znaczenia. Rozwizaniem tego problemu jest jawne zdefiniowanie typu wskazujcego na typ integer:
Type PInteger = ^Integer; var a : PInteger; b : PInteger;

Procedura New(P) jest rwnowana GetMem(P, SizeOf(P^)), natomiast Dispose(P) odpowiada FreeMem(P,SizeOf(P^). Nie dotyczy to jednak w peni typw obiektowych, chocia stosowanie do nich procedur New() i Dispose() z jednym parametrem rwnie nie jest typowe (przyp. tum.).

89

begin ..... a := b; {tu wszystko jest w porzdku}

Aliasy typw
Object Pascal umoliwia definiowanie typw rwnowanych typom ju zdefiniowanym suy do tego prosta dyrektywa zrwnania typw, np.
Type Numerki = Integer;

Od tej chwili typ Numerki nie bdzie si dla kompilatora rni od typu Integer. Nowoci Delphi, niedostpn w Turbo Pascalu s tzw. aliasy typw (strongly typed aliases) oznaczajce zgodno, lecz nie identyczno typw. Deklaracja aliasu ma posta:
Type nowy_typ = type typ_bazowy;

Jej konsekwencj jest wzajemna zgodno obydwu typw w sensie przypisania. Na przykad w wyniku poniszej deklaracji
Type Licznik = type Integer; Var L : Licznik; K : Integer;

obydwa przypisania
L := K; ... K := L;

s poprawne. Typy Licznik i Integer nie s jednak w rozumieniu skadni Object Pascala typami identycznymi, co czyni je niezgodnymi w kontekcie przekazywania do procedur i funkcji parametrw opatrzonych klauzulami var i out. Ponisze fragmenty zostan wic przez kompilator odrzucone:
procedure Dolicz(var X: Licznik); begin ... end; Procedure Verify(var Y: Integer); begin ... end; var L: Integer; N: Licznik; ....... Dolicz(L); Verify(N); // bd // bd

Poprawne s jednak nastpujce konstrukcje:


procedure Zalicz(X: Licznik); begin ... end; Procedure Granica (Y: Integer); begin ... end;

var L: Integer; N: Licznik; .......

90

Zalicz(L); Granica(N);

Rozrnialno pomidzy typem bazowym a jego aliasem ma szczeglne znaczenie w przypadku waciwoci klas, pozwala bowiem tworzy odrbne edytory waciwoci dla dwch rnych typw o identycznej strukturze. Zajmiemy si t kwesti obszerniej w rozdziale 12.

Rzutowanie i konwersja typw


Rzutowanie typw (typecasting) stanowi jeden ze sposobw osabienia rygorystycznej kontroli typw wykonywanej przez kompilator. Nie kade naruszenie zasad bezpieczestwa typw jest bdem wszystko zaley od tego, czy programista wie, co robi. Przeanalizujmy poniszy przykad:
var c : Char; b : Byte; begin ... c := 's'; b := c;

{ tu kompilator zaprotestuje}

Ostatnie przypisanie zostanie przez kompilator zakwestionowane, gdy zmienne b i c s zupenie rnych typw. Intencj programisty byo prawdopodobnie potraktowanie obszaru pamici na dwa sposoby: raz jako znaku, raz jako bajtu (przypomina to nieco zmienn cz rekordu). Moemy to osign, nakazujc kompilatorowi, by potraktowa zmienn c jak bajt9 czyli rzutujc t zmienn na typ byte:
var c : Char; b : Byte; begin ... c := 's'; b := byte(c); { to kompilator zaakceptuje} { b zawiera kod znaku 's' }

Rzutowanie typw jest narzdziem bardzo silnym (a wic dla aplikacji potencjalnie niebezpiecznym), chocia stosowane waciwie, daje wyrane korzyci. Jest ono w zasadzie tylko inn interpretacj bitowego wzorca zmiennej, w szczeglnoci nie jest zwizane z adnymi konwersjami. Niezrozumienie tego faktu prowadzi do tworzenia bezsensownych konstrukcji, jak w poniszym przykadzie:

Var k : integer; s : single; begin s := 1.0; k := integer(s);

W Delphi 6 ostatnia instrukcja jest niepoprawna; bya ona poprawna jeszcze w Delphi 5, mimo to, nawet wwczas naiwnoci byoby oczekiwa, e wartoci zmiennej k po wykonaniu ostatniego przypisania jest 1 (w rzeczywistoci zmienna ta miaaby warto 1065353216 przyp. tum.). Zmienne typu integer i single przechowywane s w pamici w zupenie rny sposb i warto ta stanowi wynik interpretacji wzorca zmiennej typu single na mod typu integer. Intencj programisty byo tu zapewne przeksztacenie liczby zmiennoprzecinkowej na odpowiadajc jej liczb cakowit. Takie przeksztacenie zwane konwersj typw wykonywane jest w Object Pascalu w sposb jawny, za pomoc funkcji standardowych, bd te w sposb niejawny przez kompilator, na przykad:
Var s : single; k : integer; begin

O takiej samej reprezentacji bitowej (przyp. tum.).

91

..... s := 4.7; k := Trunc(s); k := Round(s); ..... k := 3; s := k;

// przeksztacenie jawne, k = 4 // przeksztacenie jawne, k = 5

// przeksztacenie ukryte, s = 3.0

Zakres i reguy przeksztace domylnych s cile okrelone wrcimy do tego w dalszej czci ksiki. Rzutowanie typw obiektowych wymaga oddzielnego omwienia zajmiemy si tym w dalszej czci rozdziau.

Notatka

Warunkiem koniecznym (lecz nie wystarczajcym) wykonalnoci rzutowania typw jest identyczny rozmiar zmiennej podlegajcej rzutowaniu i typu docelowego.

Zasoby acuchowe
W Object Pascalu stae tekstowe (acuchy) przechowywane s standardowo w kodzie aplikacji. Koncepcja ta narodzia si jeszcze w Turbo Pascalu, a jej konsekwencj byo z jednej strony odcienie ograniczonego do 64 kB segmentu danych, z drugiej za po pojawieniu si DPMI i sprztowych mechanizmw ochrony dodatkowe zabezpieczenie staych tekstowych przed modyfikacj, zamierzon lub przypadkow. W rodowisku Win32, wobec rezygnacji z segmentowanego modelu pamici, aktualna jest jedynie druga z wymienionych przesanek. Wraz z Delphi 3 pojawi si alternatywny sposb przechowywania staych tekstowych, mianowicie w zasobach acuchowych (string resources), umieszczanych przez kompilator w generowanym pliku zasobowym *.RES. Nowoci s oczywicie nie same zasoby acuchowe, lecz sposb ich integracji z kodem projektu. Ot acuchy przeznaczone do przechowania w takich szczeglnych zasobach deklarowane s tak, jak zwyke stae tekstowe, z t rnic, i zamiast dyrektywy const wystpuje dyrektywa resourcestring, jak w poniszym przykadzie:
resourcestring HelloMsg = 'Hello'; WorldMsg = 'world'; var String1: String; begin String1 := HelloMsg + ' ' + WorldMsg + '!'; Writeln(String1); ... end;

Program wypisuje historyczne ju dla Pascala powitanie Hello world!. Stae tekstowe HelloMsg oraz WorldMsg zdefiniowane zostay przy uyciu dyrektywy resourcestring; skutkuje to z jednej strony umieszczeniem ich przez kompilator w generowanych zasobach i automatycznym doczaniem tych zasobw do aplikacji, z drugiej natomiast automatycznym ich adowaniem w czasie wykonania programu, za pomoc niejawnych wywoa funkcji LoadString(). I to wszystko bez zaprztania uwagi programisty. Najwaniejsz jednak korzyci, pync z oddzielenia staych tekstowych od kodu aplikacji, jest niewtpliwie uatwiona zmiana jej wersji jzykowej, ktra manifestuje si przede wszystkim w postaci wypisywanych komunikatw (cho nie tylko). acuchy deklarowane z uyciem dyrektywy resourcestring doczane s do pliku .EXE (lub .DLL) jako zasoby, zatem kwestia zmiany wersji jzykowej aplikacji sprowadza si wwczas do wymiany tyche zasobw, bez koniecznoci ponownej kompilacji kodu rdowego.

92

Instrukcje warunkowe
W kolejnych punktach przedstawimy dwie instrukcje warunkowe: instrukcj if oraz instrukcj wyboru case. Zakadamy, e nie s one dla Czytelnika nowoci, ograniczymy si do ich porwnania z odpowiednikami w C i Visual Basicu.

Instrukcja If
Instrukcja if umoliwia uzalenienie wykonania pewnej instrukcji od spenienia okrelonego warunku. Oto przykady:
{ Pascal } if x = 4 then y := x; /* C */ if ( x == 4 ) y = x; ' Visual Basic if x = 4 Then y = x

Ostrzeenie

Pamitaj, e w Pascalu operatory boolowskie maj wyszy priorytet ni operatory porwnania. Testujc wic koniunkcj czy alternatyw kilku warunkw, nie zapomnij o ujciu kadego z nich w nawiasy, na przykad:

if

(x = 7) and (y = 8) Then ....

Opuszczenie nawiasw spowoduje w tym wypadku bd kompilacji.

Instrukcja uzaleniona od spenienia warunku, zwana instrukcj uwarunkowan, moe by instrukcj zoon; innymi sowy, warunek moe wiza midzy nawiasami pionowymi begin i end kilka instrukcji, jak w poniszym przykadzie:
if x = 6 Then begin Cokolwiek; ATerazCosInnego; IJeszczeCos; end;

W jzyku C nawiasami pionowymi s nawiasy { i }. Moliwe jest te testowanie caej kaskady warunkw:
if x = 100 Then FunkcjaDlaSetki Else if x = 200 Then SpecjalnieDla200 Else if x = 300 Then IteracjaDla300 Else begin SytuacjaAwaryjna; UstawIndeks; end;

93

Instrukcja wyboru
Instrukcja ta pozwala na wykonanie jednej instrukcji z podanego zestawu, na podstawie wartoci wyraenia testowego, zwanego selektorem. Instrukcja wyboru moe by zastpiona kaskadow instrukcj if, lecz jest od niej zdecydowanie zgrabniejsza i wygodniejsza:
case x of 100: FunkcjaDlaSetki; 200: SpecjalnieDla200; 300: IteracjaDla300; Else begin SytuacjaAwaryjna; UstawIndeks; end; end;

Notatka

Selektor w instrukcji wyboru musi by wyraeniem typu porzdkowego (ordinal type), natomiast wartoci okrelajce poszczeglne warianty musz by staymi. Oznacza to, e instrukcja wyboru nie nadaje si np. do testowania wariantw acuchowych.

Oto odpowiednik prezentowanego przykadu w jzyku C:


switch (x) { case 100: FunkcjaDlaSetki; break; case 200: SpecjalnieDla200; break; case 300: IteracjaDla300; break; default: { SytuacjaAwaryjna; UstawIndeks; } }

Ptle
Ptle su do kontrolowanego powtarzania blokw instrukcji. Przedstawimy trzy rne rodzaje ptli w Object Pascalu w jzyku C istniej podobne konstrukcje, w Visual Basicu s one nieco bardziej rozbudowane.

Ptla For
Typowym zastosowaniem ptli For jest wykonanie danego bloku instrukcji pewn z gry okrelon liczb razy. Oto przykad obliczajcy sum kwadratw pierwszych stu liczb nieparzystych:
Var i, k, sum : integer; begin sum := 0; for i := 1 to 100 do begin k := i + i - 1; // kolejna liczba nieparzysta sum := sum + k * k; end; ..... end;

Oto rwnowana konstrukcja w C:

94

void main(void) { int i, k, sum; sum = 0; for (i=1; i<=100; i++) { k = i + i - 1; sum += k * k } ... }

i w Visual Basicu:
sum := 0; For i = 1 to 100 k = i + i - 1; sum = sum + k * k; Next i

Notatka

Modyfikacja zmiennej sterujcej ptli For, dozwolona (cho mocno problematyczna) w Delphi 1 oraz poprzednich wersjach Pascala, poczwszy od Delphi 2 jest wyranie zabroniona; bez tego ograniczenia moliwoci optymalizacji ptli przez kompilator byyby mocno uszczuplone.

Ptla While...Do
Instrukcja ptli WhileDo powoduje cykliczne powtarzanie instrukcji uwarunkowanej tak dugo, jak dugo speniony jest okrelony warunek. Warunek sprawdzany jest przed wykonaniem instrukcji uwarunkowanej, wic moliwe jest, e instrukcja ta nie zostanie wykonana ani razu. Jednym z typowych zastosowa ptli WhileDo jest przetwarzanie kolejnych linii pliku tekstowego, a do jego wyczerpania (warunek koca pliku Eof() jest prawdziwy, jeeli w pliku nie ma ju adnej linii do pobrania10).
Program CzytajTekst; {$APPTYPE CONSOLE} Var F: TextFile; S: String; Begin AssignFile(F, 'PLIK.TXT'); Reset(F); while not EOF(F) do begin readln(F,S); writeln(S); end; closeFile(F); end.

Pascalowa ptla WhileDo funkcjonuje w sposb analogiczny do ptli While w jzyku C i ptli Do While w Visual Basicu.

Ptla Repeat...Until
W przeciwiestwie do instrukcji While...Do, warunek uzaleniajcy wykonywanie bloku instrukcji sprawdzany jest po jego wykonaniu, wic ptla zostaje wykonana przynajmniej raz. Poniszy przykad sumuje kwadraty kolejnych liczb nieparzystych do momentu, kiedy wynik przekroczy warto 10000:

10

a nie po nieudanej prbie odczytania linii, dlatego musi by sprawdzany przed odczytem (przyp. tum.)

95

Program Sumowanie; {$APPTYPE CONSOLE} var liczba, sum: integer; begin sum := 0; liczba := -1; repeat Inc(liczba, 2); Inc(sum, liczba * liczba); until sum > 10000; writeln('Ostatni uwzgldnion liczb jest ', liczba); end.

Procedura Break()
Wywoanie procedury Break wewntrz ptli While, Repeat lub For powoduje jej natychmiastowe zakoczenie. Jeeli ptle s zagniedone, nastpuje zakoczenie najbardziej wewntrznej ptli zawierajcej wywoanie procedury Break. Oto fragment programu czytajcy i wypisujcy zawarto pliku tekstowego a do napotkania pustej linii:
{$APPTYPE CONSOLE} Var F: TextFile; S: String; Begin AssignFile(F, 'PLIK.TXT'); Reset(F); Repeat Readln(F,S); Writeln(S); Until S = ''; CloseFile(F); End.

Program ten jednak zaamie si, jeeli plik nie bdzie zawiera pustej linii prba wykonania instrukcji
Readln po wyczerpaniu jego zawartoci spowoduje wygenerowanie wyjtku. Naley wic sprawdza warunek Eof(F) przed wykonaniem tej instrukcji i gdy jest prawdziwy, przerywa ptl repeat:
{$APPTYPE CONSOLE} Var F: TextFile; S: String; Begin AssignFile(F, 'PLIK.TXT'); Reset(F); Repeat if Eof(f) then Break; Readln(F,S); Writeln(S); Until S = ''; CloseFile(F); End.

Procedura Continue ()
Wywoanie procedury Continue wewntrz ptli While, Repeat lub For powoduje porzucenie biecego cyklu ptli i natychmiastowe przejcie do sprawdzania jej warunku. Oto zmodyfikowany poprzedni przykad jeeli odczytana linia zaczyna si od rednika, nie jest w ogle wypisywana:
Program CzytajTekst;

96

{$APPTYPE CONSOLE} Var F: TextFile; S: String; Begin AssignFile(F, 'PLIK.TXT'); Reset(F); Repeat if EOF(F) Then Break; readln(F,S); if Copy(S,1,1) = ';' Then continue; // to samo co "skok" do linii Until writeln(S); Until S = ''; CloseFile(F); End.

Procedury i funkcje
Procedury i funkcje stanowi wydzielone czci aplikacji, wykonujce cile okrelony, zamknity blok instrukcji. Dodatkowo, funkcja zwraca pod sw nazw warto okrelonego typu. W jzyku C istniej wycznie funkcje; odpowiednikiem procedury jest funkcja, ktra zwraca wynik nieznaczcy (void). Przykady uycia funkcji i procedur przedstawia wydruk 2.1.

Wydruk 2.1. Przykadowe funkcje i procedury


Program FuncProc; {$APPTYPE CONSOLE} Procedure Ponad10 (i : integer); // wypisuje komunikat, jeeli parametr jest wikszy od 10 begin if i > 10 Then Writeln ('Ponad 10.'); end;

Function Nieujemna( i : integer) : Boolean; begin Result := ( i >= 0 ); end; var Num : Integer; begin Num := 23; Ponad10(Num); if Nieujemna(Num) Then Writeln ('Nieujemna.') Else Writeln ('Ujemna.'); end.

Na specjalny komentarz zasuguje zmienna Result wykorzystana w funkcji Nieujemna. Symbolizuje ona warto zwracan przez funkcj. Gdy wystpuje po lewej stronie operatora przypisania jest rwnowana nazwie funkcji. Nie mona jej jednak zastpi nazw funkcji, gdy wystpuje po prawej stronie operatora przypisania: wystpienie w tym miejscu nazwy funkcji oznacza jej wywoanie (w tym wypadku rekursywne), za wystpienie zmiennej Result jej wartociowanie. Ponisza funkcja jest cakowicie poprawna
function SignPower(X:Real; N:Integer):Real; var i: integer; Y : real;

97

begin Result := 1.0; for i := 1 to Abs(N) do begin Result := Result * X; end; if N < 0 then Result := 1.0/Result; end;

Jeeli jednak zamienimy zmienn Result na nazw funkcji, otrzymamy kod bdny syntaktycznie, bo funkcja SignPower wywoywana jest bez parametrw:
function SignPower(X:Real; N:Integer):Real; var i: integer; Y : real; begin SignPower := 1.0; for i := 1 to Abs(N) do begin SignPower := SignPower * X; // ta instrukcja jest bdna syntaktycznie end; if N < 0 then SignPower := 1.0/ SignPower; // ta instrukcja jest bdna syntaktycznie end;

Gdybymy wykonali podobny zabieg z funkcj bezparametrow, program wpadby w nieskoczon rekurencj.

Ostrzeenie

Nie naley myli przypisania do zmiennej Result z instrukcj return jzyka C ta ostatnia, wskazujc zwracan warto, powoduje jednoczenie zakoczenie dziaania funkcji (podobnie jak pascalowa instrukcja Exit); przypisanie wartoci zmiennej Result jest natomiast tylko przypisaniem i moe odbywa si w ciele funkcji wielokrotnie.

Uycie zmiennej Result dopuszczalne jest tylko wtedy, gdy ustawiony jest przecznik $X+ (lub zaznaczona jest opcja Extended Syntax na karcie Compiler opcji projektu).

Przekazywanie parametrw do procedur i funkcji


Od pocztku istnienia jzyka Pascal, parametry procedur i funkcji poddane byy niezwykle rygorystycznym reguom, zwikszajcym co prawda bezpieczestwo programowania, lecz jednoczenie ograniczajcym w znacznym stopniu swobod i naturalno wyraania koncepcji programistycznych wiedz o tym a nadto dobrze ci, ktrym przyszo zmaga si z rnymi wersjami Pascala poprzedzajcymi Turbo Pascal. Rygoryzm ten ulega stopniowemu agodzeniu w kolejnych wersjach Turbo Pascala e przywoamy tylko tablice otwarte i parametry amorficzne lecz prawdziw rewolucj przyniosy dopiero Delphi 1, oferujc hybrydowe tablice array of const, stanowice tak naprawd pierwszy znaczcy krok w kierunku elastycznych nagwkw procedur i funkcji, oraz Delphi 2 ze swoim typem Variant. Pisalimy ju wczeniej o udogodnieniach wprowadzonych w Delphi 4 przecianiu i parametrach domylnych obecnie zajmiemy si parametrami procedur i funkcji w sposb bardziej systematyczny.

Przekazywanie parametrw przez warto


Przekazanie parametru przez warto wie si z automatycznym utworzeniem jego lokalnej kopii, dostpnej przez oryginaln nazw parametru formalnego innymi sowy, wszystkie operacje wykonywane w treci

98

procedury na parametrze, wykonywane s na jego kopii lokalnej, oryginalny parametr aktualny zostaje zatem nienaruszony. Eliminuje to moliwo przypadkowego zmienienia go przez procedur, lecz uwaga wie si niekiedy z wykorzystaniem do znacznego obszaru stosu (tam zostaje utworzona wspomniana kopia lokalna). W aplikacjach 16-bitowych stanowio to czsto nie lada problem, w 32-bitowych wersjach Delphi sprawa jest moe mniej dotkliwa, niemniej jednak naley mie wiadomo opisanego zjawiska. Deklaracja parametru przekazywanego przez warto nie zawiera adnego dodatkowego sowa kluczowego, a jedynie nazw parametru i jego typ:
Procedure TakaSobie ( s : string );

Przekazywanie parametrw przez referencj


Przekazywanie parametru przez referencj, zwane rwnie przekazaniem przez adres lub przez zmienn, powoduje, e w treci procedury pod nazw parametru formalnego kryje si rzeczywisty parametr aktualny, a wszystkie operacje wykonywane s bezporednio na nim. Jest to wic waciwy sposb na przekazanie przez procedur wartoci zwrotnej do programu wywoujcego po zakoczeniu wykonywania funkcji procedury warto parametru odzwierciedla wszystkie wykonane na nim operacje. Parametr przekazywany przez referencj musi wic posiada zdolno do przypisywania mu wartoci, wic odpowiadajcym mu parametrem aktualnym musi by L-wyraenie, czyli np. zmienna, element tablicy lub dereferencja wyraenia wskanikowego. Deklaracja parametru przekazywanego przez referencj polega na poprzedzeniu jego nazwy sowem kluczowym var:
Procedure TakaSobie ( var m : integer ); begin m := m + (m div 2); end; ..... var k: integer; begin k := 3; TakaSobie(k); { warto k wynosi teraz 4 } ....

Zasada bezpieczestwa typw wymaga, by parametr aktualny przekazywany przez referencj by dokadnie tego samego typu, co odpowiadajcy mu parametr formalny; w przeciwnym wypadku wystpi bd kompilacji. Przekazywanie parametru przez zmienn nie powoduje obcienia stosu, na ktrym odkadany jest jedynie wskanik do parametru aktualnego gdy nie jest sporzdzana kopia tego parametru. W jzyku C++ odpowiednikiem pascalowego przekazywania parametru przez referencj jest przekazywanie referencji parametru aktualnego (z uyciem operatora &).

Przekazywanie parametrw przez sta


Ten sposb przekazywania parametrw pojawi si w wersji 7.0 Turbo Pascala i czy w sobie zalety dwch poprzednich sposobw. Z jednej strony, pod wzgldem skadniowym, parametr taki podlega tym samym reguom, co parametr przekazywany przez warto; jednak z drugiej strony, na stosie odkadany moe by adres11parametru,nie jego kopia zalenie od tego, co kompilator uzna za bardziej efektywne. Poniewa parametr aktualny nie musi ju by L-wyraeniem, naley zatroszczy si o jego niezmienno. Jest ona

Nie ma jednak gwarancji, i do procedury (funkcji) faktycznie zostanie przekazany adres parametru kompilator moe zastosowa przekazanie przez warto, jeli uzna to za bardziej optymalne. Nie naley ponadto zapomina, i parametrem aktualnym moe by w tym przypadku wyraenie obliczona warto tego wyraenia odkadana jest w tymczasowej zmiennej roboczej i to wanie adres tej zmiennej przekazywany jest do procedury (funkcji); moe si tak sta nawet wwczas, gdy wyraenie to jest L-wyraeniem (np. zmienn prost). Jeeli wic zdefiniujemy nastpujc funkcj
function AddrOfCParm(const X:SmallInt):pointer; begin result := @X; end;

11

nie mamy gwarancji, e wywoanie AddrOfCParm(MyVar) zwrci adres zmiennej MyVar (przyp. tum.).

99

gwarantowana na drodze syntaktycznej: kompilator nie dopuci bowiem adnej konstrukcji mogcej zmieni warto parametru12. Deklaracja parametru przekazywanego przez sta polega na poprzedzeniu jego nazwy sowem kluczowym
const:
type TMyRecord = record Counters: array[0..1000] of integer; Labels: array[0..1000] of String[30]; end; Procedure InitPivotElement ( const X : TMyRecord ); begin with X do begin Counters[0] := 0; // ta instrukcja spowoduje bd kompilacji Labels[0] := 'Pivot'; // ta rwnie end; end; Function CountVowels(const S:ShortString):byte; const Vowels = ['A','E','I','O','U','Y']; var i: byte; begin Result := 0; for i := 1 to Length(S) do begin if UpCase(S[i]) in Vowels then inc(Result); end; end; var T: ShortString; K, L : byte; K := CountVowels(T); // to wywoanie jest poprawne L := CountVowels('Exportable'); // to rwnie

Naley przyj zasad, e przekazanie parametru przez sta jest sposobem najodpowiedniejszym w przypadku, gdy parametr jest parametrem wejciowym (tzn. nie przekazuje informacji zwrotnej) i nie zamierzamy wykorzystywa jego kopii lokalnej jako obszaru roboczego.

Mechanizm tablic otwartych


Przekazywanie tablic do procedur i funkcji od pocztku stwarzao w Pascalu problemy. W oryginalnej, wzorcowej wersji jzyka z lat 70., funkcja obliczajca wyznacznik macierzy o rozmiarach 33 bya ju

Mona jednak z atwoci oszuka kompilator, przekazujc w zagniedonym wywoaniu wskanik parametru przekazanego przez sta, jak w poniszym przykadzie:
Type PMyRec = ^TMyRec; TMyRec = record a, b: integer end; procedure Inner(X: PMyRec); begin X^.a := 1; end; procedure Outer(const Y: TMyRec); begin Inner(@Y); end;

12

100

nieodpowiednia dla macierzy 55, bo z punktu widzenia Pascala byy to dwa rne typy danych. Pierwszym rozwizaniem tego problemu sta si tzw. schemat tablicy uzgadnianej, gdzie graniczne wartoci indeksw przekazywane byy wraz z identyfikatorem tablicy w miejsce pojedynczego parametru formalnego. Schemat ten nie znalaz jednak zastosowania w Turbo Pascalu i Delphi, nie bdziemy si wic nim zajmowa. Innym rozwizaniem opisanego problemu sta si mechanizm tablic otwartych, wprowadzony do Turbo Pascala w wersji 6.0. Ogranicza si on jednak tylko do tablic jednowymiarowych jeeli chcielibymy przekaza do procedury np. macierz, musimy j potraktowa jak tablic wektorw. Deklaracja tablicy otwartej sprowadza si do okrelenia typu jej elementw, podobnie jak w przypadku tablic dynamicznych:
procedure KazdaTablica1 ( var X : array of integer ); procedure KazdaTablica2 ( const X : array of integer ); procedure KazdaTablica3 ( X : array of integer );

W miejsce parametru X powyszych procedur mona przekaza dowoln tablic liczb typu integer, mona te wpisa zawarto tablicy explicite, za pomoc tzw. konstruktora tablicowego, wymieniajcego kolejne jej elementy, na przykad konstruktor
[1,2,3,4,5]

definiuje picioelementow tablic, ktrej elementami s pocztkowe liczby naturalne13. Poniewa z punktu widzenia skadni konstruktor tablicowy jest sta, nie mona przekaza go przez referencj.

Aby w treci procedury (funkcji) pozna wielko tablicy przekazanej jako parametr aktualny (w miejsce parametru formalnego bdcego tablic otwart), mona wykorzysta funkcje Low() i High(). Ale uwaga: z punktu widzenia procedury (funkcji), jej parametr, bdcy tablic otwart, postrzegany jest jako tablica indeksowana od zera (zero-based array), tak wic funkcja Low() zwraca dla niej zawsze 0, za funkcja High() warto mniejsz o 1 od faktycznej liczby elementw. Poniszy fragment programu wypisuje wartoci 0 i 4:
procedure WypiszRozmiary(var X:array of real); begin writeln(Low(X),' ',High(X)); end; var Vector: array [3 .. 7] of Real; WypiszRozmiary(Vector);

Oto jeszcze jeden przykad zastosowania tablicy otwartej ponisza funkcja oblicza redni arytmetyczn elementw wektora:
function RealAverage(const aVector: array of Real):Real; var i: integer; begin Result := 0.0; for i := Low(aVector) to High(aVector) do Result := Result + aVector[i]; Result := Result/(High(aVector)-Low(aVector)+1); end;

13 Zwr uwag, i konstruktor tablicowy moe wyglda tak samo jak zbir (set), dlatego jedynym dozwolonym jego zastosowaniem jest rola parametru aktualnego procedury (funkcji) nie mona uy go w wyraeniu ani w instrukcji przypisania (przyp. tum.).

101

Prawdziw rewolucj w zakresie skadni Pascala s z pewnoci tablice heterogeniczne. Tablica heterogeniczna to tablica zawierajca elementy rnych typw; nie da si jej zdefiniowa jako typ, a jedynie zapisa w postaci konstruktora tablicowego, na przykad:
['Delphi 6', TRUE, 1, 3.5, @MyFunc, X-Y]

Tablicy heterogenicznej nie mona uy w wyraeniach; jedynym jej zastosowaniem jest rola parametru aktualnego procedur i funkcji. Parametr formalny odpowiadajcy tablicy heterogenicznej deklarowany jest za pomoc frazy array of const:
procedure CoMyTuMamy(A: array of const); procedure ZobaczmyJeszczeTo (const A: array of const);

Mimo i moliwe jest zadeklarowanie procedury (funkcji) z tablic heterogeniczn przekazywan przez referencj
procedure ToTezCiekawe(var A: array of const);

to parametrem aktualnym odpowiadajcym takiej deklaracji moe by tylko inny parametr array of const, na przykad:
procedure First(var X: array of const); begin end; procedure Second(Y: array of const); begin First(Y); end;

W treci procedury (funkcji) liczb elementw tablicy heterogenicznej moemy pozna za pomoc funkcji High() wszak jest to odmiana tablicy otwartej. Jeeli natomiast chodzi o typy poszczeglnych elementw, to tablica heterogeniczna postrzegana jest wewntrz procedury (funkcji) jako tablica o strukturze
array [ 0 .. ] of TVarRec

gdzie TVarRec jest nastpujcym rekordem:


TVarRec = record case Byte of vtInteger: vtBoolean: vtChar: vtExtended: vtString: vtPointer: vtPChar: vtObject: vtClass: vtWideChar: vtPWideChar: vtAnsiString: vtCurrency: vtVariant: vtInterface: vtWideString: vtInt64: end; (VInteger: Integer; VType: Byte); (VBoolean: Boolean); (VChar: Char); (VExtended: PExtended); (VString: PShortString); (VPointer: Pointer); (VPChar: PChar); (VObject: TObject); (VClass: TClass); (VWideChar: WideChar); (VPWideChar: PWideChar); (VAnsiString: Pointer); (VCurrency: PCurrency); (VVariant: PVariant); (VInterface: Pointer); (VWideString: Pointer); (VInt64: PInt64);

Informacj o typie elementu czyli wariancie rekordu TVarRec odpowiadajcym elementowi zawiera pole VType. Znaczenie poszczeglnych jego wartoci jest nastpujce:
vtInteger vtBoolean vtChar vtExtended vtString = = = = = 0; 1; 2; 3; 4;

102

vtPointer vtPChar vtObject vtClass vtWideChar vtPWideChar vtAnsiString vtCurrency vtVariant vtInterface vtWideString vtInt64

= = = = = = = = = = = =

5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16;

Ponisza procedura wykorzystuje t informacj, wypisujc typ poszczeglnych elementw:


procedure ZawartoscTablicy (A: array of const); var i: Integer; TypeStr: string; begin for i := Low(A) to High(A) do begin case A[i].VType of vtInteger : TypeStr := 'Integer'; vtBoolean : TypeStr := 'Boolean'; vtChar : TypeStr := 'Char'; vtExtended : TypeStr := 'Extended'; vtString : TypeStr := 'String'; vtPointer : TypeStr := 'Pointer'; vtPChar : TypeStr := 'PChar'; vtObject : TypeStr := 'Object'; vtClass : TypeStr := 'Class'; vtWideChar : TypeStr := 'WideChar'; vtPWideChar : TypeStr := 'PWideChar'; vtAnsiString : TypeStr := 'AnsiString'; vtCurrency : TypeStr := 'Currency'; vtVariant : TypeStr := 'Variant'; vtInterface : TypeStr := 'Interface'; vtWideString : TypeStr := 'WideString'; vtInt64 : TypeStr := 'Int64'; end; ShowMessage(Format('Element %d jest typu %s', [i, TypeStr])); end; end;

Zasig deklaracji
Zasig deklaracji (scope) jest pojciem zwizanym z obowizywaniem poszczeglnych deklaracji w poszczeglnych fragmentach programu. I tak, zmienne globalne, deklarowane w programie gwnym (projekcie) widoczne s w caym programie, natomiast zmienne lokalne deklarowane w procedurze nie s widoczne na zewntrz niej. Oto prosty przykad:

Wydruk 2.2. Ilustracja zasigu deklaracji


Program Zasieg; {$APPTYPE CONSOLE} const StalaGlobalna = 100; var ZmiennaGlobalna : Integer; R : Real; Procedure Przykladowa ( var R

: Real );

103

var ZmiennaLokalna : real; begin ZmiennaLokalna := 10.0; R := R - ZmiennaLokalna; end; begin { pocztek programu gwnego } ZmiennaGlobalna := StalaGlobalna; R := 4.593; Przykladowa(R) end.

Na poziomie globalnym definiowane s tutaj trzy elementy: staa StalaGlobalna oraz zmienne ZmiennaGlobalna i R. Procedura Przykladowa deklaruje w swym wntrzu zmienn lokaln ZmiennaLokalna prba uycia tej zmiennej poza procedur spowoduje bd kompilacji. Procedura ta deklaruje take parametr o nazwie R ma on tak sam nazw, jak jedna ze zmiennych globalnych i tym samym przesania t ostatni w treci procedury. Identyfikator R ma wic rne znaczenie wewntrz procedury i na zewntrz niej.

Moduy
Moduy (units) stanowi podstawowe jednostki programu, grupujce deklaracje oraz procedury i funkcje, osigalne zarwno z programu gwnego, jak i z poziomu poszczeglnych moduw. Kady modu skada si obowizkowo z nastpujcych elementw: Dyrektywa UNIT. Stanowi ona pierwsz lini moduu i zawiera jego nazw poprzedzon sowem unit. Nazwa moduu musi by tosama z nazw pliku, w ktrym si on znajduje; modu o nazwie BANKI musi znajdowa si w pliku BANKI.PAS. Cz publiczna. Rozpoczyna si od dyrektywy interface i zawiera te czci moduu (stae, zmienne, nagwki procedur i funkcji itp.), ktre maj by widoczne dla innych moduw oraz programu gwnego. Zwracamy uwag, e deklaracje procedur i funkcji w czci publicznej ograniczaj si jedynie do nagwkw. Cz prywatna. Rozpoczyna si od dyrektywy implementation, oznaczajcej zarazem koniec czci publicznej, i zawiera te elementy moduu, ktre nie maj by widoczne na zewntrz niego. Zawiera rwnie pene teksty procedur oraz funkcji zadeklarowanych w czci publicznej (mona pomin parametry, o ile nie korzysta si z przeciania i parametrw domylnych lecz nie jest to konieczne). Ponadto, w module mog opcjonalnie wystpi nastpujce elementy: Cz inicjacyjna. Rozpoczyna si od dyrektywy initialization, a koczy dyrektyw end, koczc rwnoczenie cay modu, bd te sowem finalization (patrz nastpny punkt). Instrukcje znajdujce si w czci inicjacyjnej wykonywane s jednokrotnie, podczas rozpoczynania pracy programu, a kolejno wykonywania czci inicjacyjnych poszczeglnych moduw zaley od ich wzajemnego uzalenienia wynikajcego z dyrektyw uses (o tym za chwil). Programy nie powinny uzalenia swego dziaania od kolejnoci wykonywania czci inicjacyjnych poszczeglnych moduw, gdy kolejno ta moe si zmieni na skutek drobnej nawet modyfikacji ktrego z moduw. Cz koczca. Jeeli w ogle wystpuje, to jest ostatni czci moduu. Rozpoczyna si od dyrektywy finalization a koczy dyrektyw end, koczc zarazem cay modu. Pojawia si w Delphi 2, zastpujc znany z Turbo Pascala mechanizm tzw. funkcji koczcych ExitProc (mechanizm ten istnia jeszcze w Delphi 1, wspierany dodatkowo przez funkcj AddExitProc). Podobnie jak w przypadku czci inicjujcej, nie naley zakada adnej okrelonej kolejnoci wykonywania czci koczcych poszczeglnych moduw.

Dyrektywa uses
Dyrektywa uses w module lub programie gwnym specyfikuje list moduw, do ktrych wystpuj odwoania. Nazwy poszczeglnych moduw na licie oddzielone s przecinkami:
uses

104

Scans, Convert, Arith;

Dyrektywa uses moe wystpi w czci publicznej i (lub) w czci prywatnej. Nazwy znajdujce si na licie uses w czci publicznej, obowizujce s w caym module; lista uses w czci prywatnej moduu nie jest natomiast widoczna w jego czci publicznej. Niedopuszczalne jest wystpienie tej samej nazwy na obydwu listach. Oto schemat prostego moduu:
unit FooBar; interface uses BarFoo; // tu deklaracje czci publicznej implementation uses BarFly; // tu deklaracje i definicje czci prywatnej initialization // cz inicjujca finalization // cz koczca end.

Cykliczna zaleno midzy moduami


Jeeli nazwa A wystpuje na licie uses w czci publicznej moduu B, to mwimy, e modu B jest bezporednio zaleny od moduu A. Jeeli w publicznej czci moduu A na licie uses wystpuje nazwa C, to modu B jest zaleny porednio od moduu C poprzez modu A. Oglnie rzecz biorc, opisana zaleno pomidzy moduami moe prowadzi przez wiksz liczb moduw porednich. Zaleno taka ma bardzo wyrany aspekt praktyczny: jeeli modu X zaleny jest od moduu Y, to skompilowanie (przynajmniej) czci publicznej moduu Y jest konieczne do tego, by moga rozpocz si kompilacja moduu X. Jeeli wic zdarzy si tak, i przynajmniej dwa moduy (nazwijmy je P i Q) bd od siebie nawzajem zalene, nie bdzie moliwe skompilowanie ani moduu P, ani Q; w efekcie nie bdzie moliwe skompilowanie projektu. Takie wzajemne uzalenienie publicznych czci moduw nazywamy w Pascalu odwoaniem cyklicznym (circular unit reference); powoduje ono oczywicie bd kompilacji. Naley zaznaczy, i listy uses w czci prywatnej moduw nie powoduj opisanego uzalenienia. Wynika std prosty wniosek, i pierwszym krokiem w celu pozbycia si odwoania cyklicznego powinna by prba przeniesienia kolidujcych nazw na listach uses z czci publicznej do czci prywatnej moduw (chodzi oczywicie o te nazwy, ktre w czci publicznej nie s potrzebne i znalazy si tam np. przez niedopatrzenie). Jeeli nie rozwie to problemu, naley stworzy odrbny modu i przenie do jego czci publicznej te elementy, ktre stanowi przyczyn wystpienia odwoania cyklicznego.

Notatka

Z matematycznego punktu widzenia relacja zalenoci midzy moduami (stanowica przechodnie domknicie relacji zalenoci bezporedniej) powinna by relacj antysymetryczn, w przeciwnym razie mamy do czynienia z odwoaniem cyklicznym.

Ponisze trzy moduy uwikane s w odwoanie cykliczne:

105

UNIT A; Interface uses B; implementation .... end.

UNIT B; interface uses C; implementation .... end.

UNIT C; interface uses A; implementation .... end.

Jeeli jednak w module B przeniesiemy dyrektyw uses do czci prywatnej


UNIT B; interface implementation uses C; .... end.

pozbdziemy si odwoania cyklicznego (jest to oczywicie moliwe tylko wtedy, gdy w czci publicznej moduu B nie ma elementw odwoujcych si do moduu C). Pewnego wyjanienia wymaga relacja zwana zalenoci pseudocykliczn. Wystpuje wtedy (przy zaoeniu, e dwa moduy nazwalimy A i B), gdy modu A odwouje si do moduu B w czci publicznej, za modu B odwouje si do moduu A w czci prywatnej jak w poniszym przykadzie:
UNIT A; Interface uses B; Implementation End. UNIT B; Interface Implementation uses A; End. { program gwny } USES A,B; BEGIN .... END.

106

Poczwszy od wersji 7.0 Turbo Pascala relacja taka nie ma dla kompilatora adnego szczeglnego znaczenia i nie powoduje nigdy bdu kompilacji.

Pakiety
W Delphi 3 pojawia si moliwo podziau kodu wynikowego aplikacji na kilka oddzielnych fragmentw, ktre mog by wspdzielone przez kilka aplikacji. Takie fragmenty, majce posta bibliotek DLL i stanowice kolekcje skompilowanych moduw (units), nazywane s w Delphi pakietami (packages); poszczeglne pakiety przyczane s do aplikacji w czasie jej wykonania, nie w czasie kompilacji i konsolidacji. Przeniesienie czci kodu aplikacji do pakietw powoduje odchudzenie gwnego moduu .EXE lub .DLL, jednak z istotn oszczdnoci kodu mamy do czynienia w sytuacji, gdy pojedynczy pakiet wykorzystywany jest rwnoczenie przez kilka aplikacji. Istniej cztery typy pakietw: Pakiety wykonywalne (runtime packages) poprawniej byoby nazwa je pakietami etapu wykonania to pakiety wykorzystywane bezporednio przez dziaajc aplikacj. Ich obecno jest konieczna do uruchomienia aplikacji; przykadem pakietw tej kategorii jest firmowy pakiet Delphi 6 VCL60.BPL. Pakiety rodowiskowe (design-time packages) zwane rwnie pakietami etapu projektowania zawieraj elementy niezbdne do przeprowadzenia procesu projektowania aplikacji: komponenty, edytory komponentw i waciwoci, programy ekspertowe itp.; przykadami pakietw tej kategorii s w Delphi 6 pliki DCL*.BPL. Pakiety rodowiskowe wykorzystywane s wycznie przez rodowisko IDE; instaluje si je za pomoc opcji Component|Install Package menu gwnego. Zajmiemy si nimi dokadniej w rozdziale 11. Pakiety uniwersalne cz w sobie funkcjonalno pakietw wykonywalnych i rodowiskowych. Dziki zintegrowaniu caej funkcjonalnoci w pojedynczym pliku, pakiet tej kategorii jest nieco wygodniejszy w dystrybucji, jednak podczas wykorzystywania go przez dziaajc aplikacj wynikow spora cz kodu ta dotyczca rodowiska IDE nie jest wykorzystywana i bezproduktywnie powiksza rozmiar pliku. Pakiety nie mieszczce si w adnej z powyszych kategorii spotykane s bardzo rzadko i peni zazwyczaj rol pomocnicz w stosunku do innych pakietw, nie s za bezporednio wykorzystywane ani przez uruchomione aplikacje, ani przez rodowisko IDE.

Wykorzystywanie pakietw
Wszelkie czynnoci niezbdne do wygenerowania kodu aplikacji z podziaem na pakiety sprowadzaj si do zaznaczenia pola Build with Runtime Packages na karcie Packages opcji projektu i skompilowania projektu w trybie Build. Naley przy tym pamita, i wszystkie wygenerowane pakiety stanowi integraln cz aplikacji i niezbdne s do jej uruchomienia.

Skadnia pakietu
Kady pakiet reprezentowany jest przez plik rdowy z rozszerzeniem .DPK. Taki plik tworzony jest przez edytor pakietw (Package Editor) uruchamiany za pomoc polecenia File|New|Package menu gwnego. Zawarto pliku .DPK posiada nastpujcy format:
package nazwa_pakietu requires Pakiet1, Pakiet2, ...; contains Unit1 in 'Unit1.pas', Unit2 in 'Unit2.pas', ...; end.

107

Lista requires zawiera nazwy pakietw niezbdnych do funkcjonowania danego pakietu; s to najczciej pakiety zawierajce moduy (units) wykorzystywane przez moduy wymienione na licie contains. Lista contains zawiera nazwy moduw wczanych do pakietu; adna z tych nazw nie moe wystpi na licie contains ktregokolwiek pakietu wymienionego na licie requires; inaczej mwic kady modu (unit) musi by adowany przez dany pakiet jednokrotnie. Kady z moduw wykorzystywanych przez moduy wymienione na licie contains doczany jest do pakietu automatycznie (chyba e znalaz si ju w pakiecie z tytuu przynalenoci do listy requires).

Programowanie zorientowane obiektowo


Programowanie zorientowane obiektowo zdobyo w cigu ostatnich kilkunastu lat rang niemal kultow. Nic w tym dziwnego po jzykach algorytmicznych, a nastpnie programowaniu strukturalnym jest to nastpna idea, ktrej skutki w rewolucjonizowaniu procesu projektowania oraz programowania s widoczne a nazbyt dobrze. Doceniajc znaczenie OOP, pozostawimy jednak na boku wszelk egzaltacj niech przemwi konkrety, jak przystao na podrcznik dla zaawansowanych programistw.

Idea, na ktrej opiera si filozofia obiektw, jest jak wszystkie genialne pomysy bardzo klarowna. Zrywa mianowicie z dotychczasow ide aplikacji rozumianej jako wsppraca dwch wiatw danych i operujcego na nich kodu. W duych aplikacjach kady z owych wiatw przejawia nierzadko tendencje rozrostu do rozmiarw (niemale) wszechwiata, komplikujc coraz bardziej i tak nieatw ju prac programistw i projektantw. Caa sprawa znacznie by si uprocia, gdyby wspprac t zorganizowa na zasadzie istnienia swoistych mikrowiatw, z ktrych kady stanowi czstk logicznie powizanych danych i kodu. Owe mikrowiaty, nazwane (moe troch banalnie) obiektami, stanowi podstaw nowego paradygmatu programowania, zwanego wanie programowaniem zorientowanym obiektowo (OOP Object Oriented Programming). Mimo i sama idea programowania obiektowego niekoniecznie prowadzi do atwego programowania, to zwykle efektem jej zastosowania jest klarowny kod, ktry atwo jest utrzymywa i w ktrym atwo jest znajdowa ewentualne bdy.

Wspczesne jzyki programowania implementuj co najmniej trzy nastpujce koncepcje OOP:

Enkapsulacja zwana te hermetyzacj. Polega na cisym powizaniu kodu oraz danych sucych temu samemu celowi, poprzez zamknicie ich w ramach jednego bytu typu obiektowego, z jednoczesnym ukryciem szczegw implementacyjnych. Umoliwiajc izolowanie poszczeglnych fragmentw kodu, przyczynia si do modularyzacji programu. Dziedziczenie. W zoonych aplikacjach nie sposb nie zauway podobiestw midzy poszczeglnymi fragmentami danych i kodu. Std pomys, by przy definiowaniu nowych typw obiektowych nie zaczyna pracy ab initio, lecz wykorzysta cechy obiektw ju istniejcych. Rysunek 2.4 przedstawia zastosowanie filozofii dziedziczenia cech wrd rnych rodzajw owocw; im dalej od korzenia, tym wicej konkretnych cech rozwaanego obiektu. Polimorfizm. Tumaczony dosownie oznacza wielopostaciowo i okrela sytuacj, w ktrej jednakowo identyfikowane cechy mog manifestowa si w rny sposb, w zalenoci od konkretnego egzemplarza obiektu. Na gruncie Object Pascala oznacza to rne funkcjonowanie identycznie nazwanych metod w odniesieniu do rnych klas obiektw.

108

Rysunek 2.4. Ilustracja koncepcji dziedziczenia


Notatka

Zwr uwag na wany fakt, i na rysunku 2.4 dla kadego obiektu istnieje dokadnie jedna cieka czca go z korzeniem, a wic kady obiekt dziedziczy swe cechy od dokadnie jednego obiektu macierzystego. Taki wanie charakter ma dziedziczenie cech obiektowych w Object Pascalu. Jzyk C++ jest pod tym wzgldem znacznie bardziej rozbudowany; oferuje dziedziczenie od wielu obiektw jednoczenie (multiple inheritance) co mona by uwidoczni na wspomnianym rysunku, gdyby udao nam si wyhodowa krzywk np. renety i arbuza.

Brak wielokrotnego dziedziczenia w Object Pascalu postrzegany jest rozmaicie przez niektrych jako dotkliwe ograniczenie, przez innych natomiast jako brak jeszcze jednej okazji do popeniania bdw. Niezalenie od subiektywnego spojrzenia na brak wielokrotnego dziedziczenia, warto zastanowi si nad rozwizaniami stanowicymi jego odpowiednik a te s w Object Pascalu dwojakie. Jedno z nich, wykorzystane m.in. przy budowie biblioteki VCL, polega na zawieraniu si w danym obiekcie obiektw klasy macierzystej obiekt reprezentujcy krzywk renety i arbuza mgby wywodzi si z klasy arbuz i zawiera w sobie (jako jedno z pl) obiekt klasy reneta. Druga koncepcja polega na implementowaniu przez pojedynczy obiekt elementw zachowa kilku specyficznych klas, zwanych interfejsami zajmiemy si nimi w dalszej czci rozdziau.

Z pojciem obiektu, jako typu w jzyku programowania, wi si ponadto trzy nastpujce terminy: Pole (field) zwane take zmienn egzemplarza (instance variable) stanowi odmian zmiennej funkcjonujcej w kontekcie obiektu. W jzyku C++ pola nazywane s elementami danych (data members). Metoda (method) jest procedur lub funkcj dziaajc na rzecz pl obiektu. W C++ metody nazywane s funkcjami skadowymi (member functions). Waciwo (property) stanowi poczenie koncepcji pola i metody i realizuje mechanizm dostpu do pl i metod obiektu. Operowanie waciwociami obiektu (w przeciwiestwie do bezporedniego operowania jego polami) uniezalenia sposb korzystania z niego od jego szczegw implementacyjnych.
Wskazwka

Bezporednie operowanie na polach obiektu, cho moliwe, jest zasadniczo sprzeczne z filozofi programowania obiektowego, koncepcyjnie stanowi bowiem rodzaj ingerencji w szczegy implementacyjne obiektu. Powinnimy go unika i posugiwa si waciwociami.

rodowisko bazujce na obiektach kontra rodowisko zorientowane obiektowo


Rozrnienie dwch wymienionych w tytule rodowisk (ang. object-based i object-oriented) bierze si std, e istniej rodowiska oferujce gotowe obiekty i jednoczenie skrywajce przed programist ca filozofi OOP; sztandarowym przykadem s starsze wersje Visual Basica z kontrolkami VBX i OCX. Trudno mwi o

109

jakiejkolwiek obiektowej orientacji programowania, skoro programista jakby na przekr posiada moliwo programowania i definiowania wasnych typw jedynie w klasycznym stylu. Dlatego te mona bez przesady stwierdzi, e rodowisko to jedynie bazuje na gotowych obiektach. Delphi nie stwarza natomiast adnych ogranicze w tym wzgldzie, umoliwiajc tworzenie nowych obiektw zarwno od zera, jak i drog dziedziczenia z istniejcych obiektw wizualnych, niewizualnych, czy nawet kompletnych formularzy.

Wykorzystanie obiektw w Delphi


Obiekty, zwane take w Delphi klasami14, s (jak wspominalimy wczeniej) jednostkami zawierajcymi dane i powizany z nimi kod. Jako rodowisko w peni zorientowane obiektowo, Delphi udostpnia wszelkie korzyci pynce z trzech zasadniczych filarw OOP enkapsulacji, dziedziczenia i polimorfizmu.

Deklarowanie obiektw i kreowanie zmiennych obiektowych


Typ obiektowy w Delphi deklarujemy za pomoc sowa kluczowego class:
Type TFooObject = class;

Posiadajc ju zdefiniowany typ obiektowy, moemy zdefiniowa jego egzemplarz (instance):


Var FooObject : TFooObject;

To jednak dopiero pocztek; w przeciwiestwie do Turbo Pascala w wersji 5.5 7.0, nie ma w Delphi moliwoci definiowania statycznych egzemplarzy obiektw, natomiast powysza deklaracja okrela zmienn przechowujc wskanik do dynamicznie tworzonego egzemplarza klasy TFooObject. Do dynamicznego tworzenia egzemplarzy klas su wyrnione metody zwane konstruktorami. W Object Pascalu kada klasa posiada przynajmniej jeden konstruktor o nazwie Create(). Jego zestaw parametrw (lub ich brak) zaley od konkretnej klasy; dla uproszczenia w dalszej czci rozdziau ograniczymy si do jego wersji bezparametrowej. W przeciwiestwie do C++, konstruktory w Object Pascalu musz by wywoywane w sposb jawny. Instrukcja powodujca utworzenie egzemplarza obiektu, zgodnie ze zdefiniowanym wczeniej typem, ma nastpujc posta:
FooObject := TFooObject.Create;

Zwrmy przy tym uwag na sposb wywoania konstruktora: jest on wywoywany na rzecz okrelonego typu (klasy), nie za konkretnego egzemplarza (obiektu). Jest to zrozumiae wobec faktu, i przed wywoaniem konstruktora nie istnieje jeszcze egzemplarz obiektu.
Inicjalizacja obiektu wykonywana przez konstruktor wie si midzy innymi z wyzerowaniem caego przydzielonego dla obiektu obszaru pamici. Powoduje to, e wszystkie liczby (bdce rzecz jasna polami obiektu) staj si rwne zero, acuchy staj si pustymi napisami (''), a wskaniki pustymi wskazaniami (NIL).

Destrukcja obiektu
Po wykorzystaniu obiektu naley zwolni zajt przez niego pami. Wczeniej musz zosta wykonane charakterystyczne dla danego typu czynnoci koczce. Zadanie to wykonuje wyrniona metoda zwana destruktorem. Kada klasa w Object Pascalu zawiera destruktor zwany Destroy(). Teoretycznie, moliwe jest jego aktywowanie dla konkretnego egzemplarza obiektu, ktry mamy zamiar unicestwi:
FooObject.Destroy;

Zamiast tego zaleca si jednak wywoanie metody Free():

14

Zgodnie jednak z przyjt konwencj, pod pojciem klasy kryje si w Delphi konkretny typ danych, natomiast okrelenie obiekt uywane jest w odniesieniu do konkretnego egzemplarza tego typu (przyp. tum.).

110

FooObject.Free;

Metoda ta sprawdza wpierw, czy zmienna obiektowa (FooObject) nie zawiera pustego wskazania (NIL) jeeli nie, nastpuje wywoanie metody Destroy() dla wskazywanego obiektu.

Ostrzeenie

W C++ destruktor obiektu zadeklarowanego statycznie wywoywany jest automatycznie w momencie, gdy sterowanie opuszcza zasig deklaracji tego obiektu; obiekty tworzone dynamicznie musz by jednak zwalniane w sposb jawny, za pomoc sowa kluczowego delete. W Delphi nie ma obiektw statycznych, musimy wic jawnie zwalnia kady egzemplarz obiektu, majc na uwadze dwa (z szeregu innych) uwarunkowania. Po pierwsze, zwalniany obiekt dokonuje jednoczesnego zwolnienia wszystkich innych obiektw, dla ktrych jest wacicielem; po drugie istniej wspdzielone przez kilka aplikacji obiekty, ktrych wykorzystanie opiera si na tzw. liczniku odwoa (reference counter), i ktre s zwalniane dopiero wwczas, gdy licznik ten osignie warto 0 (czyli ostatnia z aplikacji zakoczy swe operacje na obiekcie). Przykadami takich wspdzielonych obiektw s obiekty klas TInterfacedObject i TComObject.

Nasuwa si pytanie, skd bierze si obecno konstruktora Create(), destruktora Destroy() i metody Free() w kadym typie obiektowym? Odpowied na to pytanie wskazuje jeszcze jedn rnic midzy Turbo Pascalem a Delphi. W Delphi kady obiekt bez wskazanej jawnie klasy bazowej jest traktowany jako typ pochodny klasy TObject, tak wic deklaracja
Type TFoo = class;

rwnowana jest deklaracji


Type TFoo = class (TObject);

Wymienione metody Create(), Destroy() i Free() s czci klasy TObject powrcimy za chwil do tej kwestii.

Metody
Metody s tym aspektem typu obiektowego, ktry pobudza obiekt do ycia i decyduje o jego zachowaniu (trudno to powiedzie o polach obiektu, ktre s co najwyej poywk dla metod). Przykadami metod s poznane przed chwil konstruktory i destruktory. Deklarowanie wasnej metody przebiega dwuetapowo. Etap pierwszy to umieszczenie nagwka metody wewntrz deklaracji klasy, na przykad:
Type TDyskoteka = class; Taniec : Boolean; Procedure ZatanczSambe; End;

Konkretyzacja treci procedury odbywa si w drugim etapie, w czci implementacyjnej moduu zawierajcego deklaracj klasy:
Procedure TDyskoteka.ZatanczSambe; begin Taniec := TRUE; end;

Zwr uwag na to, i waciwa nazwa procedury poprzedzona jest nazw klasy, dla ktrej ta procedura jest metod. Podobn (kwalifikowan) posta maj odwoania do metody nazwa metody poprzedzona jest okreleniem obiektu, na rzecz ktrego metoda ta jest wywoywana:

111

Var Maxim : TDyskoteka; ........ Maxim.ZatanczSambe;

Podobnie jak w przypadku rekordw, odwoania kwalifikowane mona zastpi instrukcj with:
with Maxim do ZatanczSambe;

W treci metod danej klasy odwoania do pl jej obiektw nie maj postaci kwalifikowanej, gdy tre metody jest dla tych pl zakresem ich widocznoci (vide pole Taniec w metodzie TDyskoteka.ZatanczSambe).

Typy metod
Metoda klasy w Object Pascalu moe by metod statyczn, wirtualn, dynamiczn i komunikacyjn. Oto przykad deklaracji metod kadego z wymienionych rodzajw
TFoo = class Procedure Statyczna; Procedure Wirtualna;virtual; Procedure Dynamiczna;dynamic; Procedure Komunikacyjna ( var M: TMessage ); message wm_SomeMessage; End;

Metody statyczne Metoda, ktrej deklaracja nie jest opatrzona adnymi dodatkowymi klauzulami, jest metod statyczn. Funkcjonuje ona podobnie do zwykej procedury lub funkcji, jej adres znany jest ju w czasie kompilacji, a jej wywoanie przebiega bardzo efektywnie. Metody statyczne nie udostpniaj jednak adnych korzyci pyncych z polimorfizmu.

Wskazwka

W przeciwiestwie do C++, klasy Object Pascala nie mog posiada statycznych pl. W Object Pascalu pole jest zawsze czci egzemplarza klasy (czyli obiektu) zmiana zawartoci pola w jednym obiekcie nie ma wpywu na jego zawarto w innym; pole statyczne jest natomiast czci klasy, wspln dla wszystkich jej obiektw, ma wic dla nich charakter globalny. Symulacj (do pewnego stopnia) globalnych pl klasy moe by w Object Pascalu wykorzystanie zmiennych globalnych moduu (w jego czci prywatnej) w treci 15 metod zmienne takie zachowuj si tak, jak zachowywayby si pola statyczne (gdyby istniay) .

Metody wirtualne Dziedziczenie wie si z moliwoci przedefiniowywania (overriding) metod obiektu. Oznacza to, e metoda o danej nazwie moe mie zupenie rne dziaanie dla rnych klas (macierzystej i pochodnej). Innymi sowy, kompilator, znajc nazw metody, nie potrafi okreli jej konkretnego adresu, gdy nie zna konkretnego obiektu (a waciwie jego typu), na rzecz ktrego jest ona aktywowana. Zjawisko rnego zachowania metod o tej samej nazwie w odniesieniu do rnych typw w caym poddrzewie typw pochodnych danej klasy nosi nazw polimorfizmu metoda o danej nazwie ma jak gdyby wiele twarzy. Zachowuje si rnie, w zalenoci od typu obiektu, na rzecz ktrego zostanie wywoana. Dla realizacji polimorfizmu Object Pascal utrzymuje struktury zwane tablicami VMT (Virtual Method Tables), po jednej dla kadej klasy. Kada tablica VMT zawiera adresy wszystkich metod wirtualnych swej klasy (take tych, ktre dziedziczone s z klasy bazowej bez zmian), a wic metody wirtualne przyczyniaj si w pewnym stopniu do obcienia pamici. Obcienie to mona do pewnego stopnia zmniejszy, za cen nieznacznego pogorszenia efektywnoci, uywajc metod dynamicznych.

15

Zmienne globalne moduu w poczeniu z mechanizmem waciwoci pozwalaj na symulacj statycznych pl nie tylko w treci metod niebawem powrcimy do tej kwestii (przyp. tum.).

112

Metody dynamiczne Metody dynamiczne wykorzystywane s dokadnie tak samo, jak metody wirtualne, jednak ich realizacja ukierunkowana zostaa przede wszystkim na efektywne wykorzystanie pamici, kosztem efektywnoci ich wywoywania. Dla kadej klasy deklarujcej chocia jedn metod dynamiczn kompilator utrzymuje struktur zwan tablic DMT (Dynamic Method Table). Tablica DMT zawiera adresy tylko tych metod, ktre s przedefiniowane w stosunku do klasy macierzystej; klasy nie deklarujce wasnych metod dynamicznych nie posiadaj w ogle tablicy DMT. Metody dynamiczne nie obciaj wic pamici brzemieniem dziedziczonym z klas macierzystych, ich wywoywanie jest jednak mniej efektywne (ni w przypadku metod wirtualnych), poniewa bardziej zoony jest algorytm poszukiwania adresu konkretnej metody.

Metody komunikacyjne Metody tej kategorii stanowi reminiscencj klasycznego programowania w Windows i su do obsugi wybranych komunikatw identyfikator komunikatu zawarty jest w klauzuli message w deklaracji metody. Metody komunikacyjne nie s raczej przeznaczone do bezporedniego wywoywania nale do tzw. funkcji zwrotnych (callback), wywoywanych automatycznie przez system operacyjny. Szczegami obsugi komunikatw systemowych zajmiemy si w rozdziale 3.

Przedefiniowywanie metod
Przedefiniowywanie metod jest praktyczn realizacj polimorfizmu. Na gruncie danej klasy i wszystkich jej klas pochodnych metoda o danej nazwie moe wykazywa rne zachowanie w zalenoci od konkretnej klasy (lub klasy konkretnego obiektu). Przedefiniowywane mog by tylko metody wirtualne i dynamiczne; fakt przedefiniowania metody zaznacza si klauzul override w jej deklaracji. W poniszym przykadzie klasa TFooChild przedefiniowuje metody Wirtualna i Dynamiczna, odziedziczone z klasy macierzystej TFoo:
TFooChild = class(TFoo) Procedure Wirtualna;override; Procedure Dynamiczna;override; End;

Uycie klauzuli override powoduje zmian odpowiedniego wskanika w tablicy VMT. Z przedefiniowywaniem metod w Delphi wie si dodatkowo istotna rnica w stosunku do Turbo Pascala: uycie w miejsce klauzuli override klauzuli virtual albo dynamic nie oznacza przedefiniowania (jak w Turbo Pascalu), lecz stanowi zapocztkowanie nowego acucha powizanych metod o (przypadkowo) identycznej nazwie. W poniszym przykadzie
TFooBastard = class(TFoo) Procedure Wirtualna;virtual; Procedure Dynamiczna;dynamic; End;

metody Wirtualna i Dynamiczna nie maj nic wsplnego z identycznie nazwanymi metodami klasy TFoo. Gdy kompilator napotka tak sytuacj, wygeneruje ostrzeenie, i metoda klasy pochodnej zasania identycznie nazwan metod klasy macierzystej.

Reintrodukcja metody
Opisane przed chwil zasonicie metody klasy bazowej i zapocztkowanie nowego acucha metod o identycznej nazwie moe by niekiedy dziaaniem cakowicie zamierzonym. Dla podkrelenia, i nie mamy do czynienia z pomyk i jednoczenie dla wyeliminowania ostrzee ze strony kompilatora moemy w sposb jawny zasygnalizowa ten fakt, opatrujc deklaracj metody klauzul reintroduce, jak w poniszym przykadzie:
TFoo = class Procedure Statyczna; Procedure Wirtualna;virtual; Procedure Dynamiczna;dynamic; Procedure Komunikacyjna ( var M: TMessage ); message wm_SomeMessage; End; TFooOrphan = class(TFoo) Procedure Wirtualna;reintroduce; Procedure Dynamiczna;reintroduce; End;

113

Klauzula reintroduce nie wyklucza oczywicie wystpienia innych klauzul (virtual, dynamic i message) w deklaracji metody.

Przecianie metod
Podobnie jak zwyke procedury i funkcje, rwnie metody mog by przeciane umoliwia to opatrzenie wspln nazw wielu aspektw konkretnej metody (w konkretnej klasie) rnicych si zestawem parametrw. Oto przykad:
Type TSomeClass = class procedure Amethod(I: Integer);overload; procedure Amethod(S: String);overload; procedure Amethod(D: Double);overload; end;

Poniszy przykad (zaczerpnity z systemu pomocy Delphi) ilustruje ciekawy przypadek, gdy rne aspekty danej metody nale do rnych klas:
type T1 = class(TObject) procedure Test(I: Integer); overload; virtual; end; T2 = class(T1) procedure Test(S: string); reintroduce; overload; end; ... SomeObject := T2.Create; SomeObject.Test('Hello!'); // wywouje T2.Test() SomeObject.Test(7); // wywouje T1.Test()

Identyfikator Self
Aby w treci metody moliwe byy kwalifikowane odwoania do pl, metod i waciwoci obiektu, konieczne jest uycie identyfikatora tego obiektu takim uniwersalnym identyfikatorem jest Self reprezentujcy w treci konkretnej metody obiekt, na rzecz ktrego wywoana zostaa ta metoda. Jego zawarto, stanowica wskanik do wspomnianego obiektu, przekazywana jest niejawnie jako dodatkowy parametr wywoania wszystkich metod.

Waciwoci
Natura waciwoci (property) obiektu jest nieco bardziej abstrakcyjna ni natura pola czy metody. Koncepcyjnie waciwo zbliona jest do pola, gdy podobnie jak pole przechowuje (modyfikowaln) warto okrelonego typu; bardziej skomplikowany jest natomiast sposb nadawania i odczytywania tej wartoci. Spjrzmy wpierw na deklaracj przykadowej waciwoci:
TMyObject = Class private SomeValue : Integer; Procedure SetSomeValue(Avalue: Integer); public Property Value: Integer read SomeValue write SetSomeValue; End; procedure TMyObject.SetSomeValue(AValue: Integer); begin if SomeValue <> AValue Then SomeValue := AValue; end;

114

Klasa TMyObject definiuje pole SomeValue, metod SetSomeValue i waciwo Value; ta ostatnia powizana jest z pozostaymi elementami za pomoc klauzul read i write. Klauzula read okrela sposb odczytywania waciwoci: poniewa specyfikuje ona nazw pola, aktualna warto tego pola przyjmowana jest jako warto waciwoci. Klauzula write specyfikuje natomiast nazw metody a to oznacza, e przypisanie waciwoci nowej wartoci zostanie fizycznie zrealizowane jako wywoanie teje metody z przypisywan wartoci jako parametrem. Konkretnie: dla obiektu MyObj:TMyObject instrukcja
WhatValue := MyObj.Value;

rwnowana jest instrukcji


WhatValue := MyObj.SomeValue;

Z kolei instrukcja
MyObj.Value := NewValue;

oznacza to samo, co
MyObj.SetSomeValue(NewValue);

W kadej z klauzul read i write moe wystpi bd nazwa pola, bd nazwa metody; adna z klauzul read i write nie jest obowizkowa na przykad opuszczenie klauzuli write powoduje, i waciwoci nie mona przypisywa explicite nowej wartoci. Metody specyfikowane w ramach klauzul read i write umoliwiaj pen kontrol nad odczytywaniem i modyfikacj waciwoci; stanowi one jedyny sposb dostpu do waciwoci i z tego wzgldu nazywane s jej metodami dostpowymi (property access methods). Waciwoci komponentw VCL stanowi podstawowy rodek ich komunikacji z aplikacjami; ich waciwoci opublikowane (published) dostpne s za porednictwem inspektora obiektw.

Wskazwka

A oto zapowiadana symulacja statycznych pl klasy za pomoc waciwoci waciwo StaticValue zachowuje si (prawie) tak, jak statyczne pole w C++: var GlobalField: integer; // zmienna globalna moduu Type MyStaticClass = class private function GetGlobalField: integer; procedure SetGlobalField(const Value: integer); published property StaticValue: integer read GetGlobalField write SetGlobalField; private end;

function MyStaticClass.GetGlobalField: integer; begin Result := GlobalField; end; procedure MyStaticClass.SetGlobalField(const Value: integer); begin

115

GlobalField := Value; end;

Widoczno elementw obiektu


Poszczeglne elementy obiektu mog by w rny sposb udostpniane innym elementom aplikacji. Delphi definiuje pi kategorii dostpnoci, okrelanych za pomoc kwalifikatorw protected, private, public, published oraz automated. Oto przykad:
type TSomeObject = class private APrivateVariable: Integer; AnotherPrivateVariable: Boolean; protected Procedure AProtectedProcedure; Function ProtectMe: Byte; public constructor APublicConstructor; destructor APublicKiller; published property Aproperty read APrivateVariable write APrivateVariable; End;

Znaczenie kadej z podanych kategorii jest nastpujce:


private elementy opatrzone t klauzul, zwane elementami prywatnymi, widoczne s jedynie wewntrz

moduu, w ktrym zdefiniowano dany obiekt. Umoliwia to ukrycie pewnych szczegw implementacji metod obiektu oraz ukrycie tych pl, ktre peni jedynie rol pomocnicz i nie powinny by dostpne dla uytkownika.
protected ten kwalifikator powoduje udostpnienie wskazanych elementw klasy jedynie metodom i

waciwociom jej klas pochodnych. Uniemoliwia to nieskrpowane wykorzystywanie pewnych elementw klasy do definiowania klas pochodnych i chroni je jednoczenie przed uyciem do innych celw dlatego elementy tej kategorii nazywane s elementami chronionymi.
public kwalifikuje elementy klasy jako w peni dostpne dla pozostaych elementw aplikacji (czyli

publiczne). Konstruktory i destruktory zawsze s metodami publicznymi.


published konsekwencj uycia tego kwalifikatora jest opublikowanie wybranych elementw klasy;

oprcz tego, e staj si elementami publicznymi, oznacza to take ich ewidencjonowanie w ramach mechanizmu RTTI (Runtime Type Information) udostpniajcego szczegy definicji klasy w czasie wykonywania programu. Z mechanizmu RTTI korzysta take inspektor obiektw, tworzc za jego pomoc listy waciwoci i zdarze poszczeglnych komponentw.
automated kwalifikator ten jest pozostaoci po Delphi 2 i zachowany zosta jedynie ze wzgldw

kompatybilnoci.

Wskazwka

Ewentualne pocztkowe elementy deklaracji klasy nie opatrzone adnym kwalifikatorem widocznoci s, w zalenoci od ustawienia przecznika kompilacji $M, opublikowane {$M+} bd publiczne {$M}(przyp. tum.).

Moliwa jest zmiana kwalifikatora widocznoci dziedziczonego elementu w klasie pochodnej, ale tylko w kierunku rosncym na przykad element chroniony (protected) moe zosta uczyniony elementem publicznym (public), ale nie prywatnym (private) (przyp. tum.).

116

Klasy zaprzyjanione
Zaprzyjanienie klas w C++ oznacza dostp do prywatnych elementw definicji danej klasy z poziomu innych klas (zwanych klasami zaprzyjanionymi friend classes). Koncepcja ta, mimo i nie nazwana w sposb wyrany, jest jednak faktycznie obecna w Object Pascalu, chocia w sposb zdecydowanie mniej selektywny wszystkie klasy definiowane w tym samym module s dla siebie nawzajem klasami zaprzyjanionymi.

Wewntrz obiektw
Uywanie zmiennych obiektowych w Object Pascalu wie si z pewn niekonsekwencj, ktra dla niewprawnego uytkownika moe by nieco mylca. Ot zmienne obiektowe, mimo i s uywane w sposb charakterystyczny dla zmiennych statycznych, s w istocie 32-bitowymi wskanikami do obiektw; te ostatnie alokowane s na stercie i wspomniane wskaniki stanowi jedyny sposb dostpu do nich nie ma w Object Pascalu moliwoci ich bezporedniego reprezentowania. Zgodnie z oglnymi reguami Pascala odwoanie si do wskanika wymaga uycia operatora dereferencji (^) i wydawaoby si, i zamiast
Button1.Caption

powinno si pisa
Button1^.Caption

T drug posta kompilator traktuje jednak jako bdn, zapewniajc waciw interpretacj pierwszej uycie zmiennej obiektowej poczone jest z niejawn dereferencj zawartego w niej wskanika.

Notatka

Opisana niekonsekwencja wystpuje rwnie w odniesieniu do zmiennych rekordowych. Zgodnie z poniszymi deklaracjami type TMyRecord = record A,B : integer; end; var P: ^TMyRecord; instrukcja

P.A := 1;

powinna by uznana za bdn z powodu braku operatora dereferencji i tak te jest w Turbo Pascalu. Object Pascal jednak, jakby odgadujc intencje programisty, traktuje j jako poprawn, zakadajc niejawn dereferencj. W przeciwiestwie do zmiennych obiektowych, jawne uycie operatora ^

P^.A := 1;

jest dla zmiennych rekordowych w dalszym cigu poprawne. (przyp. tum.).

117

TObject protoplasta wszystkich klas


Kada klasa w Object Pascalu wywodzi si (porednio bd bezporednio) z klasy TObject nawet wwczas, gdy nie zaznaczono tego w sposb jawny. Oznacza to, e kady obiekt w Delphi posiada na dzie dobry cakiem niemay zasb funkcjonalnoci. Moliwe jest midzy innymi uzyskanie z egzemplarza obiektu nazwy jego klasy i jej elementw oraz pewnych informacji zwizanych z dziedziczeniem. Wszystko to stanie si zrozumiae, gdy przyjrzymy si deklaracji klasy TObject:
TObject = class constructor Create; procedure Free; class function InitInstance(Instance: Pointer): TObject; procedure CleanupInstance; function ClassType: TClass; class function ClassName: ShortString; class function ClassNameIs(const Name: string): Boolean; class function ClassParent: TClass; class function ClassInfo: Pointer; class function InstanceSize: Longint; class function InheritsFrom(AClass: TClass): Boolean; class function MethodAddress(const Name: ShortString): Pointer; class function MethodName(Address: Pointer): ShortString; function FieldAddress(const Name: ShortString): Pointer; function GetInterface(const IID: TGUID; out Obj): Boolean; class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry; class function GetInterfaceTable: PInterfaceTable; function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult;virtual; procedure AfterConstruction; virtual; procedure BeforeDestruction; virtual; procedure Dispatch(var Message); virtual; procedure DefaultHandler(var Message); virtual; class function NewInstance: TObject; virtual; procedure FreeInstance; virtual; destructor Destroy; virtual; end;

Widzimy tu starych znajomych konstruktor Create, destruktor Destroy i metod Free. Znaczenie kadej z deklarowanych metod opisane jest w systemie pomocy Delphi. Dociekliwym czytelnikom proponujemy ponadto, by przyjrzeli si kodowi rdowemu definicji tych metod w module SYSTEM.PAS. Pewnego wyjanienia wymagaj metody opatrzone dyrektyw class. Metody takie, notabene analogiczne do statycznych metod C++, funkcjonuj w kontekcie klasy jako caoci, bez rnicy dla poszczeglnych obiektw i na przykad metoda ClassName, aktywowana na rzecz konkretnego obiektu, zwraca nazw jego klasy, pozostajc bez adnego zwizku z jego zawartoci16.

Interfejsy
Interfejsy (interfaces) stanowi specjaln kategori klas, zwizan z wykorzystaniem technologii obiektw komponentw (COM Component Object Model); jako odrbny element syntaktyczny zostay wydzielone dopiero w Delphi 3 w Delphi 2 funkcjonoway na rwni z innymi klasami (jako klasy pochodne w stosunku do klasy IUnknown). Generalnie, interfejs jest zbiorem funkcji i procedur umoliwiajcych interakcj z obiektem; zbir ten okrela pewien aspekt zachowania si obiektu, lecz jedynie w ujciu intencjonalnym interfejs zawiera bowiem jedynie deklaracje wspomnianych procedur i funkcji; nadanie im treci, czyli powizanie ich z konkretnymi dziaaniami, jest kwesti ich implementacji jako metod w konkretnym obiekcie. Wewntrzne szczegy funkcjonowania interfejsw opieraj si na koncepcji tzw. klasy czysto wirtualnej (pure virtual class) jest ni klasa pozbawiona pl i nie implementujca swych metod; do reprezentowania takiej klasy wystarczajca jest sama tablica VMT.

16 Poniewa podmiotem wywoania metody opatrzonej dyrektyw class jest klasa jako cao, inne jest znaczenie identyfikatora Self w jej treci zawiera on mianowicie wskanik do tzw. punktu zerowego tablicy VMT zwizanej z klas, na rzecz ktrej metoda jest wywoywana; syntaktycznie jest on zmienn metaklasy (patrz nastpny przypis) (przyp. tum.).

118

Definiowanie interfejsw
Podobnie jak wszystkie klasy Object Pascala wywodz si z klasy TObject, tak kady interfejs jest pochodn interfejsu IUnknown:
IUnknown = interface ['{00000000-0000-0000-C000-000000000046}'] function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; end;

Notatka:

W Delphi 6 bazowy interfejs nosi nazw IInterface, natomiast IUnknown jest synonimem tej nazwy: type IInterface = interface ['{00000000-0000-0000-C000-000000000046}'] function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; end; IUnknown = IInterface;

Jak wida, deklaracja interfejsu podobna jest do deklaracji klasy; jednak tym, co odrnia interfejs od klasy, jest unikatowy identyfikator (GUID Globally Unique Identifier). Identyfikuje on kady interfejs jednoznacznie dwa rne interfejsy, zdefiniowane w tej samej lub w rnych aplikacjach, stworzonych na tym samym komputerze lub rnych komputerach, w dowolnym czasie, powinny mie dwa rne identyfikatory GUID.

Wskazwka

W rodowisku IDE unikatowy identyfikator GUID otrzymuje si przez nacinicie kombinacji klawiszy Ctrl+Shift+G.

Metody interfejsu IUnknown zwizane s cile z technologi COM, ktr zajmiemy si szczegowo w drugim tomie niniejszej ksiki. Definiowanie interfejsw pochodnych nie rni si zasadniczo od definiowania klas pochodnych poniszy interfejs definiuje wasn metod F1; poniewa nie wskazano interfejsu bazowego, jest nim domylnie IUnknown.
Type IFoo = interface ['{A77A4BE0-0C82-11D6-AA88-444553540001}'] Function F1: Integer; end;

119

Poniszy fragment definiuje interfejs IBar jako pochodny w stosunku do IFoo:


Type IBar = interface(IFoo) ['{A77A4BE1-0C82-11D6-AA88-444553540001}'] Function F2: Integer; end;

Implementowanie interfejsw
Jak wspomnielimy wczeniej, docelow rol interfejsu jest jego implementacja w postaci metod jakiej klasy. Poniszy przykad ilustruje implementacj interfejsw IFoo i IBar przez klas TFooBar:
Type TFooBar = class(TInterfacedObject, IFoo, IBar) function F1: Integer; function F2: Integer; End;

... Function TFooBar.F1: Integer; begin Result := 0; end; Function TFooBar.F2: Integer; begin Result := 0; end;

Zwr uwag, i na licie klas bazowych wystpuje kilka pozycji; tak naprawd klas bazow jest jednak tylko TInterfacedObject, pozostae pozycje s nazwami implementowanych interfejsw. Klasa TInterfacedObject zawiera wszystkie niezbdne mechanizmy implementacji interfejsw, jest wic dla obiektw implementujcych interfejsy klas bazow:
TInterfacedObject = class(TObject, IInterface) protected FRefCount: Integer; function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public procedure AfterConstruction; override; procedure BeforeDestruction; override; class function NewInstance: TObject; override; property RefCount: Integer read FRefCount; end;

Jak wida, nazwy metod stanowicych implementacj interfejsu tosame s z nazwami metod tego interfejsu. Moe si jednak zdarzy, i dwa rne interfejsy implementowane przez t sam klas posiada bd metody o tej samej nazwie; jedna z tych metod (lub obydwie) musi by wwczas implementowana pod zmienion nazw (aliasem) w poniszym przykadzie zmienione zostaj nazwy obydwu metod F1:
type ITool = interface ['{7C9CAAA4-40EB-11D2-A3FB-444553540000}'] Function F1: integer; End; ITip = interface ['{7C9CAAA5-40EB-11D2-A3FB-444553540000}'] Function F1: integer; End; TPrompt = Class(TInterfacedObject, ITool, IHelp) Function Function Function Function End; ITool.F1 = ToolF1; ITip.F1 = TipF1; ToolF1: Integer; TipF1: Integer;

120

Function TPrompt.ToolF1: Integer; begin Result := 0; end; Function TPrompt.TipF1: Integer; begin Result := 0; end;

Dyrektywa implements Implementacja interfejsu moe mie rwnie charakter poredni mianowicie wskanik do implementowanego interfejsu moe by wartoci waciwoci, jak w poniszym przykadzie:
Type TSomeClass = class(TInterfacedObject, ICasual) ... Function GetCasual: TCasual; Property Casual: TCasual read GetCasual implements ICasual; ... End;

Dyrektywa implements stanowi dla kompilatora informacj, i implementacji metod interfejsu poszukiwa naley w innej klasie (w tym przypadku TCasual) zjawisko to nazywa si wic popularnie implementowaniem delegowanym (implementation by delegation). Typ waciwoci zawierajcej dyrektyw implements musi by zgodny z typem implementowanego interfejsu lub z typem implementujcej go klasy. Powody wprowadzenia implementacji delegowanej (pojawia si w Delphi 4) s dwojakie. Po pierwsze, umoliwia ona klarown realizacj koncepcji agregacji obiektw, stanowicej integracj (na gruncie technologii COM) kilku klas w celu realizacji wsplnego celu; zajmiemy si tym szczegowo w jednym z rozdziaw drugiego tomu niniejszej ksiki. Drugi powd wprowadzenia implementowania delegowanego zwizany jest z oszczdnoci zasobw systemowych. Jeeli implementacja jakiego interfejsu wie si np. z duym obcieniem pamici, a interfejs ten wykorzystywany jest bardzo rzadko, wskazane byoby t implementacj odoy do momentu, gdy wspomniany interfejs okae si faktycznie potrzebny. W niniejszym przykadzie aplikacja chcca skorzysta z interfejsu ICasual (dokadniej z jego implementacji) odwoa si w tym celu do waciwoci Casual. Przy pierwszym odwoaniu tego rodzaju metoda dostpowa GetCasual powinna utworzy obiekt typu Casual i zwrci jako wynik jego adres (przy kolejnych odwoaniach powinna zwraca wskanik do istniejcego obiektu singletonu). Jeeli odwoania do waciwoci Casual nie bdzie, nie bdzie te tworzony wspomniany obiekt i nie wystpi zwizane z tym obcienie zasobw systemu.

Korzystanie z interfejsw
W tym miejscu chcielibymy zwrci uwag na pewne charakterystyczne cechy zmiennych reprezentujcych interfejsy. Po pierwsze, zmienne wskazujce na interfejsy nale do kategorii zmiennych o kontrolowanym czasie ycia (lifetime memory-managed) oraz s inicjowane przez kompilator wartoci NIL. Pod drugie dostp do implementowanych interfejsw kontrolowany jest przez liczniki odwoa (reference counters). Poniszy przykad wyjania w sposb pogldowy zakulisowe dziaania odzwierciedlajce obydwa te mechanizmy:

var I: ISomeInterface; begin // w tym miejscu zmienna I inicjowana jest automatycznie // wartoci NIL //-----------------------------------------------------I := (jaka funkcja zwracajca wskazanie na interfejs ISomeInterface)

121

// nastpuje automatyczne zwikszenie licznika odwoa // zwizanego z interfejsem ISomeInterface. //-----------------------------------------------------... I.SomeMethod; // interfejs cigle jest w uyciu ... ------------------------------------------------------// koczy si wykonywanie bloku procedury (funkcji), // koczy si wic czas ycia zmiennej I. // Nastpuje automatyczne zmniejszenie licznika odwoa // zwizanego z interfejsem ISomeInterface. // Jeeli w wyniku tego licznik osign warto zero, // to nastpuje rwnie zwolnienie interfejsu. end;

Inn wan cech kadego interfejsu (jako typu) jest jego zgodno w sensie przypisania z kad implementujc go klas. Oto przykad poprawnej instrukcji przypisania (opieramy si tu na przedstawionych wczeniej definicjach TFooBar i IFoo):
procedure Test(FB: TFooBar) var F: IFoo; begin ... F := FB; // poprawne, bowiem FB implementuje F ...

I kolejny automatyzm Object Pascala operator as uyty w kontekcie interfejsu powoduje (automatyczne) wywoanie metody QueryInterface tego interfejsu oto przykad:
var FB : TFooBar; F: IFoo; B: IBar; begin FB := TFooBar.Create; F := FB; B := F as IBar; // powysza instrukcja rwnowana jest wywoaniu // F.QueryInterface(IBar, B);

Gdyby interfejs IFoo nie oferowa udostpniania interfejsu IBar, ostatnia instrukcja spowodowaaby wyjtek EIntfCastError.

Strukturalna obsuga wyjtkw


Strukturalna obsuga wyjtkw (SEH Structured Exception Handling) stanowi mechanizm umoliwiajcy aplikacji powrt do stanu normalnoci po wystpieniu bdu wykonania. Wyjtki (exceptions) istniay ju w Delphi 1, lecz w Delphi 2 stworzono im nowe oblicze, integrujc je z Win32 API. Materialnym wyrazem wyjtkw s obiekty zawierajce niezbdn informacj, obsugiwane z wykorzystaniem wszelakich zalet OOP poza predefiniowanymi klasami-wyjtkami Delphi uytkownik ma moliwo definiowania nowych klas wyjtkw, specyficznych dla swojej aplikacji. Zacznijmy od przykadu poniszy wydruk przedstawia prosty program z wbudowan obsug wyjtkw zwizanych z operacjami wejcia/wyjcia.

Wydruk 2.3. Przykadowa obsuga wyjtkw wejcia/wyjcia


Program FileIO; uses Classes, Dialogs; {$APPTYPE CONSOLE} Var F: TextFile; S: String; begin

122

AssignFile(F, 'FOO.TXT'); try Reset(F); try Readln(F,S); finally CloseFile(F); end; except on EInOutError do ShowMessage('Bd wejcia/wyjcia!'); end; end;

W przedstawionej konstrukcji try ... finally ... end wykonywana jest najpierw grupa instrukcji pomidzy klauzulami try i finally. Po jej zakoczeniu normalnym lub na skutek wyjtku wykonywana jest grupa instrukcji pomidzy finally i end. Ta grupa instrukcji wykonywana jest niezalenie od tego, jaki by skutek wykonania instrukcji pierwszej grupy. Jest to bardzo wygodne w przypadku, gdy trzeba na przykad bezwarunkowo zwolni przydzielone zasoby, czy te jak w przedstawionym przykadzie zamkn otwarte pliki.

Notatka

Instrukcje zawarte pomidzy finally a end wykonywane s niezalenie od ewentualnego wyjtku zaistniaego w czasie wykonywania cigu instrukcji pomidzy try a finally. Ponadto, w czasie wykonywania bloku instrukcji pomidzy finally a end, wyjtek nadal istnieje, wic po pierwsze nie mona zakada jego braku w tym momencie, po drugie, naley pamita, i po wykonaniu tych instrukcji sterowanie przekazane zostanie do najbardziej zagniedonego bloku exceptend obejmujcego instrukcj, ktra wyjtek spowodowaa.

Zewntrzna konstrukcja try ... except ... end jest wanie podstawowym narzdziem obsugi wyjtkw. Sowo except oddziela grup instrukcji zasadniczych od bloku dokonujcego obsugi wyjtku. Dokonano wic rozdziau miejsca, w ktrym wyjtek wystpuje od miejsca, w ktrym jest on obsugiwany. Istnienie dwch rnych konstrukcji zwizanych z wyjtkami tryfinally i tryexcept odzwierciedla dwojakiego rodzaju dziaania zapewniajce aplikacji bezpieczne dziaanie. Oprcz obsuenia bdw zapewnia bezwarunkowe wykonanie pewnych krytycznych instrukcji, na przykad zwalniajcych przydzielon pami czy zamykajcych otwarte pliki. Sama informacja o fakcie wystpienia wyjtku jest na og niewystarczajca wobec rnorodnoci moliwych wyjtkw ich obsuga musi by bardziej selektywna. Spjrzmy na poniszy przykad:

Wydruk 2.4. Blok obsugi wyjtkw


Program Obsluga; {$APPTYPE CONSOLE} Var R1, R2 : Double; begin While True do begin try Write('Podaj liczb rzeczywist:'); Readln(R1); Write('Podaj inn liczb rzeczywist:'); Readln(R2); Writeln ('Teraz sprbuj podzieli wprowadzone liczby ...'); Writeln ('Iloraz wynosi ', (R1/R2) :5:2 ); except on EZeroDivide do Writeln('Prba dzielenia przez zero!'); on EInOutError do

123

Writeln('Nieprawidowa posta liczby!'); end; end; end;

W powyszym przykadzie mog by poprawnie obsuone dwie kategorie wyjtkw: dzielenia przez zero oraz konwersji liczby z postaci znakowej na zmiennoprzecinkow. Pozostae wyjtki pozostan nie obsuone, chyba e blok obsugi zostanie wzbogacony w tzw. sekcj obsugi domylnej (default exception handler):
Program Obsluga; {$APPTYPE CONSOLE} Var R1, R2 : Double; begin While True do begin try ................ except on EZeroDivide do Writeln('Prba dzielenia przez zero!'); on EInOutError do Writeln('Nieprawidowa posta liczby!'); Else Writeln('Bd niesprecyzowany!'); end; end; end;

Sekcja obsugi domylnej rozpoczyna si nie od sowa kluczowego on, lecz od sowa else. Podobny efekt moemy uzyska nie specyfikujc w bloku obsugi wyjtku adnej sekcji on ... cay blok bdzie wwczas stanowi domyln sekcj:
Program Obsluga; {$APPTYPE CONSOLE} Var R1, R2 : Double; begin While True do begin try ................ except Writeln('Bd przetwarzania co jest nie w porzdku!'); end; end; end;

Ostrzeenie:

W sekcji domylnej obsugiwane s wszystkie wyjtki, nawet te najbardziej niespodziewane, wymagajce specjalnej akcji. Wskazane jest wic ponowienie wyjtku w sekcji domylnej za chwil powrcimy do tego zagadnienia.

Wyjtki jako klasy


Jak ju wczeniej wspomniano, wystpieniu wyjtku towarzyszy utworzenie obiektu zawierajcego stosown informacj. Klas bazow dla obiektw reprezentujcych wyjtki jest klasa Exception zdefiniowana nastpujco:
Type Exception = class(TObject)

124

private FMessage: string; FHelpContext: Integer; public constructor Create(const Msg: string); constructor CreateFmt (const Msg: string; const Args: array of const); constructor CreateRes (Ident: Integer; Dummy: Extended = 0); constructor CreateResFmt (Ident: Integer; const Args: array of const); constructor CreateHelp (const Msg: string; AHelpContext: Integer); constructor CreateFmtHelp (const Msg: string; const Args: array of const; AHelpContext: Integer); constructor CreateResHelp (Ident: Integer; AHelpContext: Integer); constructor CreateResFmtHelp (Ident: Integer; const Args: array of const; AHelpContext: Integer); property HelpContext: Integer read FHelpContext write FHelpContext; property Message: string read FMessage write FMessage; end;

Powysza deklaracja znajduje si w module SysUtils. Najwaniejszym elementem klasy Exception jest waciwo Message, zawierajca werbalny opis sytuacji powodujcej wystpienie wyjtku.
Ostrzeenie

Wasne klasy wyjtkw powinny by definiowane na bazie innych, prawidowo funkcjonujcych klas wyjtkw, na przykad klasy Exception. Gwarantuje si w ten sposb uycie niezbdnych, standardowych mechanizmw obsugi wyjtkw.

Ze wzgldu na obiektow natur wyjtkw w Delphi, ich obsuga ma pewien zwizek ze zjawiskiem dziedziczenia. Ot, sekcja zdefiniowana dla okrelonej klasy wyjtkw (po sowie on) jest rwnie sekcj obsugi wyjtkw pochodnych. Na przykad sekcja zdefiniowana dla wyjtku EMathError bdzie rwnie obsugiwa wyjtki EZeroDivide i EOverflow, ktre s typami pochodnymi w stosunku do EMathError. Wyjtki nie obsuone w ramach bloku except podlegaj obsudze w ramach domylnej (dla aplikacji) procedury obsugi wyjtkw. Standardowo obsuga ta polega na wypisaniu komunikatu pisalimy o tym szczegowo w 4. rozdziale Delphi 4. Vademecum profesjonalisty, w punkcie Zmiana domylnej procedury obsugi wyjtkw. Podczas obsugi wyjtku konieczne jest niekiedy uzyskanie dostpu do obiektu reprezentujcego ten wyjtek na przykad w celu odczytania treci komunikatu ukrywajcego si pod waciwoci Message. Obiekt ten dostpny jest za porednictwem funkcji ExceptObject (jeeli wyjtek aktualnie nie wystpuje, funkcja ta zwraca NIL), moemy si jednak do niego dosta znacznie prociej, specyfikujc w sekcji on reprezentujcy go identyfikator, oddzielony dwukropkiem od identyfikatora klasy, na przykad
try except on E:ESomeException do ShowMessage(E.Message); end;

W powyszym przykadzie identyfikator E reprezentuje biecy obiekt wyjtku klasy ESomeException. Ten sam efekt mona uzyska w nastpujcy sposb:
try except on E:ESomeException do ShowMessage(ESomeException(ExceptObject).Message);

125

end;

Poniewa we frazie else bloku except nie wystpuje identyfikator klasy wyjtku, nie jest take moliwa opisana konstrukcja z dwukropkiem; jedynym rodkiem dostpu do obiektu wyjtku pozostaje wwczas funkcja ExceptObject.

Poza obsugiwaniem wyjtkw, moliwe jest take ich generowanie w aplikacjach. Robi si to za pomoc sowa kluczowego raise, po ktrym wystpuje wyraenie reprezentujce obiekt wyjtku, na przykad:
raise EBadStuff.Create('Co jest nie w porzdku');

Samotne sowo raise, bez podania obiektu wyjtku, powoduje ponowienie wyjtku aktualnie istniejcego.

Wyjtki a przepyw sterowania w programie


W przypadku wystpienia wyjtku sterowanie przekazane zostaje do najbliszego czyli najbardziej zagniedonego w stosunku do instrukcji powodujcej wyjtek bloku obsugi, i by moe do kolejno zewntrznych blokw, a do obsuenia wyjtku i (automatycznego) zwolnienia reprezentujcego go obiektu. Wystpienie wyjtku zwizane jest cile z aktualnym stanem stosu wywoa podprogramw (call stack), jest wic sytuacj globaln dla caego programu, nie za dla poszczeglnych podprogramw. Spjrzmy na wydruk 2.5 przedstawiajcy kod moduu zwizanego z formularzem zawierajcym pojedynczy przycisk. Kliknicie przycisku wywouje procedur zdarzeniow Button1Click(). Procedura ta wywouje procedur Proc1(), ktra wywouje procedur Proc2() ta z kolei wywouje procedur Proc3(). W procedurze Proc3() generowany jest wyjtek; uruchamiajc aplikacj i ledzc wywietlane komunikaty, moemy zaobserwowa przepyw sterowania a do momentu obsuenia wyjtku. Wydruk 2.5. Ilustracja przepywu sterowania podczas wystpienia wyjtku
unit Main; interface uses Windows, StdCtrls;

Messages,

SysUtils,

Classes,

Graphics,

Controls,

Forms,

Dialogs,

type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} type EBadStuff = class(Exception); Procedure Proc3; begin try raise EBadStuff.Create('Dzieje si co niedobrego!'); finally ShowMessage('Wystpi wyjtek, ktry procedura Proc3 zauwaya'); end; end;

126

Procedure Proc2; begin try Proc3; finally ShowMessage('Procedura Proc2 rwnie jest wiadoma wyjtku'); end; end; Procedure Proc1; begin try Proc2; finally ShowMessage('Procedura Proc1 rwnie jest wiadoma wyjtku'); end; end; procedure TForm1.Button1Click(Sender: TObject); const ExceptMsg = 'Wystpi wyjtek okrelony jako "%s"'; begin ShowMessage('Wystpuje acuch wywoa Proc1->Proc2-> Proc3'); try Proc1; except on E:EBadStuff do ShowMessage(Format(ExceptMsg, [E.Message])); end; end; end.

Wykonanie generujcej wyjtek instrukcji raise (w procedurze Proc3()) powoduje przekazanie sterowania do bloku finally; wyjtek pozostaje nieobsuony, sterowanie wraca do procedury Proc2(). Z punktu widzenia procedury Proc2() instrukcja wywoujca procedur Proc3() jest instrukcj powodujc wyjtek; sterowanie wdruje wic do bloku finally (oczywicie w procedurze Proc2()); wyjtek pozostaje nieobsuony, sterowanie wraca do Proc1(). Tu sytuacja si powtarza instrukcja wywoujca Proc2() uwaana jest za instrukcj powodujc wyjtek; sterowanie trafia do bloku finally, a nastpnie do procedury Button1Click(). Wyjtek pozostaje nieobsuony. Z punktu widzenia procedury Button1Click() za wyjtek odpowiedzialna jest instrukcja wywoujca procedur Proc1(). Instrukcja ta znajduje si w obszarze konstrukcji tryexceptend, sterowanie wdruje wic do bloku except i wyjtek zostaje wreszcie obsuony.

Wskazwka

Opisan sytuacj moesz przeledzi samodzielnie, ustawiajc punkt przerwania (breakpoint) na instrukcji wywoujcej procedur Proc1() i kontynuujc wykonanie w sposb krokowy. Musisz jedynie zadba o to, by nie przeszkadzay Ci w tym procedury obsugi wyjtkw w zintegrowanym debuggerze wycz je poprzez usunicie zaznaczenia opcji Stop on Delphi Exceptions na karcie Language Exceptions opcji debuggera (Tools|Debugger Options).

Ponowienie wyjtku
Gdy wskutek wystpienia wyjtku sterowanie trafia do odpowiedniej sekcji on w bloku except (lub w ogle do bloku except w przypadku braku podziau na sekcje), po wyjciu sterowania z tego bloku wyjtek jest obsuony i reprezentujcy go uprzednio obiekt ju nie istnieje. Nie dotyczy to jednak wyjtkw zaistniaych w trakcie realizacji bloku except wdruj one do bloku except na wyszym poziomie zagniedenia.

127

Gdy w poniszej funkcji RPower wystpi wyjtek, jedynym jego sygnaem bdzie zwrcenie zerowego wyniku gdy do tego sprowadza si obsuga wszelkich wyjtkw w bloku except.
Function RPower(X, Y: Real):Real; begin try Result := exp(Y*ln(X)); except Result := 0.0; end; end;

Rozsdniej bdzie jednak powierzy obsug ewentualnego wyjtku zewntrznym blokom except, regenerujc (ponawiajc) go za pomoc instrukcji raise:
Function RPower(X, Y: Real):Real; begin try Result := exp(Y*ln(X)); except Result := 0.0; raise; end; end;

RTTI
Pod tytuowym skrtem kryje si mechanizm udostpniajcy uruchomionej aplikacji informacje o jej obiektach ang. Runtime Type Information. Informacja ta wykorzystywana jest take przez rodowisko IDE, warunkujc jego prawidow wspprac z komponentami na etapie projektowania aplikacji. Elementy zapewniajce czno obiektu ze strukturami danych RTTI wbudowane s ju w klas TObject, posiada je zatem kady obiekt Delphi. Najwaniejsze z metod udostpniajcych informacj RTTI opisane s w tabeli 2.8.

Tabela 2.8. Waniejsze metody klasy TObject udostpniajce informacj z kategorii RTTI Metoda
ClassName() ClassType() InheritsFrom() ClassParent() ClassInfo()

Typ wyniku
String TClass Boolean TClass Pointer

Znaczenie Nazwa klasy Klasa jako typ17 Informuje o istnieniu lub nieistnieniu relacji dziedziczenia midzy klasami Typ macierzysty w stosunku do danego Wskanik do bloku RTTI w pamici

Do kategorii RTTI nale take dwuargumentowe operatory is i as. Pierwszy z nich zwraca warto boolowsk informujc o tym, czy obiekt stanowicy lewy operand zalicza si do klasy identyfikowanej18 przez prawy operand. Ponisza funkcja zwraca nazw klasy obiektu przekazanego jako parametr; jeeli jednak obiekt ten jest obiektem klasy TEdit lub pochodnej, zwracana jest take zawarto jego waciwoci Text:

W Object Pascalu zbir wszystkich typw pochodnych w stosunku do danej klasy skada si na typ wyszego rzdu, zwany klasowym typem referencyjnym (class-reference type) lub metaklas (metaclass). Deklaracja metaklasy ma posta class of typ, gdzie typ jest nazw klasy macierzystej. Najbardziej ogln metaklas jest w Class of TObject obejmujca wszystkie klasy Object Pascala; jej synonimem jest TClass. Podobnie jak inne typy, rwnie metaklasy mog posiada swoje zmienne; wartoci kadej takiej zmiennej jest wskazanie na konkretn klas (nie obiekt!), a dokadniej na zwizan z t klas tablic VMT (przyp. tum.).
18

17

za pomoc nazwy typu lub zmiennej metaklasy (przyp. tum.)

128

function CoZaObiekt(X:TObject):String; begin Result := X.ClassName(); if X is TEdit then Result := Result + '(' + TEdit(X).Text + ')'; end;

Operator as dokonuje bezpiecznego rzutowania typw obiektowych. Konstrukcja X as Y, gdzie X identyfikuje obiekt, a Y klas, rwnowana jest konstrukcji Y(X), pod warunkiem, i prawdziwa jest relacja X is Y; w przeciwnym razie wynikiem operatora as jest warto NIL. Za pomoc operatora as mona by napisa funkcj CoZaObiekt nastpujco:
function CoZaObiekt(X:TObject):String; var P: TEdit; begin Result := X.ClassName(); P := X as TEdit; if P <> NIL then Result := Result + '(' + P.Text + ')'; end;

Informacja RTTI jest nowoci Delphi; nie byo jej w Turbo Pascalu jeeli nie liczy funkcji TypeOf() rwnowanej (w pewnym sensie) metodzie ClassType(), lecz zwracajcej (amorficzny) wskanik do tablicy VMT.

Podsumowanie
W niniejszym rozdziale przedstawilimy najwaniejsze cechy jzyka Object Pascal stanowicego lingwistyczne fundamenty Delphi. Opisalimy najwaniejsze elementy skadni i semantyki jzyka zmienne, operatory, funkcje, procedury, typy oraz instrukcje. Zajlimy si take podstawami realizacji programowania obiektowego, wyjaniajc najwaniejsze elementy i koncepcje zwizane z t filozofi: pola, metody i waciwoci obiektw oraz enkapsulacj, dziedziczenie i polimorfizm; zaprezentowalimy take proste przykady implementacji interfejsw przez obiekty. Na zakoczenie omwilimy podstawowe zasady generowania i obsugi wyjtkw oraz najistotniejsze elementy zwizane z mechanizmem RTTI.

129

Rozdzia 3.

Obsuga komunikatw Windows


Pomimo i Delphi w duym stopniu uwalnia programist od operowania komunikatami Windows na rzecz bardziej wygodnego mechanizmu zdarze to jednak warto powici komunikatom nieco uwagi, zwaszcza w kontekcie ich zwizku ze zdarzeniami Delphi. Dla projektanta nowych komponentw gruntowna znajomo natury komunikatw i zasad zarzdzania nimi jest wrcz niezbdna; dla projektantw posugujcych si gotowymi komponentami moe by przydatna, z tego wzgldu, i bezporednie operowanie komunikatami bywa niekiedy koniecznoci, bo nie wszystko da si wykona za pomoc standardowych mechanizmw Delphi. W niniejszym rozdziale opiszemy pokrtce system komunikatw Win32 i zasady ich obsugi w Object Pascalu, zajmiemy si take ich zwizkiem ze zdarzeniami Delphi.

Wskazwka

Komunikaty s mechanizmem specyficznym dla Windows i nie znajduj zastosowania w aplikacjach midzyplatformowych (CLX). Szczegowe informacje na temat aplikacji midzyplatformowych zawarte s w rozdziale 13.

Natura komunikatw
Komunikaty Windows stanowi odzwierciedlenie w postaci odpowiednich struktur danych i procedur pewnych szczeglnych sytuacji: nacinicia klawisza, kliknicia, przesunicia myszy, upynicia odcinka czasu itp. Struktur danych ucieleniajc komunikat jest rekord zawierajcy informacj o rodzaju zdarzenia oraz dane niosce informacj dodatkow; rzeczownik zdarzenie naley tu rozumie w znaczeniu potocznym, nie w sensie zdarze Delphi czy zdarze Win32. W kategoriach Object Pascala rekord ten, w najbardziej podstawowej formie, ma nastpujc struktur:
Type TMsg = packed record // uchwyt okna, do ktrego komunikat jest adresowany hwnd: HWND;

151

// identyfikator komunikatu message: UINT; // dwa 32bitowe pola zawierajce dodatkow informacj wParam: WPARAM; lParam: LPARAM; // czas utworzenia komunikatu time: DWORD; // pozycja kursora myszy w momencie utworzenia komunikatu pt: TPoint; end;

Powysza definicja znajduje si w module Windows.pas, a znaczenie poszczeglnych jej pl jest nastpujce:
hwnd 32-bitowy uchwyt okna, do ktrego komunikat jest adresowany; z kad okienkow kontrolk Delphi zwizane jest okno, ktrego uchwyt przechowywany jest pod waciwoci Handle. message staa symboliczna klasyfikujca komunikat; oprcz staych predefiniowanych w module Windows.pas moliwe jest definiowanie wasnych komunikatw. wParam zawartoci tego pola jest najczciej staa stowarzyszona z komunikatem; moe ono rwnie zawiera uchwyt okna rdowego lub numer identyfikacyjny kontrolki zwizanej z treci komunikatu. lParam pole to zawiera najczciej dodatkowe dane o zdarzeniu, bd indeks, czy wskanik do okrelonej struktury danych w pamici. Jako e pola wParam i lParam s 32-bitowe, moliwe jest ich rzutowanie na dowolny typ wskanikowy. time zawiera czas utworzenia rekordu zwizanego z komunikatem. pt zawiera pooenie kursora myszy (we wsprzdnych ekranowych) w momencie, gdy utworzono rekord zwizany z komunikatem.

Po zapoznaniu si z ogln struktur komunikatu, zobaczmy teraz, jakie typy komunikatw mona napotka w Win32.

Typy komunikatw
Dla kadego standardowego komunikatu Windows, Delphi (w module Messages.pas) definiuje charakterystyczn sta symboliczn, okrelajc jedn z wartoci pola message rekordu TMsg. Kada z tych staychsymbolicznych rozpoczyna si od liter WM_ (Windows Message). Zestawienie najczciej spotykanych komunikatw zawiera tabela 3.1.

Tabela 3.1. Najczciej wystpujce komunikaty Windows Identyfikator komunikatu


WM_ACTIVATE

Warto
$0006

Znaczenie Okno docelowe staje si aktywne lub przestaje by aktywne. Nacinito i zwolniono klawisz; komunikat ten wystpuje cznie z par komunikatw WM_KEYDOWNWM_KEYUP . Okno docelowe powinno zosta zamknite. Nacinito klawisz. Zwolniono klawisz.

WM_CHAR

$0102

WM_CLOSE WM_KEYDOWN WM_KEYUP

$0010 $0100 $0101

152

WM_LBUTTONDOWN WM_MOUSEMOVE WM_PAINT

$0201 $0200 $000F

Nacinito lewy przycisk myszy. Przesunito kursor myszy. Okno docelowe powinno odwiey swj obszar klienta (client area).

WM_TIMER WM_QUIT

$0113 $0012

Wystpio zdarzenie zegarowe. Naley zakoczy aplikacj.

Jak funkcjonuje system komunikatw Windows?


Z obsug komunikatw Windows zwizane s trzy kluczowe elementy: Kolejka komunikatw (message queue) system Windows utrzymuje oddzieln kolejk komunikatw dla kadej aplikacji; za pobieranie komunikatw z tej kolejki i ich obsug odpowiedzialna jest aplikacja. Ptla obsugi komunikatw (message loop) centraln czci kadej aplikacji Windows jest ptla pobierajca cyklicznie komunikaty z kolejki aplikacji i kierujca je do waciwych okien. Procedura okienkowa (window procedure) jest to procedura zwizana z konkretnym oknem, zajmujca si obsug komunikatw kierowanych do tego okna; jest wywoywana w trybie odwoania zwrotnego (callback), a rezultatem jej dziaania jest najczciej zapisanie informacji zwrotnej w otrzymanym rekordzie komunikatu.

Notatka

Odwoanie zwrotne polega na asynchronicznym wywoaniu (przez system operacyjny) procedury lub funkcji stanowicej cz aplikacji; dokadniej zajmiemy si tym zagadnieniem w rozdziale 6.

Koleje ycia typowego komunikatu przedstawiaj si wic nastpujco: 1. 2. 3. 4. 5. W systemie wystpuje okrelone zdarzenie. System dokonuje klasyfikacji zdarzenia, tworzy reprezentujc je struktur danych i umieszcza j w kolejce zwizanej z aplikacj, ktrej zdarzenie dotyczy. Aplikacja odczytuje wspomnian struktur z kolejki i formuje na jej podstawie rekord reprezentujcy komunikat. Aplikacja przekazuje rekord komunikatu do waciwej procedury okienkowej Procedura okienkowa wykonuje dziaania specyficzne dla otrzymanego komunikatu.

Powyszy scenariusz jest przedstawiony schematycznie na rysunku 3.1. Kroki 3. i 4. realizuj to, co przed chwil nazwalimy ptl obsugi komunikatu. Ptla taka jest charakterystyczna dla kadego programu Windows, gdy caa jego praca sprowadza si do waciwego reagowania na zdarzenia zewntrzne, czyli w konsekwencji na komunikaty Windows. Moe si oczywicie zdarzy tak, e kolejka komunikatw jest pusta i dziaanie programu zostaje zawieszone w punkcie 3., a do otrzymania jakiego komunikatu przez aplikacj.

153

Rysunek 3.1. Schemat funkcjonowania komunikatw Windows

Obsuga komunikatw w kategoriach Delphi


Biblioteka VCL w znacznym stopniu odcia programist od obsugi komunikatw, realizujc chociaby wspomnian ptl pobierajc komunikaty (jest ona zaimplementowana w module Forms.pas). Delphi definiuje ponadto (w module Messages.pas) wasn struktur reprezentujc informacj zawart w komunikacie:
TMessage = packed record Msg: Cardinal; case Integer of 0: ( WParam: Longint; LParam: Longint; Result: Longint); 1: ( WParamLo: Word; WParamHi: Word; LParamLo: Word; LParamHi: Word; ResultLo: Word; ResultHi: Word); end;

Struktura ta zawiera nieco mniej informacji ni jej pierwowzr TMsg, z ktrego przejmuje jedynie identyfikator komunikatu oraz parametry lParam i wParam; pozostae pola s wykorzystywane wewntrznie przez Delphi. Pole Result, nie majce odpowiednika w strukturze TMsg, przeznaczone jest do przekazania informacji zwrotnej jak napisalimy przed chwil, procedura okienkowa musi informowa system (w cile okrelonych kategoriach) o wyniku obsugi kadego komunikatu. Po zakoczeniu obsugi komunikatu przez aplikacj Delphi przetworzy go do postaci zgodnej ze struktur TMsg, pobierajc informacj zwrotn z tego wanie pola. Powrcimy do tej kwestii w dalszym cigu niniejszego rozdziau.

Struktury specyficzne dla rnych typw komunikatw


Udogodnienia oferowane przez Delphi nie kocz si na poziomie struktury TMessage. By uwolni programist od mozolnego odcyfrowywania informacji zawartej w polach lParam i wParam, Delphi oferuje rekordy o strukturze specyficznej dla okrelonych typw komunikatw. Oto rekord charakterystyczny dla wikszoci komunikatw zwizanych z mysz:
TWMMouse = record Msg: Cardinal; Keys: Longint; case Integer of 0: ( XPos: Smallint;

154

YPos: Smallint); 1: ( Pos: TSmallPoint; Result: Longint); end;

Ten rekord jest dostpny take pod innymi nazwami synonimicznymi, co podkrela jego zwizek z poszczeglnymi komunikatami:
TWMLButtonDblClk TWMLButtonDown TWMLButtonUp TWMMButtonDblClk TWMMButtonDown TWMMButtonUp = = = = = = TWMMouse; TWMMouse; TWMMouse; TWMMouse; TWMMouse; TWMMouse;

Podobne struktury zdefiniowane s dla niemal kadego standardowego komunikatu, zgodnie z jednolit konwencj nazewnicz: po przedrostku T nastpuje nazwa komunikatu pozbawiona podkrelenia i przeksztacona na charakterystyczn dla Pascala notacj wielbdzi na przykad komunikatowi WM_SETFONT odpowiada struktura o nazwie TWMSetFont. Nic oczywicie nie stoi na przeszkodzie, by zrezygnowa z tych udogodnie i posugiwa si uniwersalnym rekordem TMessage, przydatnym dla kadego komunikatu.

Przetwarzanie komunikatw
Jak przed chwil stwierdzilimy, przetworzenie komunikatu przez aplikacj polega na skierowaniu go do waciwej procedury okienkowej; ta dokonuje jego mozolnej klasyfikacji, interpretacji zawartej w nim informacji, po czym nastpuje jego waciwa obsuga i zwrotne przekazanie wyniku tej obsugi. Rwnie w tym przypadku Delphi stwarza niebagatelne udogodnienie, gdy, zamiast jednej uniwersalnej procedury okienkowej, moliwe jest definiowanie odrbnych procedur przeznaczonych dla wybranych typw komunikatw. Kada z tych procedur musi by metod obiektu, posiadajc jeden parametr przekazywany przez referencj i opatrzon klauzul message specyfikujc identyfikator obsugiwanego komunikatu. Oto przykad deklaracji metody obsugujcej komunikat WM_PAINT:
procedure WMPaint(var Msg: TWMPaint); message WM_PAINT;

Z punktu widzenia Delphi nazwa metody obsugujcej komunikat moe by dowolna, dla przejrzystoci zaleca si jednak stosowanie konwencji nazewniczej takiej samej, jak w przypadku struktur dla poszczeglnych komunikatw. W charakterze przykadu stwrzmy metod obsugujc komunikat WM_PAINT; obsuga bdzie si tu sprowadza do wyemitowania krtkiego sygnau dwikowego. Deklaracja metody w sekcji private formularza bdzie mie nastpujc posta:
Procedure WMPaint ( var Msg : TWMPaint ); message WM_PAINT;

Jej implementacja jest prosta:


Procedure WMPaint ( var Msg : TWMPaint ); begin MessageBeep(0); inherited; end;

Instrukcja inherited powoduje przekazanie komunikatu do procedury obsugi w klasie macierzystej w tym przypadku TForm.
Notatka

155

Zauwa, e po sowie inherited nie wystpuje nazwa metody, poniewa nie chodzi tu o konkretnie nazwan metod klasy macierzystej, lecz metod obsugujc konkretny komunikat.

Na wydruku 3.1 znajduje si kompletny kod projektu ilustrujcego opisywan obsug komunikatu; na zaczonym krku CD-ROM projekt ten ma nazw GetMess.dpr.

Wydruk 3.1. Przykad obsugi komunikatu


unit GMMain; interface uses SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs; type TForm1 = class(TForm) private procedure WMPaint(var Msg: TWMPaint); message WM_PAINT; end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.WMPaint(var Msg: TWMPaint); begin MessageBeep(0); inherited; end; end.

Gdy aplikacja otrzymuje od systemu komunikat WM_PAINT (stanowicy polecenie odwieenia zawartoci formularza) wywoywana jest metoda WMPaint(), generujca krtki sygna dwikowy i przekazujca komunikat do standardowej obsugi.
Notatka

Generowanie sygnaw dwikowych od zawsze stanowio swoist namiastk debuggera, gdy wyemitowanie okrelonego wzorca dwikowego byo wiadectwem tego, i wykonany zosta okrelony blok instrukcji. Niezalenie od niebywaego rozwoju technik programistycznych, w ostatnich dwch dziesicioleciach w prymitywny debugger babuni nadal jest bardzo atrakcyjny wywoywanie metody MessageBeep() z rnymi predefiniowanymi parametrami powoduje generowanie rozmaitych dwikw, poprzez wbudowany goniczek lub goniki przyczone do karty dwikowej. Mimo caego swego prymitywizmu ma to wiele niezaprzeczalnych zalet: nie wymaga ustawiania punktw przerwa, nie konsumuje zasobw Windows itp.

Jeeli (jak autorzy) nie lubisz wpisywa dugich nazw funkcji, moesz posuy si funkcj Beep() zdefiniowan w module SysUtils: {$IFDEF MSWINDOWS} procedure Beep; begin MessageBeep(0);

156

end; {$ENDIF} {$IFDEF LINUX} procedure Beep; var ch: Char; begin ch := #7; __write(STDOUT_FILENO, ch, 1); end; {$ENDIF}

Kontraktowy charakter obsugi komunikatw


W pierwszym rozdziale ksiki stwierdzilimy, e operowanie zdarzeniami Delphi ma charakter niekontraktowy (contract-free), co oznacza, i programista nie ma obowizku zajmowa si zdarzeniami, w zwizku z ktrymi aplikacja nie wykonuje adnych specjalnych czynnoci. Z komunikatami rzecz si ma zupenie inaczej: Windows, wysyajc do aplikacji okrelony komunikat, oczekuje od niej wykonania zwizanych z tym komunikatem konkretnych czynnoci. Na szczcie komponenty biblioteki VCL wykonuj to zadanie, a jeeli uytkownik chce (w ramach definiowanej przez siebie klasy) obsuy ktry komunikat w sposb niestandardowy, jedyn jego powinnoci wobec kontraktowego charakteru komunikatw jest przekazanie komunikatu do klasy macierzystej (za pomoc instrukcji inherited).
Notatka

Kontraktowo komunikatw to jednak co wicej ni tylko konieczno zapewnienia im obsugi odziedziczonej z klasy macierzystej; obsuga niektrych komunikatw wie si mianowicie z dodatkowymi ograniczeniami na przykad w ramach obsugi komunikatu WM_KILLFOCUS (w kontekcie danej kontrolki) niedozwolone jest przeniesienie skupienia na inn kontrolk, pod grob zawieszenia systemu operacyjnego. przyp. tumacza: Ta kontrolka aplikacji, do ktrej kierowane s zdarzenia pochodzce od klawiatury, nazywa si kontrolk skupion (focused control) , posiada on bowiem pewien wyrniony stan, zwany skupieniem (focus); w danej chwili tylko jedna kontrolka moe posiada skupienie; system operacyjny, wywaszczajc kontrolk ze stanu skupienia (na rzecz innej kontrolki) wysya do niej komunikat WM_KILLFOCUS. Kontrolka to powinna wczas jedynie przyj do wiadomoci, i pozbawia si j skupienia, nie czynic z tym skupieniem niczego wicej, w szczeglnoci nie prbujc przenie go na inn kontrolk (weszaby wczas w drog systemowi operacyjnemu).

Konsekwencje kontraktowego charakteru obsugi komunikatw moemy pozna bez trudu, usuwajc instrukcj inherited z metody WMPaint() formularza projektu GetMess.dpr:
procedure TForm1.WMPaint(var Msg: TWMPaint); begin MessageBeep(0); // inherited; end;

Takie posunicie nie daje systemowi operacyjnemu adnej szansy na przetworzenie komunikatu WM_PAINT, wskutek czego formularz w ogle nie zostanie wywietlony. Mao tego nieustanne wysyanie do formularza komunikatu WM_PAINT spowoduje seri piskw w goniku komputera, trwajc a do zamknicia lub zminimalizowania formularza.

157

Zdarza si jednak, i brak odwoania do odziedziczonej obsugi komunikatu jest efektem zamierzonym w ten wanie sposb mona np. powstrzyma minimalizacj lub maksymalizacj okna w odpowiedzi na komunikat WM_SYSCOMMAND.

Zwrotne przekazywanie wyniku obsugi komunikatu


System Windows, kierujc do aplikacji rnorodne komunikaty, oczekuje zazwyczaj informacji zwrotnej na temat przebiegu ich obsugi. W kategoriach Delphi informacja ta wpisywana jest w pole Result struktury TMessage i innych struktur zwizanych z komunikatami. Klasycznym przykadem komunikatu tego typu jest komunikat WM_CTLCOLOR: system Windows oczekuje jako informacji zwrotnej uchwytu pdzla (brush) okrelajcego kolor wywietlanego obiektu. Przy zaoeniu, i uchwyt ten przechowywany jest w zmiennej BrushHand, stosowna procedura obsugi komunikatu mogaby wyglda nastpujco:
procedure TForm1.WMCtlColor(var Msg: TWMCtlColor); begin inherited; Msg.Result := BrushHand; end;

Powyszy przykad ma jednak znaczenie raczej teoretyczne, poniewa kady komponent posiada waciwo
Color, co uwalnia programist od bezporedniej obsugi komunikatu WM_CTLCOLOR.

Zdarzenie OnMessage klasy TApplication


Zdarzenie Application.OnMessage stanowi wygodny rodek przechwytywania komunikatw skierowanych do aplikacji. Przypisanie mu procedury obsugi sprawi, e kady komunikat pobrany z kolejki zostanie przez t procedur przechwycony przed skierowaniem go do standardowej obsugi przez Windows. Umoliwia to niestandardow obsug komunikatw a wrcz cakowit kontrol nad nimi lecz programista powinien by wiadom tego, i moe si to odbi niekorzystnie na oglnej efektywnoci aplikacji, bo wspomniane zdarzenie wywoywane jest w odpowiedzi na kady komunikat umieszczony w kolejce aplikacji. Wymaga to szczeglnej efektywnoci od procedury zdarzeniowej wic np. ustawienie punktu przerwania w jej wntrzu byoby raczej nierozsdne. Nagwek procedury obsugujcej zdarzenie (TMessageEvent w kategoriach Delphi):
OnMessage

powinien

mie

nastpujc

struktur

procedure JakiObiekt.JakaNazwa ( var Msg : TMsg; var Handled : Boolean);

Wyjciowa warto drugiego parametru zawiera informacj o tym, czy komunikat zosta obsuony w caoci (True), czy te wymagana jest jeszcze jego standardowa obsuga ze strony Windows (False). Zdarzeniu Application.OnMessage mona przypisa procedur obsugi w kodzie programu: Application.OnMessage := Form1.AppMessageHandler; moemy to rwnie uczyni na etapie projektowania, umieszczajc na formularzu komponent

TApplicationEvents ze strony Additional palety komponentw i oprogramowujc jego zdarzenie OnMessage, na przykad:
var NumMessages: integer; procedure TForm1.ApplicationEvents1Message(var Nsg: tagMSG; var Handled:Boolean); begin Inc(NumMessages); Handled := False; end;

W powyszym przykadzie zmienna NumMessages stanowi licznik komunikatw pobieranych z kolejki aplikacji. Zdarzenie Application.OnMessage nie jest generowane dla komunikatw kierowanych do aplikacji w sposb bezporedni, z pominiciem kolejki. Wicej informacji na ten temat znajdziesz na pocztku rozdziau 13. ksiki Delphi 4. Vademecum profesjonalisty.

158

Wysyanie wasnych komunikatw


Jak Windows przekazuje komunikaty swoim aplikacjom, tak jest moliwe opracowanie wasnego systemu komunikatw przesyanych midzy komponentami tej samej aplikacji. Delphi oferuje kilka procedur uatwiajcych t czynno: SendMessage(), PostMessage()i Perform() pierwsze dwie dziaaj na podstawie Win32 API, trzecia niezalenie od niego.

Metoda Perform()
Jest to metoda klasy TControl; suy do bezporedniego przekazania okrelonego komunikatu do wskazanej kontrolki. Metoda ta posiada trzy parametry: identyfikator komunikatu i dwie wartoci stanowice jego tre:
Function TControl.Perform(Msg: Cardinal; WParam, LParam: Longint): Longint;

Metoda Perform() tworzy dla komunikatu odpowiedni blok TMessage oraz wywouje metod Dispatch(), omijajc w ten sposb interfejs Win32 (metoda Dispatch() jest opisana w dalszej czci niniejszego rozdziau). Metoda Perform() funkcjonuje w sposb synchroniczny koczy swe dziaanie dopiero po cakowitym obsueniu komunikatu.

Funkcje SendMessage() i PostMessage()


Metoda Perform() okazuje si jednak cakowicie bezuyteczna, jeeli chcemy przekaza komunikat do nieznanego obiektu, bd do okna reprezentowanego jedynie przez uchwyt, nalecego do aplikacji nie stworzonej w Delphi. Z pomoc przychodz wwczas dwie procedury Win32 API: SendMessage() i PostMessage(), zadowalajce si jedynie uchwytem (handle) okna docelowego. Obydwie te funkcje s niemal identyczne, jedyna rnica pomidzy nimi sprowadza si do sposobu przekazania komunikatu: SendMessage(), podobnie jak Perform(), wysya komunikat bezporednio do okna docelowego i oczekuje na zakoczenie jego obsugi. PostMessage() wprost przeciwnie przekazuje natomiast komunikat do kolejki Windows i natychmiast zwraca sterowanie. Formalna deklaracja opisywanych funkcji wyglda tak:
Function SendMessage(hWnd: HWND; Msg: UINT; wParam : WPARAM; lParam : LPARAM): LRESULT;stdcall;

Function PostMessage(hWnd: HWND; Msg: UINT; wParam : WPARAM; lParam : LPARAM): BOOL;stdcall;

Znaczenie ich parametrw jest nastpujce:


hWnd jest uchwytem okna-adresata, Msg jest identyfikatorem komunikatu, wParam i lParam to dwa parametry zawierajce tre komunikatu.

Wspomniane funkcje rni si take pod wzgldem typu i znaczenia zwracanego wyniku: SendMessage() zwraca warto rwn zwrotnej wartoci pola Result z rekordu TMsg, natomiast PostMessage() informuje jedynie o umieszczeniu (True), bd nieumieszczeniu (False) komunikatu w kolejce okna docelowego.

Komunikaty niestandardowe
Oprcz regularnych komunikatw Windows, o identyfikatorach rozpoczynajcych si od WM_, istniej dwie wane ich grupy, wymagajce odrbnego omwienia: s to komunikaty powiadamiajce (notification messages) i komunikaty uytkownika (user-defined messages).

159

Komunikaty powiadamiajce
Komunikaty powiadamiajce przesyane s od okien potomnych (child windows) do okna macierzystego (parent window) w celu poinformowania go o pewnych zdarzeniach. Maj miejsce niemal wycznie podczas wsppracy ze standardowymi kontrolkami Windows przyciskami, listami wyboru, listami rozwijanymi, okienkami edycyjnymi itp. Nacinicie przycisku, przesunicie suwaka na pasku przewijania, wybranie okrelonego tekstu wszystko to staje si rdem komunikatw powiadamiajcych. W rodowisku Delphi tego rodzaju komunikaty najczciej kierowane s ze strony komponentw do formularza, a wic to on powinien zawiera procedury ich obsugi. Tabela 3.2 zawiera zestawienie komunikatw powiadamiajcych zwizanych ze standardowymi kontrolkami Win32.

Tabela 5.2. Komunikaty powiadamiajce zwizane ze standardowymi kontrolkami Win32 Symbol Znaczenie Przycisk
BN_CLICKED BN_DISABLE BN_DOUBLECLICKED BN_HILITE BN_PAINT BN_UNHILITE

Kliknito przycisk Przycisk zosta zablokowany Kliknito dwukrotnie przycisk Przycisk sta si przyciskiem domylnym Naley odwiey obraz przycisku Przycisk przesta by przyciskiem domylnym

Lista rozwijana (combo)


CBN_CLOSEUP CBN_DBLCLK CBN_DROPDOWN CBN_EDITCHANGE CBN_EDITUPDATE CBN_ERRSPACE CBN_KILLFOCUS CBN_SELCHANGE CBN_SELENDCANCEL CBN_SELENDOK CBN_SETFOCUS

Lista zostaa zamknita Kliknito dwukrotnie pozycj listy Lista zostaa rozwinita Zostaa zmieniona zawarto w oknie edycyjnym Naley odwiey zmieniony tekst w oknie edycyjnym Brak pamici do wykonania operacji Lista przestaje by aktywna Wybrano now list Naley anulowa wybr Naley zatwierdzi wybr Lista staa si aktywna

Kontrolka edycyjna
EN_CHANGE EN_ERRSPACE EN_HSCROLL EN_KILLFOCUS EN_MAXTEXT

Zmieniono edytowany tekst Brak pamici do wykonania operacji Kliknito poziomy pasek przewijania Kontrolka edycyjna przestaa by aktywna Po wstawieniu znaku tekst wykracza poza kontrolk edycyjn Kontrolka edycyjna staje si aktywna Naley odwiey wywietlany tekst w kontrolce edycyjnej Kliknito pionowy pasek przewijania

EN_SETFOCUS EN_UPDATE EN_VSCROLL

160

Lista wyboru
LBN_DBLCLK LBN_ERRSPACE LBN_KILLFOCUS LBN_SELCANCEL LBN_SELCHANGE LBN_SETFOCUS

Dwukrotne kliknicie pozycji listy Brak pamici do wykonania operacji Lista przestaje by aktywna Anulowano wybr Przesunito wybr na inn pozycj Lista wyboru staje si aktywna

Wewntrzne komunikaty VCL


Komponenty biblioteki VCL posiadaj rozbudowan kolekcj komunikatw wewntrznych; komunikaty te nie s raczej przydatne w procesie budowania aplikacji, jednak ich znajomo jest nieodzowna dla projektanta nowych komponentw. Identyfikatory tych komunikatw rozpoczynaj si od przedrostka CM_ (Component Message) lub CN_ (Component Notification), a komunikaty te wykorzystywane s do wewntrznego ustawiania aktywnoci, kolorw, widzialnoci, odtwarzania okien, przecigania komponentw itp. Ich kompletna lista znajduje si w systemie pomocy, w czci powiconej tworzeniu komponentw. Jednym z przykadw zastosowania komunikatw na wewntrzne potrzeby komponentw VCL jest wykrywanie sytuacji, gdy kursor myszy wchodzi w obszar danej kontrolki lub obszar ten opuszcza generowane s wwczas komunikaty (odpowiednio) CM_MOUSEENTER i CM_MOUSELEAVE. Rozpatrzmy w charakterze przykadu nastpujcy fragment definicji komponentu:
TSpecialPanel = class(TPanel) protected procedure CMMouseEnter(var Msg: TMessage); message CM_MOUSEENTER; procedure CMMouseLeave(var Msg: TMessage); message CM_MOUSELEAVE; end; procedure TSpecialPanel.CMMouseEnter(var Msg: TMessage); begin inherited; Color := clWhite; end; procedure TSpecialPanel.CMMouseLeave(var Msg: TMessage); begin inherited; Color := clBtnFace; end;

Komponent TSpecialPanel reaguje na wniknicie kursora w jego obszar przyjmujc kolor biay; po przesuniciu kursora poza jego obszar wraca do standardowego koloru clBtnFace. Moesz to zaobserwowa, uruchamiajc przykadowy projekt CustMessage.dpr znajdujcy si na zaczonym krku CD-ROM.

Komunikaty definiowane przez uytkownika


Czsto zdarza si, i aplikacja chce poinformowa inne aplikacje o pewnym zdarzeniu, bd te chc si ze sob skomunikowa obiekty tej samej aplikacji chodzi tu o sytuacje charakterystyczne z punktu widzenia danej aplikacji. Wielce pomocne mog si wwczas okaza komunikaty. W przypadku komunikujcych si obiektw tej samej aplikacji mona by postawi pytanie po co w ogle posugiwa si komunikatami, czy nie wystarczy wprost wywoa odpowiedni metod obiektu docelowego? Dobre pytanie i kilka rwnie dobrych odpowiedzi. Ot, po pierwsze, przekazanie komunikatu do obiektu jest znacznie mniej krpujcym sposobem komunikacji ni wywoanie jego metody po wysaniu komunikatu mona kontynuowa prac i nie przejmowa si tym, co stanie si z wysanym komunikatem. Drugim wanym powodem jest swoisty polimorficzny charakter komunikatw wysyajc okrelony komunikat pod adresem danego obiektu, nie musimy zna szczegw jego definicji w kategoriach syntaktycznych; przy bezporednim wywoywaniu metody znajomo taka byaby nieodzowna. Trzecia przyczyna zwizana jest natomiast z

161

komunikatami rozgaszajcymi (o nich za chwil) pojedynczy komunikat tego typu zauwaalny jest dla wielu adresatw, nie znanych nawet dokadnie obiektowi-nadawcy; bezporednie wywoywanie metod obiektw-adresatw, jeeli nawet daoby si zrealizowa, wymagaoby skomplikowanych iteracji. Wreszcie obsuga komunikatu nie musi by obowizkowa: jeeli obiektu-adresata nie wi z danym komunikatem adne szczeglne zadania, nie musi on nawet definiowa obsugujcej go metody. Przekonawszy si wic o celowoci operowania komunikatami, przyjrzyjmy si dokadniej ich poszczeglnym kategoriom.

Wewntrzne komunikaty aplikacji


Jest to do nieskomplikowana kategoria komunikatw. Do ich przesyania su poznane ju funkcje
SendMessage(), PostMessage() i Perform(), a jako identyfikatory mona wykorzystywa wartoci od WM_USER+100 do $7FFF (ten wanie zakres system Windows rezerwuje dla komunikatw uytkownika). Oto

prosty przykad:
const sx_MyMessage = wm_User+100; begin SomeForm.Perform (sx_MyMessage, 0, 0); { lub } SendMessage (SomeForm.Handle, sx_MyMessage, 0, 0); { lub } PostMessage (SomeForm.Handle, sx_MyMessage, 0, 0); .... end;

Formularz SomeForm naley oczywicie wyposay w stosown procedur obsugi komunikatu sx_Message:
TForm1 = class(TForm) ... private procedure SXMyMessage(var Msg: TMessage); message sx_MyMessage; end; .... .... Procedure TForm1.SXMyMessage(var Msg: TMessage); begin MessageDlg('Ona zamienia mnie w ab!', mtInformation, [mbOK], 0); end;

Obsuga komunikatw uytkownika nie rni si wic zbytnio od obsugi standardowych komunikatw Windows. Jest rzecz zrozumia, i komunikujce si aplikacje (lub poszczeglne komponenty danej aplikacji) musz stosowa jednolit konwencj numeracji komunikatw, za dla przejrzystoci i czytelnoci kodu naley na oznaczenie poszczeglnych komunikatw wybiera nazwy powizane ze spenianymi przez nie funkcjami.

Ostrzeenie

Nie wysyaj nigdy komunikatu z zakresu WM_USER WM_USER+$7F, chyba e masz gwarancj, i obiekt docelowy obsuy go zgodnie z Twoimi oczekiwaniami. Poniewa kade okno moe posugiwa si komunikatami z tego zakresu niezalenie od pozostaych okien, nietrudno o kolizj identyfikatorw.

Komunikaty midzyaplikacyjne
Narzucenie jednolitej konwencji numerowania komunikatw w przypadku wsppracy rnych aplikacji, stworzonych czsto przez rnych autorw, jest zadaniem trudnym, a czasem wrcz niewykonalnym. Ponadto, na etapie projektowania aplikacji niemoliwe jest wybranie numerw komunikatw nie kolidujcych z innymi aplikacjami, lecymi poza sfer naszego zainteresowania. Std pomys, aby porozumiewajce si aplikacje uyway do identyfikacji komunikatw nie numerw, lecz napisw (acuchw znakw); potrzebny byby jeszcze tylko globalny mechanizm odwzorowujcy jednakowe napisy w jednakowe identyfikatory numeryczne, nie kolidujce z innymi, ju wykorzystywanymi w innych aplikacjach. Mechanizmu takiego dostarcza funkcja RegisterWindowMessage(). Posiada jeden parametr, bdcy acuchem z zerowym ogranicznikiem zawierajcym nazw komunikatu, a jej wynikiem jest (nadany przez Windows) identyfikator tego komunikatu,

162

z zakresu $C000 $FFFF. Funkcja ta gwarantuje, e w ramach tej samej sesji Windows identyczne napisy bd odwzorowywane w identyczne identyfikatory. Z komunikatami, ktrych identyfikatory uzyskiwane s za pomoc funkcji RegisterWindowMessage() wie si jednak pewna subtelna trudno. Ot, jak sobie zapewne przypominasz, w definicji metody obiektu obsugujcej komunikat, po sowie message wystpuje konkretny identyfikator komunikatu, ergo ten sposb obsugi daje si zastosowa wycznie do komunikatw, ktrych identyfikatory znane s ju w momencie kompilacji. Komunikaty o dynamicznie uzyskiwanych identyfikatorach musz by obsugiwane w inny sposb mianowicie poprzez przedefiniowanie procedury okienkowej (subclassing) lub w ramach domylnej procedury obsugi, jak jest metoda DefaultHandler()obiektu. Zagadnienie to zostao szczegowo opisane w 13. rozdziale Delphi 4. Vademecum profesjonalisty.

Notatka

Identyfikator zwracany przez funkcj RegisterWindowMessage() ma znaczenie lokalne dla biecej sesji Windows w nastpnej sesji funkcja ta moe zwrci dla tego samego parametru inny identyfikator. Nie ma wic adnego sensu uywanie tego identyfikatora na etapie projektowania.

Komunikaty rozgaszajce
Komunikaty rozgaszajce (broadcasts) skierowane s do wszystkich kontrolek potomnych (child) danej kontrolki. Do ich rozsyania suy metoda Broadcast() klasy TWinControl. Oto przykad wysania komunikatu UM_FOO do wszystkich kontrolek zawartych w panelu Panel1:
var M : TMessage; begin with M do begin Message := um_Foo; wParam := 0; lParam := 0; Result := 0; end; Panel1.Broadcast (M); end;

Anatomia systemu komunikatw VCL


Dla programisty korzystajcego z komponentw VCL metoda obiektu, zadeklarowana z uyciem klauzuli
message jest jedynym widocznym miejscem, w ktrym odbywa si obsuga komunikatu. Metoda ta jest

jednake tylko jednym z etapw, przez ktre przej musi komunikat na swej drodze yciowej pozostae etapy skrywaj si wewntrz procedur implementacyjnych biblioteki VCL. Mimo ich skrytego charakteru, dobrze jest mie wiadomo ich istnienia, za dla zaawansowanych programistw szczegy obsugi komunikatw trafiajcych do metod obiektw z pewnoci oka si interesujce. A wic: dla komunikatw wysyanych za pomoc PostMessage() nazwijmy je komunikatami kolejkowanymi pierwszym przystankiem jest metoda Application. ProcessMessage(), cyklicznie wywoywana w ramach metody Application.ProcessMessages(), a do wyczerpania kolejki wejciowej:
procedure TApplication.ProcessMessages; var Msg: TMsg; begin while ProcessMessage(Msg) do {loop}; end;

163

Istot metody ProcessMessage() jest wygenerowanie zdarzenia OnMessage:


function TApplication.ProcessMessage(var Msg: TMsg): Boolean; var Handled: Boolean; begin Result := False; if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then begin Result := True; if Msg.Message <> WM_QUIT then begin Handled := False; if Assigned(FOnMessage) then FOnMessage(Msg, Handled); if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then begin TranslateMessage(Msg); DispatchMessage(Msg); end; end else FTerminate := True; end; end;

Jak atwo zauway, zdarzenie to nie jest generowane dla komunikatu WM_QUIT. Jeeli komunikat nie zosta cakowicie obsuony w ramach zdarzenia OnMessage (Handled = False), jest on (pod pewnymi warunkami) kierowany do funkcji DispatchMessage() wywoujcej funkcj StdWndProc(); ta ostatnia kieruje komunikat do obiektu docelowego:
function StdWndProc( Window: HWND; Message, WParam: Longint; LParam: Longint): Longint; stdcall; assembler; asm XOR PUSH PUSH PUSH PUSH MOV MOV CALL ADD POP end; EAX,EAX EAX LParam WParam Message EDX,ESP EAX,[ECX].Longint[4] [ECX].Pointer ESP,12 EAX

Komunikaty wysyane za porednictwem SendMessage() nazwijmy je komunikatami bezporednimi nie trafiaj do kolejki wejciowej. Funkcja SendMessage() przekazuje je bezporednio do funkcji StdWndProc() jest wic jasne, e omijaj one metod ProcessMessage() i wobec tego nie powoduj wystpienia zdarzenia OnMessage(). Poczwszy od funkcji StdWndProc(), dalszy los komunikatu jest ju niezaleny od sposobu jego wygenerowania (PostMessage() czy SendMessage()). Miejscem, do ktrego przekazaa go procedura StdWndProc(), jest metoda MainWndProc() obiektu docelowego, zdefiniowana w klasie TWinControl:
procedure TWinControl.MainWndProc(var Message: TMessage); begin try try WindowProc(Message); finally FreeDeviceContexts; FreeMemoryContexts; end;

164

except Application.HandleException(Self); end; end;

Waciwo WindowProc reprezentuje procedur, ktrej adres znajduje si w polu FWindowProc kontrolki:
property WindowProc: TWndMethod read FWindowProc write FWindowProc;

Domylnie pole to zawiera wskazanie na metod WndProc() dokonujc standardowej dla VCL obsugi komunikatu; wskazanie to jest mu przypisywane w konstruktorze kontrolki:
constructor TControl.Create(AOwner: TComponent); begin inherited Create(AOwner); FWindowProc := WndProc; end;

Jeeli wic programista bdzie chcia zmieni standardowy sposb obsugi komunikatu przez dan kontrolk, moe to uczyni zmieniajc waciwoci WindowProc. Metoda WndProc(), po wstpnym przetworzeniu komunikatu, kieruje go do metody Dispatch():
procedure TControl.WndProc(var Message: TMessage); var Form: TCustomForm; KeyState: TKeyboardState; WheelMsg: TCMMouseWheel; begin if (csDesigning in ComponentState) then begin Form := GetParentForm(Self); if (Form <> nil) and (Form.Designer <> nil) and Form.Designer.IsDesignMsg(Self, Message) then Exit end; if (Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST) then begin Form := GetParentForm(Self); if (Form <> nil) and Form.WantChildKey(Self, Message) then Exit; end else if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST) then begin if not (csDoubleClicks in ControlStyle) then case Message.Msg of WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK: Dec(Message.Msg, WM_LBUTTONDBLCLK - WM_LBUTTONDOWN); end; case Message.Msg of WM_MOUSEMOVE: Application.HintMouseMessage(Self, Message); WM_LBUTTONDOWN, WM_LBUTTONDBLCLK: begin if FDragMode = dmAutomatic then begin BeginAutoDrag; Exit; end; Include(FControlState, csLButtonDown); end; WM_LBUTTONUP: Exclude(FControlState, csLButtonDown); else with Mouse do if WheelPresent and (RegWheelMessage <> 0) and (Message.Msg = RegWheelMessage) then begin GetKeyboardState(KeyState); with WheelMsg do begin Msg := Message.Msg; ShiftState := KeyboardStateToShiftState(KeyState); WheelDelta := Message.WParam; Pos := TSmallPoint(Message.LParam); end; MouseWheelHandler(TMessage(WheelMsg)); Exit;

165

end; end; end else if Message.Msg = CM_VISIBLECHANGED then with Message do SendDockNotification(Msg, WParam, LParam); Dispatch(Message); end;

Ostatecznie, metoda Dispatch() przekazuje komunikat do przeznaczonej dla niego metody obiektu:
procedure TObject.Dispatch(var Message); asm PUSH ESI MOV SI,[EDX] OR SI,SI JE @@default CMP SI,0C000H JAE @@default PUSH EAX MOV EAX,[EAX] CALL GetDynaMethod POP EAX JE @@default MOV ECX,ESI POP ESI JMP ECX @@default: POP MOV JMP end;

ESI ECX,[EAX] dword ptr [ECX].vmtDefaultHandler

Pierwsze dwa bajty struktury Message zawieraj identyfikator komunikatu. Metoda Dispatch() sprawdza wpierw, czy jest to identyfikator zerowy jeeli tak, to komunikat kierowany jest do obsugi domylnej przez metod DefaultHandler(). Kolejny test polega na porwnaniu identyfikatora komunikatu z wartoci $C000 jako doln granic identyfikatorw generowanych przez funkcj RegisterWindowMessage(); jeeli identyfikator naley do tej wanie kategorii, od razu kierowany jest do obsugi domylnej jak przed chwil zaznaczylimy, identyfikatory z tego zakresu nie maj sensu na etapie projektowania, nie da si im wic przypisa metody z klauzul message. Z punktu widzenia wewntrznych mechanizmw Object Pascala, metoda z klauzul message jest metod dynamiczn, a identyfikator komunikatu jest jej indeksem. Odnalezienie danej metody dynamicznej obiektu realizowane jest przez funkcj systemow GetDynaMethod(). Otrzymuje ona w rejestrze EAX warto identyfikujc klas obiektu (czyli wskanik do tablicy VMT), za w rejestrze SI indeks metody; adres danej metody zwracany jest w rejestrze ESI (o ile wyzerowana jest flaga ZF). Po powrocie z metody obsugujcej komunikat przekazywany jest do obsugi domylnej. Jeeli metoda dynamiczna o danym indeksie nie istnieje (flaga ZF jest ustawiona), komunikat kierowany jest do obsugi domylnej od razu. W ten oto sposb komunikat, z chwil przekazania go do dedykowanej mu metody obiektu, wymyka si spod kontroli systemu operacyjnego; jeeli wic ma on by przekazany do obsugi odziedziczonej (inherited), przekazania tego musi dokona wspomniana metoda. Ponadto, jak ju wczeniej powiedzielimy, komunikaty o identyfikatorach wygenerowanych przez metod
RegisterWindowMessage() trafiaj od razu do metody DefaultHandler(); jest ona wic najbardziej

odpowiednim miejscem do obsugi tego typu komunikatw. Opisan wdrwk komunikatu w ramach biblioteki VCL przedstawia schematycznie rysunek 3.2.

166

Rysunek 3.2. Obsuga komunikatu w ramach biblioteki VCL

W celu ilustracji obsugi komunikatu na poszczeglnych etapach skonstruowalimy przykadowy projekt o nazwie CatchIt; jego formularz gwny jest przedstawiony na rysunku 3.3.

Rysunek 3.3. Formularz gwny projektu CatchIt.dpr

Projekt umoliwia przesanie komunikatu do formularza za pomoc metody PostMessage() albo SendMessage() w wyniku kliknicia jednego z przyciskw. Sposb przesania komunikatu identyfikowany jest w jego polu wParam 1 dla PostMessage() i 0 dla SendMessage():
procedure TMainForm.PostMessButtonClick(Sender: TObject); { zakolejkuj komunikat dla formularza } begin PostMessage(Handle, SX_MYMESSAGE, 1, 0); end; procedure TMainForm.SendMessButtonClick(Sender: TObject); { wylij komunikat bezporednio do formularza } begin SendMessage(Handle, SX_MYMESSAGE, 0, 0); end;

Projekt ten stwarza okazj do zakoczenia obsugi komunikatu (zjedzenia go, jak pisz autorzy oryginau) na kadym z czterech etapw w obsudze zdarzenia OnMessage, w metodzie WndProc(), w dedykowanej metodzie obsugi i w metodzie DefaultHandler(); owo zjedzenie polega po prostu na zaniechaniu jego odziedziczonej obsugi. Kod rdowy formularza gwnego projektu zosta przedstawiony na wydruku 3.2.

167

Wydruk 3.2. Kod formularza gwnego projektu CatchIt.dpr


unit CIMain; interface uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, Menus; const SX_MYMESSAGE = WM_USER; uytkownika MessString = '%s komunikat jest teraz w %s.'; type TMainForm = class(TForm) GroupBox1: TGroupBox; PostMessButton: TButton; WndProcCB: TCheckBox; MessProcCB: TCheckBox; DefHandCB: TCheckBox; SendMessButton: TButton; AppMsgCB: TCheckBox; EatMsgCB: TCheckBox; EatMsgGB: TGroupBox; OnMsgRB: TRadioButton; WndProcRB: TRadioButton; MsgProcRB: TRadioButton; DefHandlerRB: TRadioButton; procedure PostMessButtonClick(Sender: TObject); procedure SendMessButtonClick(Sender: TObject); procedure EatMsgCBClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure AppMsgCBClick(Sender: TObject); private { obsuga komunikatu na szczeblu aplikacji } procedure OnAppMessage(var Msg: TMsg; var Handled: Boolean); { obsuga komunikatu w metodzie okienkowej } procedure WndProc(var Msg: TMessage); override; { obsuga komunikatu przez metod obiektu } procedure SXMyMessage(var Msg: TMessage); message SX_MYMESSAGE; { domylna obsuga komunikatu } procedure DefaultHandler(var Msg); override; end; var MainForm: TMainForm; implementation {$R *.DFM} const // acuch okrelajcy sposb wysania komunikatu SendPostStrings: array[0..1] of String = ('Wysany', 'Zakolejkowany'); procedure TMainForm.FormCreate(Sender: TObject); begin // przypisz metod obsugi na szczeblu aplikacji Application.OnMessage := OnAppMessage; // wykorzystaj waciwo Tag pl wyboru do skojarzenia ich z opcjami // w drugiej sekcji AppMsgCB.Tag := Longint(OnMsgRB); WndProcCB.Tag := Longint(WndProcRB); MessProcCB.Tag := Longint(MsgProcRB); DefHandCB.Tag := Longint(DefHandlerRB);

// komunikat definiowany przez

// wykorzystaj waciwo Tag opcji do skojarzenia ich z polami wyboru // w pierwszej sekcji

168

OnMsgRB.Tag := Longint(AppMsgCB); WndProcRB.Tag := Longint(WndProcCB); MsgProcRB.Tag := Longint(MessProcCB); DefHandlerRB.Tag := Longint(DefHandCB); end; procedure TMainForm.OnAppMessage(var Msg: TMsg; var Handled: Boolean); begin // sprawd, czy to ten komunikat: if Msg.Message = SX_MYMESSAGE then begin if AppMsgCB.Checked then begin // wywietl komunikat o aktualnym etapie obsugi i ustaw // odpowiedni flag ShowMessage(Format(MessString, [SendPostStrings[Msg.WParam], 'Application.OnMessage'])); Handled := OnMsgRB.Checked; end; end; end; procedure TMainForm.WndProc(var Msg: TMessage); { procedura okienkowa formularza } var CallInherited: Boolean; begin CallInherited := True; // za a priori obsug odziedziczon if Msg.Msg = SX_MYMESSAGE then // czy to ten komunikat ? begin if WndProcCB.Checked then // gdy zaznaczono odpowiednie pole wyboru begin // wywietl komunikat o aktualnym etapie obsugi i ustaw // odpowiedni flag ShowMessage(Format(MessString, [SendPostStrings[Msg.WParam], 'WndProc()'])); // wywoaj obsug odziedziczon, jeli uytkownik nie postanowi // zakoczy obsugi na niniejszym etapie CallInherited := not WndProcRB.Checked; end; end; if CallInherited then inherited WndProc(Msg); end; procedure TMainForm.SXMyMessage(var Msg: TMessage); { metoda obsugi } var CallInherited: Boolean; begin CallInherited := True; // za a priori obsug odziedziczon if MessProcCB.Checked then // gdy zaznaczono odpowiednie pole wyboru begin // wywietl komunikat o aktualnym etapie obsugi i ustaw // odpowiedni flag ShowMessage(Format(MessString, [SendPostStrings[Msg.WParam], 'metodzie obsugujcej'])); // wywoaj obsug odziedziczon, jeli uytkownik nie postanowi // zakoczy obsugi na niniejszym etapie CallInherited := not MsgProcRB.Checked; end; if CallInherited then Inherited; end; procedure TMainForm.DefaultHandler(var Msg); { domylna obsuga } var CallInherited: Boolean; begin CallInherited := True; // za a priori obsug odziedziczon if TMessage(Msg).Msg = SX_MYMESSAGE then // czy to ten komunikat?

169

begin if DefHandCB.Checked then // gdy zaznaczono odpowiednie pole wyboru begin // wywietl komunikat o aktualnym etapie obsugi i ustaw // odpowiedni flag ShowMessage(Format(MessString, [SendPostStrings[TMessage(Msg).WParam], 'DefaultHandler()'])); // wywoaj obsug odziedziczon, jeli uytkownik nie postanowi // zakoczy obsugi na niniejszym etapie CallInherited := not DefHandlerRB.Checked; end; end; if CallInherited then inherited DefaultHandler(Msg); end; procedure TMainForm.PostMessButtonClick(Sender: TObject); { zakolejkuj komunikat dla formularza } begin PostMessage(Handle, SX_MYMESSAGE, 1, 0); end; procedure TMainForm.SendMessButtonClick(Sender: TObject); { wylij komunikat bezporednio do formularza } begin SendMessage(Handle, SX_MYMESSAGE, 0, 0); end; procedure TMainForm.AppMsgCBClick(Sender: TObject); { zadecyduj o dostpnoci opcji drugiej sekcji w oglnoci } begin if EatMsgCB.Checked then begin with TRadioButton((Sender as TCheckBox).Tag) do begin Enabled := TCheckbox(Sender).Checked; if not Enabled then Checked := False; end; end; end; procedure TMainForm.EatMsgCBClick(Sender: TObject); { zadecyduj o dostpnoci poszczeglnych opcji w drugiej sekcji } var i: Integer; DoEnable, EatEnabled: Boolean; begin // czy dostpne w oglnoci ? EatEnabled := EatMsgCB.Checked;

// iteracja po kontrolkach potomnych drugiej sekcji // i ustawianie ich dostpnoci stosownie do stanu // pl wyboru w pierwszej sekcji for i := 0 to EatMsgGB.ControlCount - 1 do with EatMsgGB.Controls[i] as TRadioButton do begin DoEnable := EatEnabled; if DoEnable then DoEnable := TCheckbox(Tag).Checked; if not DoEnable then Checked := False; Enabled := DoEnable; end; end; end.

Notatka

170

Zwr uwag, i w ramach metod WndProc() i DefaultHandler() wystpuje nazwa metody; w tym przypadku obsuga odziedziczona polega po prostu na wywoaniu odziedziczonej metody z klasy macierzystej.

Jak zapewne zauwaye, metoda DefaultHandler() posiada parametr amorficzny; jest to zrozumiae, gdy musi by ona przygotowana na kad posta rekordu komunikatu: jedynym czynionym przez ni zaoeniem jest to, e pierwsze dwa bajty rekordu zawieraj identyfikator komunikatu. Odwoywanie si do poszczeglnych pl komunikatu wymaga jego rzutowania na odpowiedni typ, na przykad TMessage.

Zwizek komunikatw ze zdarzeniami Delphi


Wspominalimy ju niejednokrotnie, i wikszo zdarze VCL to wysokopoziomowe otoczki komunikatw Windows. Wybrane przykady zdarze generowanych przez odpowiadajce im komunikaty przedstawia tabela 3.3.

Tabela 3.3. Niektre zdarzenia VCL i odpowiadajce im komunikaty Windows Zdarzenie Komunikat(y)
OnActivate OnClick OnCreate OnDblClick OnKeyDown OnKeyPress OnKeyUp OnPaint OnResize OnTimer WM_ACTIVATE WM_LBUTTONDOWN, WM_MBUTTONDOWN WM_CREATE WM_XBUTTONDBLCLICK WM_KEYDOWN WM_CHAR WM_KEYUP WM_PAINT WM_SIZE WM_TIMER WM_RBUTTONDOWN,

Wskazwka

Naley unika bezporedniego obsugiwania komunikatu w sytuacji, gdy w VCL istnieje odpowiadajce mu zdarzenie. Ze wzgldu na niekontraktowy charakter obsugi, zdarzenia s mechanizmem pewniejszym i bezpieczniejszym ni komunikaty Windows.

Podsumowanie
Delphi, oferujc wygodny mechanizm zdarze, nie uniemoliwia bynajmniej bezporedniego operowania komunikatami, koniecznego w niektrych sytuacjach. W niniejszym rozdziale przedstawilimy wic ogln natur komunikatw Windows oraz szczegy ich obsugi w ramach biblioteki VCL. Z komunikatami bdziemy spotyka si jeszcze wielokrotnie, w tym i nastpnym tomie niniejszej ksiki; nie naley jednak zapomina, i s one mechanizmem charakterystycznych dla systemu Windows i nie znajduj zastosowania w aplikacjach midzyplatformowych, uywajcych innych sposobw komunikowania si, opisanych w rozdziale 13.

171

Cz II Zaawansowane zagadnienia projektowe

177

Rozdzia 4.

Tworzenie przenonego kodu


W niniejszym rozdziale zajmiemy si problematyk dostosowania do wymogw Delphi 6 aplikacji stworzonych za pomoc wczeniejszych wersji Delphi. Rozpoczniemy od przedstawienia najwaniejszych rnic pomidzy kolejnymi wersjami Delphi i wynikajcych z tych rnic konsekwencji zwizanych z unowoczenianiem aplikacji. Poniewa Delphi 6 przeamuje tradycyjne granice rodowiska Windows umoliwiajc tworzenie aplikacji take dla platformy linuksowej (w rodowisku o nazwie Kylix), w dalszym cigu rozdziau zajmiemy si cechami specyficznymi tej platformy i przedstawimy kilka uytecznych wskazwek dotyczcych budowy aplikacji przenonych pomidzy obydwoma rodowiskami. Kocow cz rozdziau powicimy niektrym subtelnym rnicom wystpujcym pomidzy poszczeglnymi wersjami Delphi, z uwzgldnieniem wpywu tych rnic na zachowanie przenonoci aplikacji pomidzy wersjami. Mimo i firma Borland, opracowujc nowsze wersje swych produktw, stara si czyni to w miar bezbolenie dla stworzonych wczeniej aplikacji, to jednak niektre unowoczenienia wymagaj pewnych ingerencji w istniejcy kod rdowy aplikacji, by zapewni ich zgodno z najnowszymi wersjami narzdzi programistycznych. Wydaje si to jednak cakowicie zrozumiae to rozsdna cena postpu.

Zaoenia oglne
Rnice wystpujce pomidzy poszczeglnymi wersjami Delphi, a take pomidzy Delphi a Kyliksem, czy te C++Builderem powoduj, i kod rdowy konkretnej aplikacji musi wyglda mniej lub bardziej odmiennie w kadym z wymienionych rodowisk. Z kolei wzgldy niezawodnoci oprogramowania jak rwnie wzgldy samej wygody programistw przemawiaj za tym, by w kadym z tych rodowisk kod wynikowy generowany by na podstawie tego samego, bazowego kodu rdowego. Zobaczmy, jak na gruncie Delphi udao si pogodzi te pozornie sprzeczne wymagania.

Ktra wersja?
Podstawowym rodkiem umoliwiajcym wykorzystanie pojedynczego egzemplarza kodu rdowego w rnych rodowiskach projektowych s symbole kompilacji warunkowej. Firma Borland stosuje spjn numeracj wersji Delphi i C++Buildera (a odtd take Kyliksa) w tym sensie, i w konkretnej wersji kadego z tych produktw obowizujcy jest dokadnie jeden symbol kompilacji warunkowej o postaci VERxxx, zgodnie z ponisz tabel:

179

Tabela 4.1. Symbole kompilacji warunkowej odzwierciedlajce wersj produktu

Produkt
Delphi 1 Delphi 2 C++Builder 1 Delphi 3 C++Builder 3 Delphi 4 C++Builder 4 Delphi 5 C++Builder 5 Kylix 1 Delphi 6

Symbol
VER80 VER90 VER95 VER100 VER110 VER120 VER120 VER130 VER130 VER140 VER140

Dziki wymienionym symbolom moliwe jest dedykowanie wybranych fragmentw kodu rdowego konkretnym wersjom produktu, na przykad:

{$IFDEF VER80} { kod dla Delphi 1 } {$ENDIF} {$IFDEF VER90} // kod dla Delphi 2 {$ENDIF} {$IFDEF VER95} // kod dla C++Buildera 1 {$ENDIF} {$IFDEF VER100} // kod dla Delphi 3 {$ENDIF} {$IFDEF VER110} // kod dla C++Buildera 3 {$ENDIF} {$IFDEF VER120} // kod dla Delphi 4 i C++Buildera 4 {$ENDIF} {$IFDEF VER130} // kod dla Delphi 5 i C++Buildera 5 {$ENDIF} {$IFDEF VER140} // kod dla Delphi 6 i Kyliksa {$ENDIF}

180

Notatka

Zagadkowe na pozr numerowanie wersji rozpoczynajce si od VER80 wynika z faktu, i Delphi stanowi kontynuacj linii rozwojowej Turbo Pascala ostatnia jego wersja nosia numer 7, a obowizujcym na jej gruncie symbolem kompilacji warunkowej by VER70.

Moduy, komponenty i pakiety


Rnice pomidzy poszczeglnymi wersjami Delphi przekadaj si bezporednio na rnice w formacie skompilowanych moduw kada wersja produktu generuje specyficzne dla siebie pliki *.dcu nie dajce si uy w innych wersjach (wczeniejszych lub pniejszych). Jeeli wic dla niektrych moduw tworzonej aplikacji brak jest kodu rdowego, a aplikacja ta ma by kompilowana w rnych wersjach Delphi, musimy dysponowa odrbnym zestawem tych moduw dla kadej z wersji. Nabiera to szczeglnego znaczenia w przypadku rozpowszechniania niezalenych komponentw bez udostpniania ich kodu rdowego jednak nawet udostpniony kod rdowy moe by niekiedy przydatny tylko w konkretnej wersji Delphi. Przechodzc do nowej wersji, powinnimy podj prb uzyskania odpowiednich dla tej wersji binariw lub odpowiednio przystosowanego kodu rdowego.
Notatka

Rnicowanie formatw plikw *.dcu w poszczeglnych wersjach Delphi jest zjawiskiem tej samej natury, co rnicowanie formatw plikw wynikowych *.obj w rnych wersjach jzyka C++. Naley ponadto pamita o tym, i konieczno ponownego skompilowania moduu *.dcu nie musi wynika ze zmiany wersji kompilatora rwnie prawdopodobn przyczyn mog by zmiany dokonane w ramach biblioteki VCL.

Wersja Delphi 3 przyniosa ide pakietu, stanowicego zesp powizanych ze sob moduw rdowych. Biblioteki komponentw przestay by odtd masywnymi bibliotekami DLL, a stay si kolekcjami pakietw. Podobnie jak moduy *.dcu, take pakiety zwizane s cile z wersj produktu, pod kontrol ktrego zostay utworzone; wymagaj wic ponownej kompilacji przy zmianie wersji kompilatora.

Zmiany w rodowisku IDE


Zmiany w szczegach funkcjonowania IDE s bodaj najwczeniej zauwaalne dla uytkownika. Oto kilka przykadw niezgodnoci tego rodzaju pomidzy rnymi wersjami Delphi: Pliki *.RSM zawierajce informacj symboliczn dla debuggera, utworzone w poprzednich wersjach Delphi nie zawsze s czytelne dla Delphi 6; podczas prby ich wczytywania otrzymamy komunikat Error reading symbol file. Przeprowadzajc kompilacj zupen aplikacji (Build) spowodujemy utworzenie nowych, poprawnych plikw *.RSM. Poczwszy od Delphi 5 pliki *.dfm nowo tworzonych formatw maj posta tekstow, nieczyteln dla wczeniejszych wersji Delphi. Ten stan rzeczy moemy oczywicie zmieni, likwidujc zaznaczenie opcji New Forms As Text na karcie Preferences opcji rodowiska. Istniejce pliki *.dfm nie s podczas edycji konwertowane na posta tekstow, niezalenie od stanu wspomnianej opcji. Automatyczne generowanie kodu rdowego podczas tworzenia lub importowania bibliotek typu (TypeInfo Libraries) rni si niektrymi szczegami w kolejnych wersjach Delphi. Podobnie jak w Delphi 5, ustawienia zwizane z odwzorowaniem nazw symboli na posta czyteln dla debuggera znajduj si w pliku tlibimp.sym; znaczenie tych ustawie opisane jest w systemie pomocy Delphi pod hasem Mapping symbol names in the Type Library.

Zgodno pomidzy Delphi i Kyliksem


Zakadajc jakikolwiek stopie przenonoci tworzonej aplikacji pomidzy Delphi a Kyliksem, powinnimy przede wszystkim uwiadomi sobie podstawowy fakt, i biblioteki VCL s tworami specyficznymi dla

181

technologii Windows. Na uytek aplikacji przeznaczonych dla innych rodowisk naley posuy si wic bibliotek zawierajc komponenty dla tzw. platformy X (CLX Component Library for X-platform) obsugiwanej w ramach Delphi 6 i Kyliksa. Biblioteka CLX opisana jest ze szczegami w rozdziale 13.; jej zawarto mona podzieli na cztery podstawowe grupy:

BaseCLX realizuje mechanizmy podstawowe dla wszystkich komponentw. DataCLX udostpnia technologi dbExpress zapewniajc szybki dostp do danych i efektywne zarzdzanie nimi. Szczegy tej technologii opisane s w rozdziale 8. NetCLX zawiera komponenty i kreatory niezbdne do tworzenia sieciowych klientw i serwerw dla

Windows i Linuksa. Tworzenie aplikacji internetowych jest teraz jeszcze atwiejsze i efektywniejsze ni na gruncie technologii WebBroker w poprzednich wersjach Delphi.
VisualCLX stanowi podstaw do tworzenia midzyplatformowych interfejsw uytkownika (GUI). Mimo i zewntrznie podobna do VCL, zrealizowanej jak wiadomo na bazie Windows API, VisualCLX powstaa na podstawie biblioteki Qt firmy Troll Tech (http://www.trolltech.com). Biblioteka Qt udostpnia

mechanizmy wizualne dla wielu platform, midzy innymi Windows i Linuksa.

Gdy rozpoczniemy tworzenie aplikacji midzyplatformowej CLX za pomoc opcji File|New|CLX Application menu gwnego IDE i przeanalizujemy zawarto listy uses w utworzonym module formularza gwnego, moemy zaobserwowa wiele nazw moduw rozpoczynajcych si od litery Q (QGraphics, QControls, QForms itp.). S to midzyplatformowe odpowiedniki podobnie nazwanych moduw biblioteki VCL.
Notatka

Cho biblioteka CLX w obecnej wersji umoliwia tworzenie aplikacji jedynie dla Windows i Kyliksa, planuje si jej rozszerzenie w przyszych wersjach na wzr biblioteki Qt, dostarczajcej mechanizmw dla ponad tuzina rnych platform.

Nie w Linuksie
Jest zrozumiae, i na platformie linuksowej nie znajdziemy specyficznych dla Windows technologii ADO, COM/COM+, BDE, MAPI itp., naley wic w aplikacjach midzyplatformowych unika uywania moduw zwizanych z tymi technologiami (Windows, ComObj, ComServ, ActiveX, AdoDb itp.) oraz funkcji specyficznych dla platformy Win32 API, jak np. RaiseLastWin32Error() czy Win32Check(). Ponadto niektre technologie dostpne w Delphi 6 nie s jeszcze dostpne w obecnej wersji Kyliksa (Kylix 1); planuje si ich wprowadzenie do jego nastpnych wersji mowa tu midzy innymi o mechanizmach DataSnap, BizSnap (SOAP) i WebSnap.

Rnice midzy kompilatorami


Mimo i obydwa kompilatory Delphi i Kyliksa ukierunkowane s na architektur procesorw 86, istnieje midzy nimi kilka istotnych rnic, ktrych naley by wiadomym podczas tworzenia przenonych aplikacji midzyplatformowych.

Symbol kompilacji warunkowej LINUX


Obydwa wspomniane kompilatory predefiniuj charakterystyczne dla siebie symbole kompilacji warunkowej: dla Kyliksa jest to symbol LINUX, dla Delphi symbole MSWINDOWS i WIN32. Mona wic atwo wprowadza do aplikacji elementy kodu rdowego charakterystyczne wycznie dla jednego z tych rodowisk:

182

{$IFDEF LINUX} // kod specyficzny dla Linuksa {$ENDIF} {$IFDEF MSWINDOWS} // kod specyficzny dla Windows {$ENDIF}

Format PIC
Kompilator Kyliksa produkuje kod wynikowy w tzw. formacie PIC (Position Independent Code), rnicy si pod wieloma wzgldami od kodu w formacie PE (Portable Executable) produkowanego przez kompilatory windowsowe. Cho rnice te s bez znaczenia dla programisty ograniczajcego si do czystego Pascala, staj si niezwykle istotne w przypadku uywania jzyka asemblera wbudowanego lub pod postaci doczanych moduw zewntrznych. Najbardziej charakterystyczn cech formatu PIC jest adresowanie wszystkich danych globalnych wzgldem rejestru EBX (jako rejestru bazowego), tak wic poprawna w rodowisku Windows instrukcja
mov eax,SomeVar

w rodowisku Linuksa powinna by zapisana jako


mov eax, [ebx].SomeVar

Ponadto zawarto samego rejestru EBX musi by chroniona przed zmianami przez wywoywane funkcje i procedury. W zwizku z tym kompilator Kyliksa predefiniuje kolejny symbol kompilacji warunkowej PIC dziki czemu rozrnia mona kod przeznaczony dla poszczeglnych platform:

{$IFDEF PIC} mov eax, [ebx].SomeVar {$ELSE} mov eax,SomeVar {$ENDIF}

Konwencje wywoywania
Charakterystyczne dla Windows konwencje stdcall i safecall nie istniej w Kyliksie i mapowane s w jego rodowisku na konwencj cdecl. Naley uwzgldni ten fakt przy ustaleniach dotyczcych kolejnoci przekazywania parametrw i czyszczenia stosu wywoania.

Indywidualne cechy platformy systemowej


Jest oczywiste, i w aplikacjach midzyplatformowych nie naley odwoywa si do mechanizmw typowych tylko dla konkretnej platformy oto kilka z nich:

Linux nie stosuje literowych oznacze napdw dyskowych. Separatorem katalogw w ciece jest w Windows odwrotny ukonik (backslash \), natomiast w Linuksie prosty ukonik (slash /). Waciw posta separatora mona (w obydwu rodowiskach) odczyta ze staej PathSeparator.

183

W Windows poszczeglne cieki na licie cieek rozdzielane s rednikiem, za w Linuksie dwukropkiem. Konwencja nazewnicza plikw UNC (Universal Naming Convention) istnieje tylko w Windows. Niektre katalogi charakterystyczne s dla konkretnej platformy na przykad c:\winnt\system32 lub /usr/bin.

Nowoci Delphi 6
W stosunku do poprzednich, wersja Delphi 6 zostaa wzbogacona w kilka uytecznych nowoci, gwnie w zakresie jzyka Object Pascal i kompilatora. Uatwiaj one tworzenie aplikacji, lecz nie s oczywicie obsugiwane w Delphi 5 i wersjach poprzednich.

Definiowalne typy wariantowe


Poczwszy od Delphi 6, typ Variant przestaje by wbudowanym elementem kompilatora i jest udostpniony programicie w celu definiowania nowych typw wariantowych. Niezbdne do tego mechanizmy znajduj si w module Variants.

Jawne wartociowanie elementw typu wyliczeniowego


W celu zwikszenia zgodnoci Delphi z jzykiem C++, umoliwiono jawne przypisywanie wartoci poszczeglnym elementom typu wyliczeniowego, na przykad:
type TFoo = (fTwo=2, fFour=4, fSix=6, fEight=8);

Dyrektywa $IF
Jedn z oczekiwanych od dawna nowoci s dyrektywy $IF i $ELSEIF, umoliwiajce nie tylko kontrol symboli kompilacji warunkowej, lecz take dokonywanie porwna za pomoc zdefiniowanych staych. Oto przykad:
{$IF Defined(MSWINDOWS) and SomeConstant >= 6} // tu jaki fragment kodu {$ELSEIF SomeConstant < 2} // tu inny fragment kodu {$ELSE} // tu jeszcze co innego {$ENDIF}

Moliwa niezgodno plikw *.DFM


Mechanizm odczytu i zapisu formularzy w postaci binarnej zosta zmieniony w odniesieniu do znakw z grnej powki kodu ASCII (powyej 127). Pliki *.DFM zawierajce takie znaki mog by nieczytelne dla poprzednich wersji Delphi. Pewnym rozwizaniem moe by przechowywanie formularzy w postaci tekstowej.

184

Migracja z Delphi 5
Mimo bardzo duej zgodnoci midzy Delphi 5 a Delphi 6, istniej jednak midzy nimi pewne drobne rnice, ktre mog okaza si istotne przy przenoszeniu aplikacji.

Zmiana wartoci staych typowanych


Przecznik kompilacji $J, odpowiadajcy opcji $WRITEABLECONST, domylnie jest w Delphi 6 wyczony w przeciwiestwie do Delphi 5, gdzie by domylnie wczony. Oznacza to, e wszelkie prby przypisania wartoi staym typowanym (typed constants) zostan przez kompilator zakwestionowane, jeeli przecznik $J nie zostanie jawnie wczony ($J+).

Negacja wartoci typu Cardinal


W Delphi 5 zmiana znaku wyraenia typu Cardinal dokonywana bya na podstawie arytmetyki 32-bitowej, co niekiedy prowadzio do nieoczekiwanych wynikw. Po wykonaniu poniszego fragmentu kodu
var c: Cardinal; i: Int64; begin c := 4294967294; i := -c; WriteLn(i); end;

zmienna i przyjmowaa warto 2. Mimo i jest to zachowanie zgoa niepoprawne, by moe niektre aplikacje zostay do niego przystosowane, czy wrcz uzalenione od niego. Opisany bd zosta poprawiony w Delphi 6 przed negacj typ Cardinal rozszerzany jest do typu Int64, negacja wykonywana jest wic w arytmetyce 64-bitowej i zmienna i przyjmuje spodziewan warto 4294967294.

Migracja z Delphi 4
Poniej przedstawiamy kilka najwaniejszych rnic pomidzy Delphi 4 a Delphi 6, istotnych, gdy chcemy unowoczeni istniejc aplikacj.

Zmiany w zakresie biblioteki RTL


Jedyna istotna zmiana w bibliotece RTL dotyczy wsppracy bibliotek DLL z jednostk zmiennoprzecinkow (FPU) procesora. Poczwszy od Delphi 5, kod startowy biblioteki DLL nie ustawia ju rejestru sterowania koprocesora, przejmujc ustawienia dokonane przez aplikacj wywoujc. Flagi w rejestrze sterujcym koprocesora (FPU Control Register) okrelaj dokadno oblicze, sposb wykonywania zaokrglenia, postpowanie z nieskoczonoci oraz umoliwiaj selektywne maskowanie bdw numerycznych. Jeeli z jakichkolwiek wzgldw konieczna jest zmiana zawartoci tego rejestru, powinnimy jej dokona explicite w czci inicjacyjnej biblioteki, wykorzystujc w tym celu funkcj Set8087CW(); przedtem naley jednak zachowa obowizujce ustawienia (s one przechowywane w zmiennej Default8087CW) i odtworzy je w kodzie koczcym biblioteki. Rozpatrujc rzecz bardziej szczegowo zawarto rejestru FPU jest sum flag, z ktrych kada naley do jednej z poniszych kategorii: tryb zaokrglania:
$0000 zaokrglanie powek do najbliszej liczby parzystej, za pozostaych liczb do najbliszej liczby cakowitej; mwic o liczbie cakowitej mamy tu na myli mantys jako wzorzec bitowy, nie za liczb zmiennopozycyjn jako cao $0400 zaokrglanie w d (w kierunku ) $0800 zaokrglanie w gr (w kierunku + )

185

$0C00 ucicie (w kierunku zera)

dokadno oblicze (liczba bitw mantysy):


$0000 24 bity (typ Single) $0200 53 bity (typ Double) $0300 64 bity (typ Extended)

maskowanie bdw numerycznych:


$0020 maskowanie bdu niedokadnoci wyniku $0010 maskowanie bdu niedomiaru $0008 maskowanie bdu nadmiaru $0004 maskowanie bdu dzielenia przez zero $0002 maskowanie bdu zdenormalizowanego argumentu $0001 maskowanie bdu niedozwolonej operacji

Domylnie rejestr sterujcy koprocesora ustawiony jest w taki sposb, i wyniki oblicze zaokrglane s zgodnie z trybem $0000 (do najbliszej cakowitej lub parzystej), obowizuje maksymalna, 64-bitowa dokadno mantysy ($0300) i zamaskowane s bdy niedokadnoci wyniku, niedomiaru i zdenormalizowanego argumentu ($0032). Ustawienie to mona zmienia za pomoc procedur i funkcji moduu Math ich szczegowy wykaz znajduje si w systemie pomocy pod hasem FPU Control.

Zmiany w bibliotece RTL


Mimo licznych zmian w bibliotece RTL, wikszo zwizanych z nimi zabiegw dostosowawczych sprowadza si bdzie do nieskomplikowanych edycji formularzy i kodu rdowego istniejcych aplikacji. Oto najwaniejsze ze zmian tej kategorii: Typ waciwoci reprezentujcej indeks obrazka na licie TImageList zmieniony zosta (w Delphi 5) z Integer na TImageIndex:
type TImageIndex = type Integer;

(powysza definicja znajduje si w module ImgList). Moe to powodowa konieczno zmiany kodu rdowego w sytuacji, gdy wymagana jest bezwzgldna zgodno typu, na przykad podczas przekazywania parametrw procedur i funkcji przez referencj (var). Metoda
TCustomTreeview.CustomDrawItem() wzbogacona zostaa (w Delphi 5) w parametr PaintImages typu Boolean. Jeeli istniejca aplikacja przedefiniowuje t metod, podczas jej kompilacji

w Delphi 5 lub Delphi 6 pojawi si bdy. Aktywowanie menu kontekstowych w odpowiedzi na komunikat WM_RBUTTONUP lub w ramach zdarzenia OnMouseUp moe doprowadzi do sytuacji, i oczekiwane menu kontekstowe nie pojawi si wcale lub pojawi si dwukrotnie. Staje si to zrozumiae, zwaywszy na mnogo sposobw wywoywania menu kontekstowego prawy przycisk myszy, kombinacja klawiszy Shift+F10, dedykowane klawisze na windowsowej klawiaturze itp. By uchroni programistw przed przykrymi konsekwencjami tego faktu, wprowadzono w Delphi 5 kontrol opisanych dziaa i w sytuacji, gdy menu kontekstowe powinno si pojawi, generowany jest komunikat WM_CONTEXTMENU (=$007B) ; komunikat ten obudowywany jest przez wikszo komponentw zdarzeniem OnContextPopup.

Zmiany w zakresie obsugi Internetu


Dla programistw unowoczeniajcych swe aplikacje internetowe mamy dwie wiadomoci dobr i z: Na pocztek za wiadomo komponent THTML firmy NetMasters, realizujcy mechanizmy przegldarki WWW, zastpiony zosta w Delphi 5 przez komponent TWebBrowser o wyranie wikszych moliwociach, lecz (niestety) zupenie odmiennym interfejsie uytkownika. Zwizane z tym zmiany w

186

kodzie rdowym wymaga bd do duego nakadu pracy. Dla programistw chccych oszczdzi sobie tej fatygi mamy z kolei dobr wiadomo w dalszym cigu dostpna jest kontrolka HTML.OCX, na bazie ktrej zbudowano komponent THTML. Znajduje si ona w katalogu \Info\Extras\NetManage na instalacyjnym CD-ROM-ie Delphi 6. Dobr wiadomoci bdzie na pewno moliwo zrealizowania aplikacji-rozszerze serwera ISAPI i NSAPI z podziaem na pakiety. Aby wykorzysta t moliwo, naley na licie uses zastpi modu HTTPApp przez WebBroker.

Zmiany w obsudze baz danych


Zmiany w zakresie warstwy bazodanowej Delphi zwizane s w duej czci ze zmian nazw istniejcych symboli, co wymaga moe pewnej pracy redakcyjnej. Znacznym przeobraeniom ulega ponadto popularna technologia MIDAS: Typ zdarzenia TDatabase.OnLogin zmieniony zosta z TLoginEvent na TDatabaseLoginEvent. Nie powinno to stwarza wikszych problemw, moe jednak wymaga modyfikacji kodu rdowego podczas programowego kojarzenia wspomnianego zdarzenia z procedur obsugi. Funkcje FMTBCDToCurr() i CurrToFMTBCD() zastpione zostay przez nowe funkcje (odpowiednio) BCDToCurr() i CurrToBCD(). Zwizane z tymi funkcjami chronione (protected) metody komponentu TDataSet zastpione zostay przez chronion, nieudokumentowan metod DataConvert. Znana z poprzednich wersji Delphi technologia MIDAS ulega znacznym przeobraeniom i obecnie (tj. w Delphi 6) dostpna jest pod now nazw DataSnap. Technologii DataSnap powicimy jeden z rozdziaw drugiego tomu niniejszej ksiki.

Migracja z Delphi 3
Wikszo problemw pojawiajcych si podczas przenoszenia aplikacji stworzonych w Delphi 3 do wyszych wersji wynika z wprowadzenia w Delphi 4 kilku nowych typw danych i zmian w zakresie typw ju istniejcych.

32-bitowe liczby cakowite bez znaku


W wersjach poprzedzajcych Delphi 4 podstawowym typem reprezentujcym liczby cakowite by typ Integer, bdcy 32-bitow liczb ze znakiem. Z typem tym utosamiano wiele typw systemowych o rozmiarze 32-bitw DWORD, UINT, HRESULT, HWND, HINSTANCE itp. Wersja Delphi 3 posiadaa take typ Cardinal, majcy w zaoeniu reprezentowa liczby cakowite bez znaku (unsigned integers), lecz reprezentowanie to byo nieco zwodnicze, gdy obejmowao jedynie nieujemn powk typu integer (bit znaku by po prostu ignorowany). W Delphi 4 liczby cakowite bez znaku zyskay penoprawn reprezentacj w postaci typu LongWord, wykorzystujcego wszystkie 32-bity interpretowane jako liczba nieujemna. Spowodowao to midzy innymi nastpujce konsekwencje w zakresie semantyki Object Pascala:

Typy Integer i LongWord nie s ju zgodne w sensie przekazywania parametrw przez referencj przekazanie parametru aktualnego typu Integer w miejsce parametru formalnego typu LongWord spowoduje bd kompilacji. Literay cakowitoliczbowe w zakresie $80000000 $FFFFFFFF uwaane s za wartoci typu LongWord; przypisanie ich do zmiennej typu Integer wymaga jawnego rzutowania typu, na przykad:
var I: Integer; begin I := Integer($FFFFFFFF);

187

Po wykonaniu powyszego przypisania zmienna I bdzie miaa warto 1. Rzutowania typw wymaga rwnie przypisywanie zmiennym typu LongWord wartoci ujemnych:
var L: LongWord; begin L := LongWord(-1);

Po wykonaniu powyszego przypisania zmienna L bdzie miaa warto $FFFFFFFF (dziesitnie 4.294.967.295). Podczas porwnywania wartoci cakowitych o rnych znakach kompilator Delphi 4 konwertuje je na 64bitowe liczby cakowite ze znakiem (Int64), tak wic w poniszej sekwencji
var I: Integer; D: DWORD; begin I := -1; D := $FFFFFFFF; if I = D then Cokolwiek

instrukcja Cokolwiek wykonana zostanie po skompilowaniu jej w Delphi 3, lecz ju nie w Delphi 4. W Delphi 3 wartoci -1 i $FFFFFFFF s bowiem tosame w reprezentacji 32-bitowej, w Delphi 4 zostan one jednak skonwertowane na (rne) wartoci 64-bitowe ze znakiem: $FFFFFFFFFFFFFFFF i $00000000FFFFFFFF.
Wskazwka

Poczwszy od Delphi 4, kompilator dysponuje bogatym zasobem komunikatw o bdach (errors), ostrzee (warnings) i wskazwek (hints), zwizanych z implikacjami domylnej konwersji wartoci cakowitych na wsplny typ Int64. Upewnij si, e generowanie ostrzee i wskazwek nie zostao w kompilatorze wyczone.

64-bitowe liczby cakowite


W Delphi 4 pojawi si take nowy typ Int64, reprezentujcy 64-bitow liczb cakowit ze znakiem. Typ ten sta si jednoczenie domylnym typem cakowitoliczbowym biblioteki RTL i VCL wszdzie tam, gdzie byo to wskazane na przykad, funkcje Trunc() i Round()zwracaj wynik tego wanie typu, uwzgldniaj go rwnie funkcje konwersyjne w rodzaju IntToStr()i IntToHex().

Typ Real
Istniejcy od pocztkw Turbo Pascala typ Real, bdcy uniwersalnym reprezentantem liczb zmiennoprzecinkowych, w Delphi 4 sta si synonimem typu Double. Z unikatowego, szeciobajtowego formatu, specyficznego dla Turbo Pascala i obsugiwanego wycznie w sposb programowy, sta si wic jednym ze standardowych typw jednostki zmiennopozycyjnej (koprocesora) o rozmiarze 8 bajtw i cakowicie odmiennej strukturze wewntrznej. Najpowaniejsz konsekwencj tego przeobraenia jest niezgodno nowego oblicza typu Real z danymi zapisanymi wczeniej w pamiciach zewntrznych, zawierajcymi rekordy posiadajce pola typu Real w jego tradycyjnej postaci. Problemy mog pojawi si take w aplikacjach uzalenionych od pierwotnej struktury danych typu Real. Czynic typ Real synonimem typu Double, autorzy Delphi 4 zdefiniowali jednoczenie nowy typ o nazwie Real48, stanowicy wierny odpowiednik pierwotnego typu Real. Moliwe jest take posunicie nieco bardziej radykalne uytkownicy chccy zachowa dawne znaczenie typu Real mog to uczyni, specyfikujc opcj kompilacji {$REALCOMPATIBILITY ON}.

188

Migracja z Delphi 2
Mimo licznych przeobrae, jakich dowiadczy jzyk Delphi w cigu kilku lat swej drogi rozwojowej, udao si zachowa do duy stopie zgodnoci jego pierwszej wersji 32-bitowej (czyli wanie Delphi 2) z wersj najnowsz (czyli Delphi 6). Generalnie, przeniesienie do Delphi 6 aplikacji stworzonej za pomoc Delphi 2 sprowadza si do systematycznego zastpowania przestarzaych ju konstrukcji konstrukcjami bardziej nowoczesnymi zarwno w ramach biblioteki komponentw, jak i samego jzyka Object Pascal. Do bodaj najbardziej rewolucyjnych a na pewno najliczniejszych nale te nowoci, ktre pojawiy si w Delphi 3. Oto najwaniejsze z nich.

Zmiany w zakresie typw boolowskich


W Delphi 2 wszystkie typy boolowskie (Boolean, ByteBool, WordBool i LongBool) posiaday niezalenie od rozmiaru spjn reprezentacj: wartoci FALSE odpowiadaa warto 0, a wartoci TRUE warto 1. Ta odpowiednio, bdca notabene dziedzictwem Turbo Pascala, musiaa jednak ustpi miejsca technicznym wymogom Win32 API, w efekcie czego warto TRUE przestaa by utosamiana z wartoci 1 i staa si cigiem bitw jedynkowych przyjmujc w ramach typw ByteBool, WordBool i LongBool wartoci (odpowiednio) $FF, $FFFF i $FFFFFFFF. W ten oto sposb wymienione typy boolowskie, bdce w Delphi 2 typami dwuelementowymi, zyskay nagle na liczebnoci poczwszy od Delphi 3, posiadaj one (odpowiednio) 256, 65536 i 4294967294 elementy! Mona si o tym przekona podczas prby zadeklarowania tablicy
var A: array[LongBool] of Integer;

co na pewno si nie powiedzie, zwaywszy na rozmiar tej tablicy (16 GB) i ograniczenia kompilatora (2 GB). W Delphi 2 powysza deklaracja oznaczaa tablic dwuelementow. Dwuelementow jest take tablica deklarowana nastpujco:
var A: array[Boolean] of Integer;

niezalenie od wersji Delphi. Paradoksalnie, powodem caego tego zamieszania sta si sposb sprawdzania przez wiele kontrolek ActiveX, czy zmienna boolowska ma warto TRUE zamiast testowa t warto na niezerowo, porwnywano j z wartoci 1 (minus jeden), co z pewnoci miao sw genez w Visual Basicu.

Wskazwka

Wynika z powyszego do istotny wniosek: aby uniezaleni aplikacj od konkretnej reprezentacji wartoci TRUE (co niewtpliwie przyczyni si do przenonoci teje aplikacji), naley unika jawnych porwna w rodzaju
if BoolVar = TRUE then

na rzecz testw
if BoolVar then

Dyrektywa resourcestring
W Delphi 3 pojawia si moliwo umieszczania staych acuchowych w zasobach aplikacji zamiast jak dotychczas w jej kodzie. Ca spraw zaatwia pojedyncza dyrektywa resourcestring, opisana szczegowo w rozdziale 2. Wczytanie danej z zasobu trwa oczywicie znacznie duej ni odwoanie si do zmiennej znajdujcej si w pamici i opisany mechanizm z pewnoci nie poprawia efektywnoci, jednak daje

189

w zamian moliwo przystosowania aplikacji do rnych wersji jzykowych drog nieskomplikowanej wymiany zasobw, bez ingerencji w kod rdowy czy podstawowe pakiety.

Zmiany w bibliotece RTL


Wersja Delphi 3 przyniosa zmian znaczenia zmiennych globalnych HInstance i IsLibrary. Pierwsza z nich zawiera odtd uchwyt identyfikujcy instancj wykonywanego aktualnie moduu .EXE, biblioteki .DLL lub pakietu; uchwyt zwizany z gwnym moduem wykonywalnym jest natomiast stale obecny pod zmienn globaln MainInstance. Zmienna IsLibrary, umoliwiajca uprzednio rozrnienie, czy biecy kod wykonuje si w kontekcie biblioteki DLL, czy te moduu EXE, nie odzwierciedla poprawnie faktu, i wykonywany kod jest czci pakietu, posiadajc warto False. Do rozrnienia pomidzy moduem EXE, bibliotek DLL i pakietem naley wic posuy si dwiema nowymi zmiennymi: ModuleIsLib i ModuleIsPackage, przyjmujcymi warto True wwczas, gdy wykonywany kod jest czci (odpowiednio) biblioteki DLL lub pakietu.

Klasa TCustomForm
W Delphi 3 pojawia si klasa TCustomForm, porednia pomidzy TScrollingWinControl a TForm. Nie powinno to sprawia problemw podczas przenoszenia aplikacji z Delphi 2 do wyszych wersji, moe jednak wymaga zmiany kodu operujcego bezporednio egzemplarzami klasy TForm w taki sposb, by byy to egzemplarze klasy TCustomForm; dotyczy to midzy innymi wywoa funkcji GetParentForm() i ValidParentForm(), jak rwnie niektrych przypadkw uycia klasy TFormDesigner.

Ostrzeenie:

W Delphi 3 zmienio si rwnie nieco znaczenie funkcji GetParentForm() i ValidParentForm() oraz innych metod biblioteki VCL zwracajcych wskazanie na kontrolk rodzicielsk (Parent). Ich wynikiem moe by teraz NIL, nawet wwczas, gdy odnona kontrolka posiada okno macierzyste, w kontekcie ktrego jest wywietlana. Dzieje si tak na przykad w sytuacji, gdy oknem macierzystym kontrolki nie jest formularz VCL taki przypadek typowy jest dla kontrolek ActiveX.

Naley w zwizku z tym odnale w kodzie (stworzonym w Delphi 2) wszelkie fragmenty w rodzaju

with GetParentForm() do

i usun je, bd te zastpi rwnowanymi fragmentami wykorzystujcymi waciwo ParentWindow gdy zawsze zawiera ona uchwyt (handle) okna macierzystego.

Metoda GetChildren()
Poczwszy od Delphi 3 metoda TComponent.GetChildren() posiada nowy parametr Root:
procedure GetChildren(Proc: TGetChildProc; Root: TComponent); dynamic;

Parametr ten zawiera wskazanie na szczytowy komponent-waciciela, czyli ostatni komponent w acuchu komponentw-wacicieli; zwykle jest to formularz lub modu danych.

190

Serwery automatyzacji
Interfejsy COM, w Delphi 2 bdce jedynie obiektami domeny IUnknown, w Delphi 3 stay si odrbnymi jednostkami syntaktycznymi Object Pascala. Tworzenie serwerw COM stao si dziki temu znacznie prostsze, cho oczywicie jego zasady rni si od tych charakterystycznych dla Delphi 2. Kod wykorzystujcy klas IUnknown jest w dalszym cigu dostpny (w postaci moduw OLEAuto i OLE2), dziki czemu przenoszenie aplikacji COM z Delphi 2 moe odbywa si w miar bezbolenie, natomiast na potrzeby nowych projektw naley raczej uywa moduw ComObj, ComServ i ActiveX. Trzeba jednak pamita, by nie miesza obydwu tych mechanizmw w jednym projekcie.

Migracja z Delphi 1
Programista zmuszony do utrzymywania kodu zgodnego zarwno z Delphi 1, jak i nowszymi wersjami, znajduje si w niewesoej sytuacji, a to z powodu szeregu podstawowych rnic poczwszy od zasadniczej rnicy pomidzy 16- a 32-bitowym Windows, poprzez rnice w samym Object Pascalu, na rnicach w bibliotece VCL skoczywszy. W tych warunkach zagadnienie unowoczeniania 16-bitowych projektw Delphi postrzega mona dwojako: w kategoriach koniecznoci lub w kategoriach szans. Ograniczenie zmian do niezbdnego minimum czyli unowoczenianie z koniecznoci prowadzi do powstania aplikacji co prawda formalnie 32-bitowej, lecz zachowujcej typowe elementy aplikacji 16-bitowej: pojedynczy wtek, tradycyjne kontrolki Windows 3.x itp. Unowoczenienie aplikacji w penym tego sowa znaczeniu czyli zastosowanie wszystkich nowoczesnych technik Win32, np. mechanizmu plikw odwzorowanych wymaga w zasadzie przeprojektowania jej od podstaw i cho wie si niekiedy z do znacznymi inwestycjami, to moe stanowi prawdziw szans dla aplikacji w sensie jej zaistnienia w nowych warunkach systemowych. Wybr pomidzy tymi dwiema moliwociami uwarunkowany jest konkretnym zastosowaniem i preferencjami projektantw.

Znaki i acuchy
W wyniku silnej presji uytkownikw Pascala i Delphi na bardziej efektywn obsug acuchw znakowych, w Delphi 2 pojawi si mechanizm dugich acuchw (long strings), posiadajcych zalety klasycznych acuchw pascalowych i jednoczenie wolnych od dotkliwego ograniczenia dugoci do 255 znakw. Oprcz tego, obok klasycznych jednobajtowych znakw ASCII, pojawiy si dwubajtowe znaki Unicode, wystarczajce, z duym zapasem, dla najbardziej nawet skomplikowanego jzyka. Znaki te pocztkowo nie posiaday moliwoci grupowania w dugie acuchy jedynym dugim acuchem w Delphi 2 by acuch AnsiString, skadajcy si ze znakw jednobajtowych lecz wersja Delphi 3 wypenia t luk, przynoszc acuch WideString.

Nowe typy znakowe


acuchy, co oczywiste, zbudowane s ze znakw, tote analiz nowych typw acuchw poprzedzimy omwieniem nowych typw znakowych. Klasyczny, jednobajtowy znak, reprezentowany przez typ Char w Delphi 1, w nastpnych wersjach reprezentowany jest przez typ AnsiChar o liczebnoci 256 znakw. Nowy, dwubajtowy znak Unicode reprezentowany jest przez typ WideChar o liczebnoci 65536 rnych znakw trudno wyobrazi sobie jzyk mogcy odczu to ograniczenie. Sam identyfikator Char pozostaje nadal poprawny i jak dotychczas oznacza dokadnie to samo, co AnsiChar (czyli wszystko po staremu); autorzy Delphi nie gwarantuj jednak zachowania tego stanu rzeczy we wszystkich nastpnych wersjach. Wic dla bezpieczestwa, by uczyni aplikacj bardziej przenon, nie naley zakada jednobajtowej wielkoci zmiennych typu Char, lecz wszdzie, gdzie wielko ta jest istotna, uywa funkcji SizeOf().

191

Nowe typy acuchowe


O acuchach pisalimy obszernie w rozdziale 2., tutaj wymienimy tylko dla przypomnienia typy acuchowe Delphi 6:
AnsiString podstawowy typ acuchw w Object Pascalu. Jest cigiem znakw AnsiChar o

potencjalnie nieograniczonej dugoci. Zgodny jest z typem reprezentujcym cig jednobajtowych znakw zakoczony bajtem zerowym.
ShortString wywodzcy si z Turbo Pascala acuch znakw AnsiChar, o z gry ustalonej

maksymalnej dugoci, nie przekraczajcej 255 znakw. Podstawowy typ acuchowy w Delphi 1.
WideString stanowi cig znakw WideChar i, podobnie jak AnsiString, nie ma limitowanej dugoci

oraz zakoczony jest zerowym ogranicznikiem.


PChar to wskanik do cigu znakw typu Char zakoczonego bajtem zerowym, na wzr typw char* lub lpstr w jzyku C. PAnsiChar to wskanik do zakoczonego zerem cigu znakw typu AnsiChar. PWideChar to wskanik do zakoczonego zerem cigu znakw typu WideChar.

Znaczenie identyfikatora String zalene jest od ustawienia przecznika kompilacji $H: gdy przecznik ten jest wyczony ($H-), identyfikator zachowuje swoje klasyczne znaczenie wyraajc to samo, co ShortString; przy wczonym przeczniku ($H+) typ String rwnowany jest typowi AnsiString. Wyjtek od tej zasady stanowi deklaracje w postaci String[nnn], gdzie nnn jest liczb z zakresu 1 ... 255; niezalenie od ustawienia przecznika $H, oznaczaj one zawsze acuchy typu ShortString o ustalonej maksymalnej dugoci.
Ostrzeenie

Uwaaj podczas przekazywania do procedur i funkcji parametrw typu String; jeeli deklaracja procedury (funkcji) nastpuje przy innym ustawieniu przecznika $H ni deklaracja zmiennej bdcej parametrem aktualnym, spowoduje to trudne do wykrycia bdy wykonania.

Ustawianie biecej dugoci acucha


W Delphi 1 (i wczeniejszych wersjach Pascala) ustawienie dugoci acucha polegao na wpisaniu odpowiedniej wartoci w jego zerowy bajt:
S[0] := Chr(6)

lub
Byte(s[0]) := 6

Bezporednie ustalanie dugoci acucha byo najefektywniejszym sposobem jego skracania, gdy np. sekwencja
S := 'Ala ma kota'; .... Byte(s[0]) := 3;

jest znacznie efektywniejsza ni rwnowana jej sekwencja


S := 'Ala ma kota'; .... Delete(S,4,Length(S)-3);

lub
S := 'Ala ma kota'; .... S := Copy(S,1,3);

chocia i tutaj czyhay na programist pewne puapki, jak np.


var S : String[63]; ....

192

Byte(S[0]) := 65;

W odniesieniu do dugich acuchw postpowanie takie nie ma sensu, gdy po pierwsze, maj one inn struktur ni klasyczne acuchy pascalowe (por. rozdzia 2.), po wtre, operacjom na dugich acuchach towarzyszy automatyczne przydzielanie i odzyskiwanie pamici na ich potrzeby. Tak wic ustalenie biecej dugoci acucha odbywa si przez wywoanie wbudowanej procedury SetLength:
Procedure SetLength (var S: String; NewLength: Integer );

W odniesieniu do krtkich acuchw procedura SetLength() wykonuje opisane wyej wpisywanie wartoci w bajt zerowy jest wic funkcj uniwersaln w stosunku do wszystkiego, co ukrywa si moe pod oznaczeniem typu String. Poniewa w Delphi 1 funkcja ta nie wystpuje, to chcc zachowa moliwo kompilacji kodu w tym rodowisku, musimy go uzupeni o jej jawn definicj, niewidoczn dla wyszych wersji:
{$IFDEF VER80} Procedure SetLength(var S: String; NewLength: Integer ); begin S[0] := Char(NewLength); end; {$ENDIF}

Dynamiczna alokacja acuchw


W Delphi 1 oraz we wczeniejszych wersjach Pascala, przydzielanie i zwalnianie pamici dla acuchw odbywao si w sposb jawny. Podstaw wykorzystania dynamicznych acuchw stanowiy: typ PString, funkcja NewStr()oraz procedura DisposeStr(). Oto typowy fragment programu w Delphi 1:
var S1, S2 : PString; begin S1 := AllocMem(SizeOf(S1^)); S1^ := 'Niech yje rock!'; S2 := NewStr(S1^); FreeMem(S1, SizeOf(S1^)); Edit1.Text := S2^; DisposeStr(S2); end;

Popatrzmy na rwnowany fragment w nowszych wersjach Delphi:


{$H+} ... Var S1, S2 : String; begin S1 := 'Niech yje rock!'; S2 := S1; Edit1.Text := S2; end;

Te dwa fragmenty naprawd s sobie rwnowane, jednak o ile czytelniejszy (i efektywniejszy!) jest drugi z nich; w 32-bitowych wersjach Delphi zarzdzanie pamici na potrzeby acuchw odbywa si automatycznie.

acuchy jako tablice znakw


Czasami wygodne jest bezporednie odwoywanie si do poszczeglnych znakw acucha, na przykad konstrukcje.
C := S[63]; ... S[12] := 'A';

dopuszczalne s zarwno dla krtkich, jak i dugich acuchw; jest oczywiste, e dla tych pierwszych indeks znaku nie moe przekracza maksymalnego rozmiaru acucha. Z dugimi acuchami wie si jeszcze jedno wymaganie, wynikajce z automatyzmu zarzdzania ich pamici: indeks znaku nie moe wykracza poza aktualnie przydzielony dla acucha obszar pamici, a trzeba pamita, e na pocztku programu (procedury) do adnego dugiego acucha nie jest przyporzdkowany obszar pamici; wskanik stanowicy reprezentacj zmiennej acuchowej ma warto NIL, a wic ponisza sekwencja
var S : String; begin S[5] := 'A'; ...

193

jest ewidentnie bdna i spowoduje wyjtek ochrony dostpu (access violation). Przydzielenie pamici dla acucha moe by wykonane w sposb jawny (przez wywoanie procedury SetLength()) lub w wyniku przypisania mu konkretnej wartoci:
SetLength(S1,5); .... S2 := 'Hello';

W powyszym przykadzie zarwno dla S1, jak S2 przydzielany jest obszar pamici zdolny pomieci co najmniej pi znakw, wic odwoania do S1[5] oraz S2[5] s poprawne.
Ostrzeenie

Naley mie wiadomo tego, e dla acucha WideString indeks znaku nie jest tosamy z offsetem pamici liczonym od jego pocztku odwoanie si np. do pitego znaku wymaga, by do acucha przydzielone byo co najmniej 10 bajtw pamici.

acuchy z zerowym ogranicznikiem


Procedury i funkcje interfejsu programisty wymagaj argumentw tekstowych (acuchowych) w postaci acuchw z zerowym ogranicznikiem. Klasyczne acuchy pascalowe s wic do tego celu nieprzydatne i tego faktu musia by wiadom kady programista Delphi 1, wykonujcy we wasnym zakresie wszystkie niezbdne konwersje, jak w poniszym przykadzie:
{ Delphi 1 } Procedure Foo ( X: PChar ); begin .... end; var S: String; { klasyczny acuch } P: PChar; { wskanik do acucha z zerowym ogranicznikiem } begin S := 'Cze!'; P := AllocMem(255); StrPCopy(P,S); Foo(P); FreeMem(P,255); end;

W 32-bitowych wersjach Delphi dugie acuchy mog by traktowane na rwni z acuchami z zerowym ogranicznikiem, wic powyszy przykad upraszcza si w sposb niewiarygodny:
Procedure Foo ( X: PChar ); begin .... end; var S : String; begin S := 'Cze!'; Foo(PChar(S)); end;

Upraszcza si nie tylko sam tekst programu odpada rwnie konieczno wprowadzania zmiennych pomocniczych (w tym wypadku zmiennej P). acuchy z zerowym ogranicznikiem jako bufory Jednym z zastosowa acuchw wymienionych w tytule jest wykorzystanie ich jako buforw dla informacji tekstowej bdcej wynikiem dziaania funkcji i procedur API typowym tego przykadem jest funkcja GetWindowsDirectory():
Function GetWindowsDirectory( lpBuffer : PChar; uSize: UINT ) : UINT;

Programista chccy otrzyma nazw biecego katalogu w postaci klasycznego acucha musia ucieka si do pewnego triku: w charakterze buforu, przeznaczonego na wynikowy acuch z zerowym ogranicznikiem,

194

specyfikowa on obszar rozpoczynajcy si od pierwszego znaku przedmiotowego acucha, a po zakoczeniu funkcji, badajc dugo wpisanej informacji, sam ustawia ograniczajcy bajt zerowy:
{ Delphi 1 } Var S : String; ... GetWindowsDirectory ( @S[1], 254 ); Byte(S[0]) := StrLen(@S[1]); ...

Poczwszy od Delphi 2, powyszy fragment jawi si jako kuriozalny, i to z dwch powodw. Po pierwsze, jak przed chwil powiedzielimy, bezporednie wpisywanie znakw na pozycje acucha (bo do tego sprowadza si wykorzystywanie go jako bufora) musi by poprzedzone przydzieleniem mu odpowiedniej iloci pamici. Po drugie, zawartoci zmiennej deklarowanej jako String (w Delphi 2 i nastpnych) jest de facto wskazanie na pierwszy znak stosownego acucha, a wic zmienna ta jest w rzeczywistoci wskanikiem; przeniesienie bez zmian powyszej konstrukcji rwnaoby si tworzeniu wskanikw do wskanikw! Oto wic prawidowy odpowiednik powyszego kodu w 32-bitowych wersjach Delphi:
{ Delphi 2 i nastpne } Var S : String; begin SetLength (S, MAX_PATH + 1); // przydziel pami GetWindowsDirectory ( PChar(S), MAX_PATH ); SetLength(S, StrLen(PChar(S)); end;

Ostatnia instrukcja
SetLength(S, StrLen(PChar(S))

peni tutaj do istotn rol. Po wykonaniu funkcji GetWindowsDirectory() funkcja Length() zastosowana do acucha S zwrci warto MAX_PATH+1, gdy taka dugo zostaa ustalona w wyniku wykonania pierwszej z powyszych instrukcji i wpisywanie znakw na konkretne pozycje acucha niczego pod tym wzgldem nie zmienia. Znacznikiem koca wpisywanej informacji jest zerowy ogranicznik i jego wanie poszukuje funkcja StrLen(), zliczajc jednoczenie znaki stanowice uyteczn informacj; liczba tych znakw jest wanie szukan dugoci acucha, ktr trzeba w tym acuchu zapisa za pomoc procedury SetLength().

acuch PChar w roli dugiego acucha


Koniec dugiego acucha sygnalizowany jest przez zerowy ogranicznik, a wic dugie acuchy mog by traktowane tak, jak acuchy z zerowym ogranicznikiem (PChar). Relacja odwrotna nie jest ju w peni prawdziwa acuchy PChar mog by uywane w roli dugich acuchw dopiero po uprzednim spenieniu kilku dodatkowych warunkw. W Delphi 1 niezgodno typw PChar i String bya czym oczywistym, a pomostem czcym te dwa wiaty byy funkcje StrPas()i StrPCopy(). Oto przykad:
{ Delphi 1 } var S : String; P : PChar; begin P := StrNew('Object Pascal'); S := StrPas(P); StrDispose(P); end;

W nastpnych wersjach Delphi funkcja StrPas() okazuje si zbdna:


{ Delphi 2 i nastpne } var S : String; P : PChar; begin P := StrNew('Object Pascal'); S := P; StrDispose(P); end;

Jak wida, napisy reprezentowane przez acuchy PChar mog by wprost przypisywane dugim acuchom.

195

acuch PChar moe by przekazany przez warto do procedury (funkcji) wymagajcej parametru typu
String:
Procedure Bar( S: String ); begin ... end; begin S := StrNew('Cze!'); Bar(P); StrDispose(P); end;

Parametr aktualny PChar jest jednak niedopuszczalny w miejscu parametru formalnego typu String przekazywanego przez referencj:
Procedure VBar( var S: String ); begin ... end; var P: PChar; begin S := StrNew('Cze!'); VBar(P); {tutaj kompilator zaprotestuje } StrDispose(P); end;

Aby mc wywoa procedur VBar(), naleaoby wpierw stworzy acuch o zawartoci identycznej z zawartoci acucha wskazywanego przez P. Zadanie to wykonuje (w 32-bitowych wersjach Delphi) procedura SetString():
procedure SetString(var S: String; Buffer: Char; Len: Integer);

Ustawia ona dan dugo acucha oraz kopiuje do niego zawarto buforu. Mona j traktowa jak zoenie nastpujcych instrukcji:
SetLength(S,Len); if Buffer <> NIL Then Move ( Buffer^, PChar(S), Len );

W przypadku przypisywania zmiennym typu PChar wskaza na dugie acuchy, naley zachowa szczegln ostrono w nastpujcej kwestii: jeeli zakres zmiennej PChar jest zewntrzny w stosunku do zakresu (czyli czasu ycia) dugiego acucha, w pewnych sytuacjach wskazanie zawarte w zmiennej PChar jest bezsensowne. Ilustruje to nastpujcy przykad:

Var P : PChar; procedure Bar ( Var X: PChar ); var S: String; begin S := 'Wewntrz BAR() ...'; P := PChar(S); { z chwil wyjcia z niniejszej procedury acuch S przestaje istnie i wskazanie zawarte w zmiennej P staje si bezsensowne } end; BEGIN Bar(P); ShowMessage(P); // w tym momencie zmienna P nie reprezentuje niczego sensownego END.

196

Rozmiar i zakres zmiennych


Rnice pomidzy Delphi 1 a nastpnymi wersjami dotycz rwnie rozmiaru i zakresu zmiennych standardowych typw. Szczegy zawarte s w poniszych tabelach:

Tabela 4.2. Rnice w rozmiarze zmiennych standardowych typw Typ Delphi 1 Delphi 2 i nastpne
Integer Cardinal String

2 bajty 2 bajty 256 bajtw

4 bajty 4 bajty 4 bajty

Tabela 4.3. Rnice zakresw zmiennych standardowych typw Typ Delphi 1 Delphi 2 i nastpne
Integer Cardinal 32.768 32.767 0 65.536 2.147.483.648 2.147.483.647 0 4.294.967.295

(Delphi 2 i 3 0 2.147.483.647)
String 255 znakw 1.073.741.824 znaki

W wikszoci wypadkw powysze rnice nie maj wikszego znaczenia, szczeglnie jeeli programista unika jawnego odwoywania si do rozmiaru i granicznych wielkoci zmiennych, uywajc w tym celu funkcji SizeOf(), Low()i High(). Powane kopoty mog zacz si w przypadku, gdy programista zamierza wykorzystywa w 32-bitowych wersjach Delphi pliki rekordw lub tzw. struktury BLOB (Binary Large Objects), utworzone w Delphi 1; w takim wypadku naley posugiwa si tzw. typami kompatybilnymi, wymienionymi w tabeli 4.4.

Tabela 4.4. Odpowiednio typw 16- i 32-bitowych wersji Delphi Delphi 1 Delphi 2 i nastpne
Integer Cardinal String SmallInt Word ShortString

Wyrwnywanie pl rekordw
W przeciwiestwie do Delphi 1, gdzie pola rekordw alokowane byy w kolejnych bajtach pamici, 32-bitowe wersje stosuj standardowo wyrwnywanie pl (fields alignment): pola 32-bitowe oraz pierwsze pole rekordu niezalenie od typu wyrwnywane s na poziomie dwusowa (4 bajty, z ktrych pierwszy, najmniej znaczcy, ma adres podzielny przez 4), za dane 16-bitowe na granicy sowa, ktrego pierwszy bajt ma adres parzysty. Powoduje to nieco wiksze obcienie pamici, lecz czyni bardziej efektywnym odczytywanie danych w pamici przez procesor. Dla programisty najwaniejsze s jednak luki, ktre mog tworzy si midzy polami rozpatrzmy nastpujc definicj:
type TX = record B : Byte; L : Longint; end;

197

W Delphi 1 rozmiar tego rekordu jest sum rozmiarw jego pl funkcja SizeOf() zwraca warto 5. W nastpnych wersjach Delphi rozmiar rekordu wynosi jednak 8, gdy zarwno pole B, jak i pole L wyrwnane s na granicy podwjnego sowa midzy polami istnieje wic trzybajtowa luka. Ponadto sam rozmiar rekordu zaokrglany jest w gr do penej wielokrotnoci podwjnego sowa. Moemy wymusi rezygnacj z wyrwnywania pl danego rekordu, umieszczajc w deklaracji jego typu klauzul packed:
type TX = packed record B : Byte; L : Longint; end;

Rozmiar spakowanego rekordu jest sum rozmiarw jego pl nie jest zaokrglany do wielokrotnoci podwjnego sowa. Mona cakowicie zrezygnowa z wyrwnywania pl rekordu, specyfikujc opcj kompilacji {$A}. Ponadto, jeeli kolejno definicji pl w rekordzie nie jest istotna, moemy unikn kopotliwych luk, rozpoczynajc od pl 32-bitowych, poprzez pola 16-bitowe (oraz pola o nieparzystej wielokrotnoci 16 bitw), koczc na pozostaych polach.

32-bitowa arytmetyka
32-bitowe wersje Delphi stosuj arytmetyk 32-bitow jako domyln, w miejsce 16-bitowej arytmetyki stosowanej w Delphi 1.0. Przeanalizujmy nastpujcy fragment kodu:
Var L : Longint; w1, w2 : Word; begin w1 := $FFFE; w2 := 5; L := w1 + w2; end;

Wartoci wyraenia w1 + w2 jest (szesnastkowo) $10003; warto ta przekracza zarwno zakres zmiennych typu word, jak i zakres 16-bitowej arytmetyki Delphi 1. Zmiennej L zostanie wic przypisana warto 3 jako wynik obcicia powyszej wartoci do 16 bitw, i to niezalenie od faktu, e zmienna ta byaby w stanie pomieci prawdziwy wynik wyraenia. Nie ma tego kopotu w Delphi 2 i nastpnych, gdzie wyniki porednie s 32- lub 64-bitowe.

Rnice dotyczce typu TDateTime


Ze wzgldu na zgodno z mechanizmem OLE oraz bibliotek Win32 API, w Delphi 2 przyjto jako podstaw pomiaru daty/czasu pocztek dnia (pnoc) 30 grudnia 1899 roku odpowiada jej zerowa reprezentacja zmiennej. W Delphi 1 odpowiednikiem zerowej wartoci TDateTime by dosowny moment zerowy godzina 0:00 dnia 00/00/0000. Rnica ta nie ma oczywicie adnego znaczenia dla daty/czasu przechowywanej w bazach danych w postaci znakowej, objawia si natomiast w przypadku przechowywania ich w postaci binarnej w polach BLOB.

Sekcja finalization
W Turbo Pascalu istnia mechanizm jednorazowego wykonywania czynnoci kocowych w trakcie koczenia aplikacji mowa tu o acuchu procedur wskazywanych przez zmienn ExitProc. W Delphi 1 pojawia si procedura AddExitProc(), uatwiajca instalacj nowej procedury koczcej. Do Delphi 2 wprowadzono natomiast nowy element syntaktyczny w postaci sekcji koczcej modu, wykonywanej jednokrotnie podczas koczenia aplikacji; kolejno wykonywania poszczeglnych sekcji koczcych jest odwrotna do kolejnoci wykonywania sekcji initialization. Rozpatrzmy nastpujcy fragment moduu Delphi 1:
.... Procedure MyExitProc; begin MyGlobalObject.Free;

198

end; initialization AddExitProc(MyExitProc); MyGlobalObject := TGlobalObject.Create; END.

Poczwszy od Delphi 2, przykad ten mona uproci dziki zastosowaniu sekcji finalization:
initialization MyGlobalObject := TGlobalObject.Create; finalization MyGlobalObject.Free; END.

Jzyk asemblera
32-bitowy charakter rodowiska ujawnia si najwyraniej podczas programowania w jzyku asemblera z reguy wymaga to kompletnego przeprogramowania kodu, gdy zmieniony sposb adresowania odbija si niemale na kadej instrukcji. Z uywaniem jzyka asemblera wie si pytanie, do jakich celw jest on wykorzystywany, gdy s to przede wszystkim odwoania do niskopoziomowych mechanizmw systemowych, ktre by moe nie istniej ju w rodowisku 32-bitowym. Klasycznym przykadem jest tutaj mechanizm DPMI, osigalny dotychczas za pomoc przerwania $31, nieobecny dla aplikacji Win32 (funkcje DPMI dostpne s w dalszym cigu dla aplikacji DOSowych). Wraz z nastaniem Delphi 2 skoczy si rwnie ywot instrukcji inline nie jest ju tolerowana przez kompilator.

Konwencje przekazywania parametrw do procedur i funkcji


Z przekazywaniem parametrw do procedur i funkcji wi si dwa zagadnienia: kolejnoci umieszczania ich na stosie oraz zdejmowania ich ze stosu. Kolejno umieszczania parametrw na stosie moe by zgodna z ich kolejnoci w deklaracji procedury/funkcji (jest tak w Pascalu) lub przeciwna (jak jest w C); opis ten jest uproszczony, poniewa nie wszystkie parametry musz znajdowa si na stosie (o tym za chwil). Konwencje przekazywania parametrw okrelaj, kto zdejmuje je ze stosu wywoywana procedura (Pascal) czy te program wywoujcy (C). We wszystkich wersjach Delphi dostpne s obydwie konwencje pascalowa i C z czym wie si obecno deklaracji pascal i cdecl; poczwszy od Delphi 2 dostpne s rwnie konwencje register i stdcall. Zgodnie z konwencj register, pierwsze trzy 32-bitowe parametry przekazywane s w rejestrach eax, edx oraz ecx, pozostae za w konwencji pascalowej, zgodnie z ktr odbywa si rwnie czyszczenie stosu. Metoda ta zapewnia najwiksz efektywno i poczwszy od Delphi 2 jest konwencj domyln. Konwencja stdcall jest mieszank konwencji cdecl i pascal: przekazywanie parametrw odbywa si zgodnie z pierwsz z nich, a czyszczenie stosu z drug. W Delphi 3 wprowadzono jeszcze jedn konwencj safecall. Zgodnie z jej zasadami przekazywanie parametrw i czyszczenie stosu odbywa si identycznie jak w przypadku konwencji stdcall, a ponadto wyjtek zaistniay w czasie realizacji procedury/funkcji jest automatycznie obsugiwany przed jej zakoczeniem.
Notatka

W przeciwiestwie do interfejsu 16-bitowego, uywajcego konwencji pascal, Win32 API stosuje konwencj stdcall. Musisz wic pamita o tym, i wszystkie funkcje zwrotne (callback functions), ktre posiadasz w

199

swoim kodzie, musz uywa konwencji stdcall. Tak wic funkcja zwrotna, ktra dla wsppracy z interfejsem 16-bitowym zostaa zadeklarowana nastpujco: function EnumWindowsProc (Handle: hwnd; lParam: Longint) : BOOL; export; w rodowisku 32-bitowym powinna by zadeklarowana jako function EnumWindowsProc (Handle: hwnd; lParam: Longint) : BOOL; stdcall;

Biblioteki DLL
Zasady tworzenia i wykorzystywania bibliotek DLL w 32-bitowych wersjach Delphi niewiele rni si od analogicznych zasad w Delphi 1 najwaniejsze rnice sprowadzaj si do nastpujcych zagadnie: Poniewa Win32 stosuje paski (flat) model pamici, kwalifikator export w procedurach zwrotnych (callback) nie ma ju znaczenia i jest ignorowany przez kompilator1. Jeeli tworzona biblioteka ma mie charakter uniwersalny, naley stosowa konwencj przekazywania parametrw stdcall, w celu zachowania maksymalnej zgodnoci z wieloma innymi platformami. Eksportowanie procedur i funkcji biblioteki powinno odbywa si raczej przez nazw ni przez indeks. Poniszy fragment kodu z Delphi 1 ilustruje eksportowanie przez indeks:
...
Function SomeFunction: integer; export; begin ... end; Procedure SomeProcedure; export; begin ... end; exports SomeFunction index 1, Someprocedure index 2;

zalecane eksportowanie przez nazw ma natomiast nastpujc posta:


Function SomeFunction: integer; export; begin ... end; Procedure SomeProcedure; export; begin ... end; exports SomeFunction name 'SomeFunction', SomeProcedure name 'SomeProcedure' ;

Eksportowane nazwy wraliwe s na wielko liter (case sensitive); naley o tym pamita podczas deklarowania procedur/funkcji importowanych oraz wywoa funkcji GetProcAddr(). Podczas specyfikowania nazwy biblioteki w dyrektywie external funkcji importowanej, mona opcjonalnie poda rozszerzenie; domylnym rozszerzeniem jest .DLL.

Zasadniczym zadaniem dyrektywy export jest wyposaenie funkcji zwrotnej w mechanizm dokonujcy chwilowego przeczenia globalnego segmentu danych by on inny dla moduu wywoujcego i inny dla moduu zawierajcego funkcj zwrotn. W Win32 wszystko odbywa si w pojedynczym segmencie i przeczanie takie jest cakowicie zbdne (przyp. tum.).

200

W rodowisku Windows 3.x biblioteka DLL posiadaa tylko jeden globalny segment danych, dzielony midzy wszystkie aplikacje wykorzystujce bibliotek (wszystkie instancje biblioteki), dziki czemu mogy one wymienia dane midzy sob, jednoczenie bdc nawzajem od siebie uzalenione (jedna aplikacja moga zakca prac pozostaych). W rodowisku Win32 biblioteki DLL wczane s do przestrzeni adresowej poszczeglnych aplikacji i wymiana taka nie jest ju moliwa.

Zmiany zwizane z systemem operacyjnym


Zmiany w systemie operacyjnym Windows 95/NT w stosunku do Windows 3.x s tak daleko idce, e nie mogo to pozosta bez wpywu na niektre elementy Object Pascala. Rozpatrzymy kolejno poszczeglne konsekwencje tych zmian.

Adresowanie 32-bitowe
Win32 opiera si na paskim modelu pamici (flat memory model), co oznacza, e pami aplikacji jest w sensie logicznym pojedynczym segmentem o rozmiarze 4 gigabajtw. Wszystkie rejestry segmentowe maj podczas pracy danej aplikacji t sam warto (selektor przyporzdkowanego jej segmentu), natomiast wskaniki (pointers) Object Pascala zrywaj z dotychczasow filozofi segment:offset i stanowi 32-bitowe offsety w 4-gigabajtowym segmencie. Implikuje to nieobecno w 32-bitowych wersjach Delphi takich elementw, jak CSeg, DSeg, SSeg, Seg, Ofs i SPtr; fragmenty kodu wykorzystujce pami segmentowan nie nadaj si wic do przeniesienia na grunt 32-bitowych wersji Delphi. W zwizku ze sposobem zarzdzania pamici (stronicowanie na danie paging on demand), stracio rwnie sens pojcie wolnej pamici czy, tym bardziej, spjnej wolnej pamici; nie maj wic sensu funkcje MemAvail()i MaxAvail(). Informacj o stanie pamici aplikacji mona uzyska przez wywoanie funkcji GetHeapStatus():
Function GetHeapStatus: THeapStatus;

gdzie typ THeapStatus jest rekordem w postaci:


type THeapStatus = record TotalAddrSpace: Cardinal;s TotalUncommitted: Cardinal; TotalCommitted: Cardinal; TotalAllocated: Cardinal; TotalFree: Cardinal; FreeSmall: Cardinal; FreeBig: Cardinal; Unused: Cardinal; Overhead: Cardinal; HeapErrorCode: Cardinal; end;

Informacj o wielkoci wykorzystanej przez aplikacj pamici (przydatn gwnie dla celw ledzenia) mona uzyska za pomoc funkcji TotalAllocated().

32-bitowe zasoby
Wszystkie 16-bitowe zasoby .RES oraz .DCR musz zosta przeksztacone na posta 32-bitow. Nie stanowi to wikszego problemu naley uruchomi edycj zasobu (na przykad za pomoc Image Edidtor lub Resource Workshop) i zapisa go w formacie 32-bitowym.

Kontrolki VBX
Poczwszy od Delphi 2, zarzucona zostaa obsuga kontrolek VBX z tej prostej przyczyny, i takiej obsugi nie zapewnia Win32, same za kontrolki uznane zostay przez Microsoft za przeytek. Jedyn wic drog do zaadaptowania kontrolki VBX na gruncie 32-bitowych wersji Delphi jest uzyskanie u producenta jej nowszej wersji w postaci kontrolki ActiveX.

Zmiany w zakresie funkcji API


Daleko idce zmiany w systemie operacyjnym nie mogy nie spowodowa zmian w zakresie podstawowych mechanizmw dostpu do usug systemowych, czyli repertuarze funkcji Win32 API. Niektre z 16-bitowych

201

funkcji straciy racj bytu, inne okazay si niezbyt wygodne, ponadto pojawiy si nowe funkcje, zwizane z nowymi moliwociami systemu. Waniejsze rnice zostay wyszczeglnione w poniszej tabeli.

Tabela 4.5. Przestarzae funkcje Windows 3.x i ich odpowiedniki w Win32 API Funkcja Win16 Odpowiednik w Win32
AccessResource() AllocDSToCSAlias() AllocResource() AllocSelector() ChangeSelector() CloseComm() DefineHandleTable() DeviceCapabilities() DeviceMode() DlgDirSelect() DlgDirSelectComboBox() ExtDeviceMode() FlushComm() FreeProcInstance() FreeSelector() GetAspectRatioFilter() GetBitmapDimension() GetBrushOrg() GetCodeHandle() GetCodeInfo() GetCommError() GetCurrentPDB()

brak brak brak brak brak


CloseHandle()

brak
DeviceCapabilitiesEx() DeviceModeEx() DlgDirSelectEx() DlgDirSelectComboBoxEx() ExtDeviceModeEx() PurgeComm()

brak brak
GetAspectRatioFilterEx() GetBitmapDimensionEx() GetBrushOrgEx()

brak brak
ClearCommError() GetCommandLine(), GetEnvironmentStrings()

GetCurrentPosition() GetEnvironment() GetFreeSpace() GetInstanceData() GetKBCodePage() GetMetafileBits() GetModuleUsage() GetTextExtent() GetViewportExt() GetViewportOrg() GetWindowExt() GetWindowOrg() GlobalCompact() GlobalDOSAlloc()

GetCurrentPositionEx()

Funkcje wejcia/wyjcia Win32


GlobalMemoryStatus()

brak brak
GetMetafileBitsEx()

brak
GetTextExtentPoint() GetViewportExtEx() GetViewportOrgEx() GetWindowExtEx() GetWindowOrgEx()

brak brak

202

GlobalDOSFree() GlobalFix() GlobalNotify() GlobalPageLock() GlobalUnfix() GlobalUnwire() GlobalWire() LocalCompact() LocalShrink() LockData() LockSegment() MakeProcInstance() MoveTo() OffsetViewportOrg() OffsetWindowOrg() OpenComm() ReadComm() ScaleViewportExt() ScaleWindowExt() SetBitmapDimension() SetEnvironment() SetMetafileBits() SetResourceHandler() SetSwapAreaSize() SetViewportExt() SetViewportOrg() SetWindowExt() SetWindowOrg() SwitchStackBack() SwitchStackTo() UngetCommChar() UnlockData() UnlockSegment() ValidateCodeSegments() ValidateFreeSpaces() WriteComm() Yield()

brak brak brak


VirtualLock()

brak brak brak brak brak brak brak brak


MoveToEx() OffsetViewportOrgEx() OffsetWindowOrgEx() CreateFile() ReadFile() ScaleViewportExtEx() ScaleWindowExtEx() SetBitmapDimensionEx()

Funkcje wejcia/wyjcia Win32


SetMetafileBitsEx()

brak brak
SetViewportExtEx() SetViewportOrgEx() SetWindowExtEx() SetWindowOrgEx()

brak brak brak brak brak brak brak


WriteFile() WaitMessage(), Sleep()

203

Podsumowanie
Uzbrojeni w wiedz przedstawion w niniejszym rozdziale moemy z mniejszym lub wikszym wysikiem adaptowa w rodowisku Delphi 6 aplikacje stworzone za pomoc wersji wczeniejszych. Kosztem dodatkowego nakadu pracy moemy te tworzy projekty zgodne jednoczenie z kilkoma wersjami Delphi.

204

Rozdzia 5.

Aplikacje wielowtkowe
Jedn z najwaniejszych cech 32-bitowej platformy Windows jest obsuga aplikacji wielowtkowych. Umoliwia wykorzystanie wszelkich zalet programowania wspbienego, upraszcza proces programowania i generalnie czyni aplikacje atwiejszymi w obsudze. W 16-bitowych wersjach Windows nie byo wielowtkowoci, dlatego jest ona jednym z gwnych czynnikw przemawiajcych za przenoszeniem aplikacji z Delphi 1 do wyszych, 32-bitowych wersji. W niniejszym rozdziale opiszemy mechanizmy Win32 API suce do realizacji aplikacji wielowtkowych oraz elementy Delphi stanowice odzwierciedlenie tych mechanizmw; przy okazji przedstawimy ograniczenia zwizane z programowaniem wspbienym w Delphi i postaramy si uzasadni ich przyczyny.

Natura wtkw
Wtek (thread) jest obiektem systemu operacyjnego, reprezentujcym wydzielon cz kodu w ramach procesu. Kada aplikacja Win32 posiada przynajmniej jeden wtek zwany wtkiem gwnym albo wtkiem pierwotnym (primary thread, default thread); aplikacja moe take posiada inne wtki, zwane wtkami pobocznymi lub drugorzdnymi (secondary threads). Mechanizm wtkw pozwala na niezalen, jednoczesn realizacj wielu rnych funkcji aplikacji; jednoczesno ta jednak jest pozorna, gdy w rzeczywistoci polega to na szybkim przeczaniu procesora midzy poszczeglnymi wtkami na tyle szybkim, i sprawia wraenie realizacji jednoczesnej (chyba e komputer wyposaony jest w kilka procesorw, ale to ju zupenie inna sprawa).
Wskazwka

Wielowtkowo jest cech rodowiska 32-bitowego nie istnieje ona (i nigdy nie bdzie istnie) w 16bitowych wersjach Windows. Wielowtkowe aplikacje tworzone w Delphi nigdy nie bd wic kompatybilne z Delphi 1.

Rodzaje wielozadaniowoci
Wielozadaniowo z wykorzystaniem wtkw jest czym zgoa innym ni wielozadaniowo (a waciwie jej namiastka) w 16-bitowym rodowisku Windows 3.x. W ramach Windows 3.x moliwe jest jednoczesne uruchamianie wielu aplikacji, trudno jednak mwi o cakowitym ich podporzdkowaniu systemowi 211

operacyjnemu. Aplikacja, otrzymawszy od systemu sterowanie, zyskuje tym samym kontrol nad czasem procesora i moe go zawaszczy do woli; takie zawaszczenie rozmylne lub niezamierzone, np. na skutek zaptlenia, zawsze paraliuje prac systemu, a czsto prowadzi do jego zaamania. Od aplikacji 16-bitowej wymaga si wic przestrzegania pewnych zasad wsppracy z innymi aplikacjami; z tego wzgldu wielozadaniowo Windows 3.x zostaa nazwana wielozadaniowoci kooperacyjn (cooperative multitasking). W Win32 wielozadaniowo ma cakowicie odmienny charakter. Obiektami ubiegajcymi si o czas procesora s nie zadania, lecz wanie wtki, nie to jest jednak najwaniejsze: znacznie istotniejsza jest niemono zmonopolizowania czasu procesora przez pojedynczy wtek. Otrzymuje on jedynie kwant czasu, po wykorzystaniu ktrego jest po prostu wywaszczany (bez ostrzeenia) przez system operacyjny. Mamy wic do czynienia z sytuacj, kiedy to system operacyjny ustala reguy gry, przydzielajc czas poszczeglnym wtkom i odbierajc im sterowanie, gdy uzna to za stosowne; tego typu wielozadaniowo zostaa nazwana wielozadaniowoci z wywaszczaniem (preemptive multitasking).

Do czego moe si przyda wielowtkowo?


Moliwo podziau aplikacji na niezalene wtki jest dla programisty (nie tylko w Windows) niezwykle atrakcyjna, i to z wielu wzgldw. Zalety wielowtkowoci staj si szczeglnie widoczne w przypadku, gdy aplikacja wykonuje jedn lub kilka akcji w tle, niezalenie od dialogu, ktry jednoczenie prowadzi z uytkownikiem w ramach swego interfejsu. Dobrym tego przykadem moe by obliczanie wartoci komrek arkusza kalkulacyjnego rwnolegle z wprowadzaniem nowych danych lub coraz powszechniejsze drukowanie wynikw aplikacji rwnolegle z innymi jej dziaaniami. Projektant aplikacji moe si skupi na dialogu z uytkownikiem uznajc, e caa reszta zostanie zaatwiona w ramach innych wtkw. Zreszt tak bardzo podana w procesie projektowania metoda dekompozycji problemw daje si bardzo atwo zrealizowa wanie dziki wielowtkowoci; mona wic powierzy poszczeglne aspekty aplikacji poszczeglnym jej wtkom, opracowywanym niezalenie od siebie, z uwzgldnieniem jedynie niezbdnej synchronizacji (o czym bdziemy pisa w dalszej czci rozdziau).

Wielowtkowo a komponenty VCL


Tak si jednak skada, i wiksza cz biblioteki VCL nie jest bezpieczna wtkowo (thread-safe) przy jej tworzeniu przyjto bowiem zaoenie, i w danej chwili dostp do komponentw ma co najwyej jeden wtek. Ograniczenie to dotyczy w wikszoci komponentw tworzcych interfejs uytkownika, chocia wiele innych komponentw take nie jest przystosowanych do dostpu wielowtkowego. Dla niektrych z nich VCL udostpnia wielowtkowe alternatywy na przykad TThreadList jest bezpieczn wtkowo odmian komponentu TList. Przykadem mechanizmu przystosowanego do wielowtkowoci jest natomiast strumieniowanie komponentw dopuszcza si odczyt (lub zapis) strumieni (np. plikw .DFM) jednoczenie przez kilka wtkw. W stosunku do komponentw tworzcych interfejs uytkownika obowizuje w VCL zastrzeenie, i ich obsuga moe si odbywa jedynie w kontekcie wtku gwnego aplikacji wanym wyjtkiem od tej zasady jest obiekt ptna (Canvas), ktry posiada wbudowane mechanizmy obsugi wielowtkowej. Nie oznacza to oczywicie cakowitego odizolowania wtkw pobocznych od komponentw, poniewa Delphi udostpnia narzdzia umoliwiajce modyfikowanie interfejsu uytkownika w kontekcie wtku gwnego, lecz z inicjatywy wtkw pobocznych. Nie zmienia to jednak faktu, i w warunkach aplikacji wielowtkowej obsuga interfejsu uytkownika musi by zrealizowana szczeglnie starannie.

Bdne wykorzystanie wielowtkowoci


Nadmiar dobrego czasami przeobraa si w zo; ta zasada ma zastosowanie rwnie w odniesieniu do wtkw Win32 API. Cho podzia aplikacji na niezalene wtki uwalnia programist od wielu problemw, to jednoczenie przysparza mu wielu nowych kopotw tyle e innego rodzaju. Przede wszystkim krytyczny staje si problem synchronizacji dwch lub kilku wtkw wykorzystujcych te same zasoby. Wyobra sobie wprowadzanie zmian do tekstu programu, ktry wanie jest kompilowany: jeli kompilator i edytor nie s nawzajem wiadome swoich skutkw, to taka sytuacja przypomina przestawienie zwrotnicy pod przejedajcym pocigiem. W tym szczeglnym przypadku rodki zaradcze s niemal banalne: 212

mona na przykad zablokowa moliwo zmian w module na czas jego kompilacji, mona te utworzy kopi moduu i potraktowa j jako wejcie dla kompilatora (jednak na czas kopiowania te trzeba zablokowa edycj), mona wreszcie ledzi postp kompilacji (w przypadku kompilatorw jednoprzebiegowych) i umoliwi edycj tylko tej czci tekstu, ktra zostaa ju skompilowana. Konkretne rozwizanie nie jest tu istotne, wane jest, aby nie traktowa wielowtkowoci jako panaceum na dotychczasowe problemy towarzyszce klasycznemu programowaniu sekwencyjnemu. Programowanie wspbiene, oferujc ogromne moliwoci i rozwizujc problemy, ktrych rozwizanie w ramach dotychczasowych rodkw mogo by jedynie poowiczne (lub adne bywa i tak), kryje jednoczenie wiele zdradliwych puapek, gdy korzystamy z niego w niewaciwy sposb.

Klasa TThread
Podstawow klas Delphi, implementujc mechanizmy charakterystyczne dla wtkw, jest klasa TThread. Chocia jej waciwoci i metody uwzgldniaj wikszo aspektw wielowtkowoci (rwnie tych specyficznych dla Delphi), to jednak w wielu wypadkach (jak pniej zobaczymy) konieczne staj si bezporednie odwoania do Win32 API: najbardziej oczywistym tego przykadem s mechanizmy synchronizacji wtkw, o ktrej przed chwil wspominalimy. Obecnie skoncentrujmy si jednak na samej klasie TThread; jej deklaracja, prezentowana poniej, znajduje si w module Classes.pas.

TThread = class private FHandle: THandle; {$IFDEF MSWINDOWS} FThreadID: THandle; {$ENDIF} {$IFDEF LINUX} // ** FThreadID is not THandle in Linux ** FThreadID: Cardinal; FCreateSuspendedSem: TSemaphore; FInitialSuspendDone: Boolean; {$ENDIF} FCreateSuspended: Boolean; FTerminated: Boolean; FSuspended: Boolean; FFreeOnTerminate: Boolean; FFinished: Boolean; FReturnValue: Integer; FOnTerminate: TNotifyEvent; FMethod: TThreadMethod; FSynchronizeException: TObject; FFatalException: TObject; procedure CheckThreadError(ErrCode: Integer); overload; procedure CheckThreadError(Success: Boolean); overload; procedure CallOnTerminate; {$IFDEF MSWINDOWS} function GetPriority: TThreadPriority; procedure SetPriority(Value: TThreadPriority); procedure SetSuspended(Value: Boolean);

213

{$ENDIF} {$IFDEF LINUX} // ** Priority is an Integer value in Linux function GetPriority: Integer; procedure SetPriority(Value: Integer); function GetPolicy: Integer; procedure SetPolicy(Value: Integer); procedure SetSuspended(Value: Boolean); {$ENDIF} protected procedure DoTerminate; virtual; procedure Execute; virtual; abstract; procedure Synchronize(Method: TThreadMethod); property ReturnValue: Integer read FReturnValue write FReturnValue; property Terminated: Boolean read FTerminated; public constructor Create(CreateSuspended: Boolean); destructor Destroy; override; procedure AfterConstruction; override; procedure Resume; procedure Suspend; procedure Terminate; function WaitFor: LongWord; property FatalException: TObject read FFatalException; property FreeOnTerminate: Boolean read FFreeOnTerminate write FFreeOnTerminate; property Handle: THandle read FHandle; {$IFDEF MSWINDOWS} property Priority: TThreadPriority read GetPriority write SetPriority; {$ENDIF} {$IFDEF LINUX} // ** Priority is an Integer ** property Priority: Integer read GetPriority write SetPriority; property Policy: Integer read GetPolicy write SetPolicy; {$ENDIF} property Suspended: Boolean read FSuspended write SetSuspended; {$IFDEF MSWINDOWS} property ThreadID: THandle read FThreadID; {$ENDIF} {$IFDEF LINUX} // ** ThreadId is Cardinal ** property ThreadID: Cardinal read FThreadID; {$ENDIF} property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate; end;

214

Jak wida, klasa TThread jest bezporednim potomkiem klasy TObject, wic obiekt klasy TThread nie jest komponentem i nie znajdziemy go w palecie komponentw. Liczne dyrektywy $IFDEF w deklaracji klasy wiadcz o tym, i jest ona klas uniwersaln w sensie zgodnoci z Delphi i z Kyliksem. Na uwag zasuguje take fakt, i metoda Execute(), realizujca wtek w sensie fizycznym, jest metod abstrakcyjn; oznacza to, i abstrakcyjna jest caa klasa TThread, a wic w konkretnej aplikacji musimy posugiwa si jej klasami pochodnymi, przedefiniowujcymi metod Execute() stosownie do specyfiki poszczeglnych wtkw. Najprostszym sposobem utworzenia nowej klasy wtku jest wybranie pozycji Thread Object z karty New okna New Items (rys. 5.1):

Rysunek 5.1. Definiowanie nowego wtku za pomoc repozytorium

Po wybraniu obiektu Thread Object Delphi wywietli pytanie o nazw tworzonej klasy; przyjmijmy, i jest ni TTestThread. Po wprowadzeniu nazwy Delphi utworzy nowy modu zawierajcy deklaracj nowej klasy z przedefiniowan metod Execute():

type TTestThread = class(TThread) private { Private declarations } protected procedure Execute; override; end;

Nie silc si w tym momencie na jaki wyrafinowany przykad, uczymy treci tej metody jakie proste obliczenia, na przykad takie:
procedure TTestThread.Execute; var k: integer;

215

begin for k := 1 to 2000000 do Inc( Answer, Round(Abs(Sin(Sqrt(k))))); end;

Umiemy teraz na formularzu przycisk, ktrego kliknicie spowoduje utworzenie obiektu zdefiniowanej klasy wtku:
procedure TForm1.Button1Click(Sender: TObject); var NewThread: TTestThread; begin NewThread := TTestThread.Create(False); end;

Pojedynczy parametr wywoania konstruktora klasy wtkowej okrela sposb postpowania z utworzonym obiektem wtku; jeeli ma warto False, wtek jest automatycznie uruchamiany, w przeciwnym razie wtek ten pozostaje w stanie zawieszenia jego uruchomienie nastpi dopiero w wyniku wywoania metody Resume(). Ta druga moliwo daje okazj do zmodyfikowania niektrych waciwoci obiektu wtkowego przed jego uruchomieniem. Modyfikowanie dziaajcego wtku jest w wielu przypadkach nieskuteczne, czsto te daje efekty rne od zamierzonych. Moliwo wstrzymywania zawieszonego wtku nie jest cech Delphi, lecz Win32; wstrzymanie takie nastpuje wwczas, gdy tworzca nowy wtek funkcja CreateThread() wywoana zostaje z parametrem CREATE_SUSPENDED. W procedurze TForm1.Button1Click parametr wywoania konstruktora ma warto False, zatem tworzony wtek jest automatycznie uruchamiany. atwo si wwczas przekona, i funkcjonowanie wtku pobocznego w niczym nie blokuje moliwoci manipulowania formularzem jego przemieszczania, minimalizacji, maksymalizacji, zmiany rozmiarw itp.

Obiekty wtkw a zmienne


Przyjrzyjmy si zmiennej lokalnej k w procedurze TTestThread.Execute() i zastanwmy si, co si stanie w przypadku rwnolegej pracy kilku egzemplarzy wtku TTestThread: czy bd one wsplnie wykorzystywa t zmienn, co, rzecz jasna, musiaoby doprowadzi do nieprzewidywalnych wynikw? Czy moe dostp do niej bdzie si odbywa wedug jakich priorytetw? Nic z tych rzeczy: kady wtek posiada wasny, oddzielny obszar stosu, a poniewa zmienne lokalne umieszczane s wanie na stosie, kady wtek bdzie si posugiwa wasn, oddzieln kopi zmiennej k. Jednak zupenie inaczej rzecz si ma ze zmiennymi globalnymi; ich rozczno musi by zapewniona za pomoc specjalnych rodkw, ktre opiszemy w dalszej czci rozdziau.

Koczenie wtku
Zasadnicza akcja wtku reprezentowanego przez obiekt klasy wtkowej rozgrywa si w ramach metody
Execute(), tote jej zakoczenie rwnowane jest zakoczeniu samego wtku. Po zakoczeniu wtku wywoywana jest funkcja Delphi o nazwie EndThread(), wywoujca z kolei funkcj API ExitThread()

zwalniajc przydzielony do wtku stos i zwizany z wtkiem obiekt Win32. Naley take zadba o zwolnienie obiektu klasy wtkowej w Delphi. Zwr uwag, i zwyke wywoanie jego metody Free() nie jest spraw atw, poniewa naleaoby uchwyci moment koczenia wtku; jest to moliwe dziki zdarzeniu OnTerminate, w ramach ktrego mona t metod wywoa. Delphi oferuje jednak jeszcze wygodniejsze rozwizanie tego problemu: ot ustawiajc na True waciwo FreeOnTerminate obiektu

216

wtkowego, zapewniamy jego automatyczne zwolnienie po zakoczeniu wtku. Najbardziej odpowiednim miejscem do ustawienia wspomnianej waciwoci jest oczywicie pocztek samej metody Execute():

procedure TTestThread.Execute; var k: integer; begin FreeOnTerminate := True; for k := 1 to 2000000 do Inc( Answer, Round(Abs(Sin(Sqrt(k))))); end;

Wskazwka

Obsuga zdarzenia OnTerminate odbywa si zawsze w kontekcie wtku gwnego aplikacji, a wic w ramach procedury zdarzeniowej dopuszczalne jest bezporednie manipulowanie komponentami VCL, bez koniecznoci posikowania si metod Synchronize(). Powrcimy do tego zagadnienia w dalszej czci rozdziau.

Ponadto, zgodnie z przyjt w Delphi konwencj, metoda Execute() powinna jak najczciej sprawdza warto waciwoci Terminate. Po stwierdzeniu, e waciwo ta ma warto True, metoda Execute() powinna jak najszybciej zakoczy sw prac. Oto przykad rozwizania czynicego zado temu wymaganiu:

procedure TTestThread.Execute; var k : integer; begin FreeOnTerminate := TRUE; For k := 1 to 2000000 do begin if Terminated Then Break; Inc(Answer, Round(Abs(Sin(Sqrt(k)))); end; end;

Na pierwszy rzut oka moe to wyglda na dodatkowe utrudnienie, jednak po chwilowym zastanowieniu okazuje si by do istotn zalet: wtek gwny, chcc wymusi zakoczenie wtku drugorzdnego, nie czyni tego bez ostrzeenia, przysowiowym strzaem w plecy, lecz informuje go o swych zamiarach, ustawiajc na True waciwo Terminate. Takie postpowanie daje szans wtkowi drugorzdnemu na wykonanie specyficznych dla niego funkcji zwizanych z zakoczeniem pracy, na przykad zamknicie plikw, czy te zwolnienie zarezerwowanych obszarw pamici operacyjnej.

217

Ostrzeenie:

Zdarza si jednak, e wtek drugorzdny nie przejmuje si zbytnio wartoci waciwoci Terminate i jego zakoczenie naley po prostu wymusi. Suy do tego funkcja API TerminateThread():

function TerminateThread(hThread: THandle; dwExitCode:DWORD): Boolean;

Pierwszy parametr jest tu uchwytem odnonego wtku, dostpnym w polu Handle obiektu wtkowego, drugi natomiast okrela kod zakoczenia wtku.

TerminateThread() jest funkcj bardzo niebezpieczn i powinna by stosowana tylko w ostatecznoci. Siowe zakoczenie wtku odbywa si przy cakowitej jego niewiadomoci, nie ma wic on moliwoci wykonania adnych czynnoci koczcych (nawet jeeli tre wtku ujta jest w ramy konstrukcji tryfinally). W szczeglnoci, jeeli dany wtek wykonywa jak sekcj krytyczn, pozostanie ona zablokowana przez cay czas realizacji procesu. Poza tym zakoczenie wtku odbywa si bez wiedzy wykorzystywanych przez niego bibliotek DLL (nie jest wywoywana ich procedura inicjujco-koczca z parametrem DLL_THREAD_DETACH). Wreszcie, jeeli w ramach koczonego wtku wykonywana bya jaka funkcja jdra systemu, moe ono pozosta w stanie nieokrelonym dla wszystkich pozostaych wtkw procesu.

Dodatkowo, w Windows NT/2000 obszar stosu przydzielony do wtku nie jest zwalniany i jest obszarem straconym do koca realizacji procesu (w Windows 95/98/Me funkcja TerminateThread() zwalnia stos przydzielony dla wtku).

Synchroniczne wykorzystywanie komponentw VCL


Jak ju wspominalimy, dostp do komponentw VCL moe odbywa si zasadniczo tylko w ramach wtku gwnego procesu. Pewna uciliwo takiego wymogu wydaje si oczywista, przedstawimy wic teraz drug stron tego medalu.

Zalety jednowtkowego interfejsu uytkownika


Win32 API wymaga dla kadego wtku osobnego okna gwnego oraz zwizanej z nim funkcji GetMessage(), odpowiedzialnej za obsug komunikatw. Gdyby wic dopuci rwnoprawny dostp do komponentw VCL ze strony wszystkich wtkw, atwo przewidzie, jak skomplikowaoby si wykorzystywanie biblioteki VCL w aplikacji, a co za tym idzie sama aplikacja. Z uwagi na serializacj obsugi komunikatw kady z nich pobierany jest z pojedynczej kolejki i przetwarzany w sposb kompletny przed pobraniem nastpnego zachowanie si aplikacji byoby uzalenione od kolejnoci przybywania poszczeglnych komunikatw do kolejki. Znalezienie ewentualnego bdu w tak skomplikowanych warunkach byoby znacznie trudniejsze ni w aplikacji jednowtkowej. Aby zapobiec opisanym trudnociom, naleaoby wyposay bibliotek VCL w mechanizmy synchronizujce dostp do komponentw ze strony poszczeglnych wtkw; jest ona jednak takich mechanizmw pozbawiona, std ograniczenie dostpu do komponentw wycznie dla wtku gwnego.

Metoda Synchronize()
Istnieje jednak pewna furtka, pozwalajca wtkom drugorzdnym na dostp do komponentw wykorzystywanych przez aplikacj. Jest ni moliwo wykonywania wybranej metody wtku drugorzdnego w kontekcie wtku gwnego procesu. Zadanie to wykonuje metoda Synchronize()klasy wtkowej okrelona nastpujco:
procedure Synchronize(Method: TThreadMethod);

Jedynym parametrem jej wywoania jest wybrana metoda obiektu, ktra ma by wykonana w kontekcie wtku gwnego; jak wynika z deklaracji, musi by ona bezparametrow procedur: 218

Type TThreadMethod = procedure of object;

Powrmy do naszego przykadu wtku wykonujcego czasochonne sumowanie. Tajemnicza zmienna Answer, wystpujca w procedurze Execute(), jest po prostu prywatnym polem testowego obiektu wtkowego. Po zakoczeniu sumowania chcielibymy wywietli jej zawarto w okrelonym polu formularza (Edit1.Text), jednak w wietle tego, co dotychczas napisalimy, wydaje si to niemoliwe, gdy wtek drugorzdny nie posiada dostpu do komponentw aplikacji. Wyjciem z tej sytuacji jest oczywicie zmiana waciwoci komponentu Edit1 w ramach metody wykonywanej w kontekcie wtku gwnego; na prezentowanym poniej wydruku metod t jest GiveAnswer(). Wydruk 5.1. Przykad wykorzystania metody Synchronize()
unit ThrdU;

interface

uses Classes;

type TTestThread = class(TThread) private Answer: integer; protected procedure GiveAnswer; procedure Execute; override; end;

implementation

uses SysUtils, Main;

{ TTestThread }

procedure TTestThread.GiveAnswer; begin MainForm.Edit1.Text := InttoStr(Answer); end;

procedure TTestThread.Execute; var k: Integer; begin FreeOnTerminate := True; for k := 1 to 2000000 do begin if Terminated then Break;

219

Inc(Answer, Round(Abs(Sin(Sqrt(k))))); Synchronize(GiveAnswer); end; end;

end.

Przyjrzyjmy si bliej dziaaniu metody Synchronize(). Podczas kadorazowego tworzenia nowego wtku, procedury biblioteki VCL tworz dla tego wtku ukryte okno (thread window), obsugiwane w kontekcie wtku gwnego; jego jedynym przeznaczeniem jest wanie wsppraca z procedur Synchronize(). Procedura ta, jako metoda obiektu, zapisuje w jego polu FMethod swj jedyny parametr i wysya pod adresem wspomnianego przed chwil okna komunikat CM_EXEPROC (zdefiniowany w ramach biblioteki VCL); w jego polu lParam przekazywany jest adres obiektu wtkowego (Self). Procedura komunikacyjna okna, otrzymawszy wspomniany komunikat, wywouje wskazan metod wtku (Self.FMethod); odbywa si to oczywicie w kontekcie wtku gwnego. Opisany scenariusz zosta zilustrowany schematycznie na rysunku 5.2.

Rysunek 5.2. Schemat dziaania metody TThread.Synchronize

Wykorzystanie komunikatw do synchronizacji wtkw


Alternatyw dla metody Synchronize() moe by zupene powstrzymanie wtku drugorzdnego od wykonywania jakichkolwiek procedur operujcych na komponentach VCL i ograniczenie jego roli do przekazywania wtkowi gwnemu (lub bezporednio komponentom formularza) jedynie komunikatw stanowicych polecenie wykonania odpowiednich czynnoci. Oto przykad wysania komunikatu bezporednio do kontrolki edycyjnej SomeEdit:
var S: String; begin S := 'Pozdrowienia z wtku drugorzdnego'; SendMessage(SomeEdit.Handle, WM_SETTEXT, 0, Integer(PChar(S))); end;

Przesyany komunikat WM_SETTEXT stanowi tu polecenie zmiany tekstu reprezentowanego przez kontrolk, ukrywajcego si (w Delphi) pod waciwoci Text. Adres nowej zawartoci (ktr jest acuch z zerowym ogranicznikiem) przekazywany jest w polu lParam komunikatu.

Przykadowa aplikacja wielowtkowa


Kompletnym przykadem aplikacji, ktra wykorzystuje wtek drugorzdny, jest projekt EzThrd.dpr, znajdujcy si na zaczonym krku CD-ROM. Wykorzystuje on prezentowany przed chwil modu

220

ThrdU.pas, zawierajcy definicj klasy TTestThread. Obsuga komponentu Memo1 odbywa si cakowicie w ramach wtku gwnego.

Formularz projektu jest przedstawiony na rysunku 5.3. Kliknicie przycisku Start spowoduje uruchomienie wtku pobocznego, wykonujcego sumowanie. W polu Odpowied wywietlana jest poprawnie tymczasowa warto sumy; za samo sumowanie w niczym nie koliduje z wprowadzaniem tekstu do Memo.

Rysunek 5.3. Formularz przykadowej aplikacji wielowtkowej Kod rdowy moduu formularza przedstawia wydruk 5.2. Wydruk 5.2. Modu gwny aplikacji ilustrujcej synchroniczny dostp do komponentw VCL
unit Main;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ThrdU;

type TMainForm = class(TForm) Edit1: TEdit; Button1: TButton; Memo1: TMemo; Label1: TLabel; Label2: TLabel; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end;

var MainForm: TMainForm;

implementation

221

{$R *.DFM}

procedure TMainForm.Button1Click(Sender: TObject); begin TTestThread.Create(False); end;

end.

Priorytety i szeregowanie wtkw


Jak ju wczeniej pisalimy, wtki s obiektami API ubiegajcymi si niezalenie o czas procesora. Rozdzia czasu i wielko przydzielanego kwantu uzalenione s od wartoci priorytetu wtku (priority), ktry w Win32 jest wypadkow dwch wielkoci: klasy priorytetowej procesu (priority class) oraz priorytetu wzgldnego wtku w ramach procesu.

Klasa priorytetowa procesu


Klasa priorytetowa okrela stopie preferencji procesu (jako caoci) podczas ubiegania si jego poszczeglnych wtkw o czas procesora. W Win32 procesowi moe by przydzielona jedna z czterech nastpujcych klas priorytetowych: Jaowy (Idle) proces opatrzony t klas priorytetow otrzymuje czas procesora jedynie wtedy, gdy nie potrzebuj go procesy o wyszej klasie priorytetowej. Przykadem procesw posiadajcych t klas priorytetow s wygaszacze ekranu. Normalny (Normal) jest to domylna klasa priorytetowa, przypisywana przez system wszystkim procesom uruchamianym z pulpitu lub wiersza polece. Wysoki (High) klas t opatruje si procesy, ktre powinny otrzymywa czas procesora tak szybko, jak tylko jest to moliwe. Proces z t klas priorytetow jest zdolny do wywaszczania z czasu procesora procesw z klasami Idle i Normal. Przykadem tego typu procesu jest lista zada Windows, ktra niezalenie od innych czynnoci musi ukaza si uytkownikowi niezwocznie. Nie naley opatrywa t klas procesw wykonujcych intensywne obliczenia, gdy grozi to sparaliowaniem m.in. procesw o klasie Normal, a wic np. uruchamianych z pulpitu. Czasu rzeczywistego (Realtime) wtki procesu posiadajcego t klas priorytetow zdolne s do wywaszczania wtkw wszystkich innych procesw, wcznie z procesami systemowymi. Zmonopolizowanie procesora przez wtek procesu o tak wysokim priorytecie moe stanowi zagroenie dla normalnego funkcjonowania systemu i spowodowa np. zablokowanie zapisywania zawartoci buforw dyskowych albo nieczuo na operowanie mysz bd klawiatur, dlatego te klas t rezerwuje si dla procesw wykonujcych ekstremalnie krtkie czynnoci, stanowice reakcj na zachodzce zdarzenia gwnie procesw systemowych.

W Windows 2000/XP dostpne s ponadto klasy priorytetowe Podnormalny (Below Normal) i Nadnormalny (Above Normal), nie uwzgldnia ich jednak modu Windows.pas w Delphi 6.

Numeryczne wartoci priorytetu kadej z wymienionych klas oraz zwizane z nimi identyfikatory Win32 przedstawia tabela 5.1.

222

Tabela 5.1. Klasy priorytetowe procesu Klasa priorytetowa Idle Below Normal Normal Above Normal High Realtime Flaga Win32
IDLE_PRIORITY_CLASS

Warto
$40

BELOW_NORMAL_PRIORITY_CLASS $4000 NORMAL_PRIORITY_CLASS $20

ABOVE_NORMAL_PRIORITY_CLASS $8000 HIGH_PRIORITY_CLASS REALTIME_PRIORITY_CLASS $80 $100

Domylnie, procesy uruchamiane z poziomu pulpitu lub wiersza polece otrzymuj klas priorytetow Normal. Dla procesu tworzonego w sposb jawny (tj. za pomoc funkcji CreateProcess()) moliwe jest take jawne okrelenie klasy priorytetowej suy do tego szsty parametr wywoania dwCreationFlags; poszczeglnym klasom priorytetowym odpowiadaj flagi wymienione w tabeli 5.2. Win32 umoliwia ponadto odczyt oraz dynamiczn zmian aktualnej klasy priorytetowej wskazanego procesu. Su do tego funkcje (odpowiednio) GetPriorityClass() i SetPriorityClass():
function GetPriorityClass(hProcess: THandle): DWORD; stdcall; function SetPriorityClass(hProcess: THandle; dwPriorityClass:DWORD): BOOL; stdcall;

Parametr hProcess jest tutaj tzw. pseudouchwytem (pseudo-handle) odnonego procesu kady proces moe uzyska swj wasny pseudouchwyt wywoujc funkcj GetCurrentProcess():
function GetCurrentProcess: THandle; stdcall;

Pseudouchwyt stanowi swego rodzaju odnonik do normalnego uchwytu nie jest wic odrbnym obiektem i nie podlega zamykaniu przez CloseHandle() (prba zamknicia pseudouchwytu nie powoduje w Win32 adnego efektu). Jeli wic chciaby nada swej aplikacji klas priorytetow (powiedzmy) High, to stosowne wywoanie miaoby nastpujc posta:
if not SetPriorityClass(GetCurrentProcess, HIGH_PRIORITY_CLASS) Then ShowMessage('Bd nadawania klasy priorytetowej');

Notatka

Modyfikacja klasy priorytetowej procesu w Windows NT/2000 wymaga okrelonych przywilejw procesowych. Domylne dla aplikacji klasy priorytetowe mog by ponadto zmienione przez administratora, zwaszcza w intensywnie obcionych serwerach Windows NT/2000.

Nie naley naduywa klasy priorytetowej Realtime. Poniewa wtki systemu operacyjnego funkcjonuj w warunkach niszego priorytetu, przypisanie tak wysokiego priorytetu aplikacji moe niekiedy doprowadzi do sparaliowania systemu operacyjnego, przez nadmierne spowolnienie (lub wrcz zatrzymanie) obsugi komunikatw czy operacji wejcia-wyjcia.

Nawet jednak ustawienie tylko klasy High moe powodowa problemy, jeeli wtki procesu opatrzonego t klas nie pozostaj przez wikszo czasu w stanie oczekiwania, a dokonuj intensywnych oblicze. Zalety wielozadaniowoci z wywaszczaniem mog wwczas szybko przeobrazi si w wady.

223

Priorytet wzgldny wtku


Idea okrelania wzgldnych priorytetw wtkw ma na celu zrnicowanie w r a mac h p r o ce s u pilnoci poszczeglnych wtkw w ubieganiu si o czas procesora; w przyblieniu priorytety wzgldne s dla wtkw danego procesu tym, czym klasa priorytetowa dla poszczeglnych procesw w systemie. Danemu wtkowi mona przypisa jeden z siedmiu nastpujcych priorytetw wzgldnych: jaowy (tpIdle), niski (tpLowest), podnormalny (tpBelowNormal), normalny (tpNormal), nadnormalny (tpAboveNormal), wysoki (tpHighest), krytyczny (tpTimeCritical).

Podobnie jak kada z klas priorytetowych procesu, tak kady z priorytetw wzgldnych wtku posiada okrelon wag; dodajc j do klasy priorytetowej procesu, otrzymujemy ostateczn warto okrelajc priorytet wtku w ubieganiu si o czas procesora (z tego wzgldu priorytet wzgldny wtku bywa czsto okrelany mianem priorytetu delta ang. delta-priority). Wagi reprezentujce poszczeglne kategorie priorytetu wzgldnego, wraz z symbolicznymi oznaczeniami tych kategorii w Win32, przedstawia tabela 5.2.

Tabela 5.2. Priorytety wzgldne wtku Priorytet


tpIdle tpLowest tpLower tpNormal tpHigher tpHighest tpTimeCritical

Flaga Win32
THREAD_PRIORITY_IDLE THREAD_PRIORITY_LOWEST THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_HIGHEST THREAD_PRIORITY_TIME_CRITICAL

Waga liczbowa
-15 -2 -1 0 1 2 15

W pewnych szczeglnych przypadkach ostateczny priorytet wtku jest jednak inny ni suma klasy priorytetowej procesu i priorytetu wzgldnego wtku, mianowicie: Wtek o priorytecie wzgldnym tpIdle, nalecy do procesu o klasie priorytetowej innej ni RealTime, posiada ostateczny priorytet rwny 1. Wtek o priorytecie wzgldnym tpIdle, nalecy do procesu o klasie priorytetowej RealTime, posiada ostateczny priorytet rwny 16. Wtek o priorytecie wzgldnym tpTimeCritical, nalecy do procesu o klasie priorytetowej innej ni RealTime, posiada ostateczny priorytet rwny 15. Wtek o priorytecie wzgldnym tpTimeCritical, nalecy do procesu o klasie priorytetowej RealTime, posiada ostateczny priorytet rwny 31.

224

Zawieszanie i wznawianie wtkw


Przy okazji omawiania konstruktora TThread.Create() zwrcilimy uwag na to, e utworzony wtek moe pozostawa w zawieszeniu; procedur wyprowadzajc go z tego stanu jest metoda Resume(). Czynno odwrotn zawieszenie dziaajcego wtku realizuje metoda Suspend(). Z kadym wtkiem, jako obiektem Win32, zwizany jest tzw. licznik zawiesze (suspend counter). Jeeli ma niezerow warto, wtek pozostaje w zawieszeniu. Zwikszaniem tego licznika zajmuje si metoda Suspend(), za zmniejszaniem metoda Resume() (jeeli metoda Resume() napotka zerow warto licznika, nic si nie dzieje). Konstruktor klasy TThread ustawia pocztkowo warto wspomnianego licznika na 0, a jeeli parametr wywoania tego konstruktora ma warto True, wywoywana jest automatycznie metoda Suspend(). Wynika std wniosek, i wywoanie metody Resume() nie zawsze wyprowadza wtek ze stanu zawieszenia.

Pomiar czasu w ramach wtku


W rodowisku Windows 3.x pomiar czasu nie stanowi adnego problemu proces nie mg zosta wywaszczony przez system, a wic mierzony czas rzeczywisty tosamy by z czasem powiconym danemu procesowi przez procesor centralny. Przykadowa sekwencja rozkazw, dokonujca pomiaru czasu w tych warunkach, moga wyglda mniej wicej tak:
{WIN16 API} var StartTime, Total : Longint; begin ... StartTime := GetTickCount; ... // tutaj biegn obliczenia, ktrych czas mierzymy Total := GetTickCount - StartTime;

W rodowisku wielozadaniowym stosujcym wywaszczanie nie mona jednak utosamia czasu rzeczywistego z czasem przeznaczonym dla danego wtku, gdy kada sekwencja rozkazw moe by w dowolnej chwili przerwana przez system w celu przekazania procesora innemu wtkowi. Sprawa ta jest o tyle smutna, e Windows 95/98 nie posiadaj adnego mechanizmu pozwalajcego mierzy czas biegncej aplikacji! W Windows NT/2000/XP istnieje natomiast funkcja o nazwie GetThreadTimes(). Jej nagwek ma nastpujc posta:
function GetThreadTimes(hThread: THandle; var lpCreationTime, lpExitTime, lpKernelTime, lpUserTime:TFileTime): BOOL; stdcall;

Parametr hThread jest uchwytem identyfikujcym wtek, a pozostae parametry, po pomylnym wykonaniu funkcji, zawieraj informacj zgodnie z ponisz tabel; wynik funkcji informuje, czy jej wykonanie zakoczyo si pomylnie.
lpCreationTime lpExitTime

Czas utworzenia wtku Czas zakoczenia realizacji wtku; jeeli wtek wci dziaa, to warto ta jest nieokrelona Czas realizacji usug systemowych na rzecz wtku Czas realizacji kodu wtku poza usugami systemowymi

lpKernelTime lpUserTime

225

Zwracane przez funkcje API wskazania czasu s wartociami 64-bitowymi, liczonymi w jednostkach 100nanosekundowych. Poniewa jednak 64-bitowe liczby cakowite pojawiy si (jako rodzimy typ Object Pascala) dopiero w Delphi 4, wartoci te rozdzielane s pomidzy dwie liczby 32-bitowe, stanowice pola nastpujcego rekordu:

TFileTime = record dwLowDateTime :DWORD //cz mniej znaczca

dwHighDateTime :DWORD //cz bardziej znaczca end;

Typ TFileTime uywany jest te do oznaczania momentu utworzenia (modyfikacji) pliku; zerowa warto obydwu pl oznacza pnoc rozpoczynajc dzie 1 stycznia 1601 roku (trudno byoby wic reprezentowa w tej konwencji czas rozpoczcia np. bitwy pod Grunwaldem), jednostka ma oczywicie warto 100 nanosekund.

Wskazwka

Dysponujc ju typem Int64, reprezentujcym 64-bitow liczb ze znakiem, moemy z powodzeniem 1 dokonywa rzutowania na typu TFileTime , co upraszcza porwnywanie wskaza czasowych, np. var UserTine, KernelTime: TFileTime; ... if Int64(UserTime) > Int64(KernelTime) then Beep;

Dla przypomnienia reprezentacja czasu w ramach standardowego dla Delphi typu TDateTime jest kracowo odmienna: czas reprezentowany jest jako liczba zmiennoprzecinkowa typu double
type TDateTime = type Double;

zawierajca ilo dni (ilo, nie liczb, wynik bowiem niekoniecznie jest liczb cakowit), ktre upyny od pnocy rozpoczynajcej dzie 30 grudnia 1899 roku2; tej wanie chwili odpowiada warto zero, chwile wczeniejsze reprezentowane s przez wartoci ujemne. Konwersj pomidzy obydwiema reprezentacjami TFileTime i TDateTime wykonuj ponisze funkcje:
Function FileTimeToDateTime(FileTime: TFileTime): TDateTime; var SysTime: TSystemTime; begin if not FileTimetoSystemTime(FileTime, SysTime) Then raise EConvertError.CreateFmt ('Bd konwersji FileTimetoSystemTime kod bdu %d', with SysTime do [GetLastError]);

Naley jednak uwaa, by nie ustawi wyrwnywania pl rekordw (record field alignment) na warto 8, gdy wwczas pomidzy polami rekordu TFileTime mogyby pojawi si luki (przyp. tum.).

W Delphi 1 jest jednak inaczej zerowa warto TDateTime reprezentuje pocztek naszej ery, czyli pnoc rozpoczynajc dzie 1 stycznia roku 1 (przyp. tum.).

226

Result := EncodeDate(wYear, wMonth, wDay) + EncodeTime (wHour, wMinute, wSecond, wMilliSeconds); end;

Function DateTimeToFileTime(DateTime: TDateTime): TFileTime; var SysTime: TSystemTime; begin with Systime do begin DecodeDate(DateTime, wYear, wMonth, wDay); DecodeTime(DateTime, wHour, wMinute, wSecond, wMilliseconds); wDayOfWeek := DayOfWeek(DateTime); end;

if not SystemTimeToFileTime(SysTime, Result) then raise EConvertError.CreateFmt ('Bd konwersji SystemTimeToFileTime kod bdu %d', [GetLastError]); end;

Wskazwka

Przypominamy, i funkcja GetThreadTime() nie jest zaimplementowana w Windows 95/98 jej wywoanie daje wynik False i nieokrelone wartoci parametrw.

Wspdziaanie wtkw aplikacji


Wraz z pojawieniem si wielowtkowoci (i w ogle programowania wspbienego) ujawnio si wiele problemw przedtem niespotykanych. Podczas analizy kodu poszczeglnych wtkw nie sposb ju okreli zachowania si aplikacji tak jednoznacznie, jak w przypadku aplikacji jednowtkowych. Oddziaywanie na siebie poszczeglnych wtkw uwidacznia si gwnie w dwch aspektach przetwarzania: wspdzieleniu globalnych zasobw oraz synchronizacji okrelonych czynnoci. W warunkach niekontrolowanego dostpu wtkw do danego zasobu, jego stan zaleny jest na og od wzgldnej szybkoci tych wtkw; ponadto niektre cigi operacji dotyczcych zasobu musz by wykonane w sposb niepodzielny, pod grob utraty jego integralnoci. Czynnoci wykonywane przez poszczeglne wtki procesu bywaj w mniejszym lub wikszym stopniu uzalenione od siebie w konkretnym miejscu konkretnego wtku, przed kontynuacj przetwarzania, konieczne moe by zaoenie, i inny wtek wykona ju okrelon czynno. Takie zaoenie musi by oczywicie oparte na rzetelnych podstawach, ktrych dostarczy mog jedynie niezawodne narzdzia systemowe. Zobaczmy, jak z opisanymi problemami radz sobie Delphi i Win32 API.

Pami lokalna wtku


Kady wtek reprezentuje (w pewnym sensie) niezalen cz przetwarzania, powinien zatem uywa wasnego zestawu zmiennych roboczych. Taka rozczno zapewniona jest w sposb automatyczny, jeeli zmienne te stanowi pola obiektu reprezentujcego wtek w rozpatrywanym niedawno obiekcie 227

TTestThread polem takim jest Answer. Nie ma rwnie problemu z lokalnymi zmiennymi procedur i funkcji egzystuj one na stosie, za kady wtek posiada swj prywatny stos.

Ze zmiennymi globalnymi sprawa jest nieco bardziej zoona, poniewa ich rozczno (dla poszczeglnych wtkw) musi by zapewniona za pomoc specjalnych rodkw, na szczcie bardzo atwych w uyciu.

Pami obiektu wtkowego


Najwygodniejszym i najbardziej zgodnym z obiektow filozofi Delphi jest przechowywanie prywatnych danych wtku w polach jego obiektu wtkowego, jak w poniszym przykadzie:

type TMyThread = class(TThread) private FLocalInt : integer; FLocalStr: String; end;

Oprcz tego, e rozwizanie takie jest klarowne, jest ponadto bardziej efektywne od mechanizmw oferowanych przez Win32 API, dostpnych w Delphi za porednictwem dyrektywy threadvar dostp do pl obiektu wtkowego jest rednio 10 razy szybszy ni dostp do tzw. pamici TLS (thread-local storage). Ponadto zmienne, ktrych warto istotna jest jedynie w obrbie okrelonych metod obiektu wtkowego, powinny by zmiennymi lokalnymi tyche metod dostp do zmiennych lokalnych jest jeszcze szybszy ni dostp do pl obiektu.

threadvar pami wtkowa Win32 API


Jak ju wczeniej wspominalimy, zmienne globalne aplikacji wspdzielone s przez jej poszczeglne wtki, a to rodzi okrelone konsekwencje typowe dla wspdzielenia zasobw, ktrymi przecie s wspomniane zmienne. Spjrzmy na poniszy fragment:
var GlobalStr: String; ... procedure SetShowStr(const S: String); begin if S = '' Then MessageBox(0, PChar(GlobalStr), 'Warto zmiennej globalnej wynosi', MB_OK.) Else GlobalStr := S; end;

Dziaanie powyszej procedury nie wymaga komentarzy, jeeli jest ona wykonywana w ramach tylko jednego wtku: jej wywoanie z pustym parametrem spowoduje wywietlenie zawartoci zmiennej globalnej GlobalStr, za wywoanie z parametrem niepustym spowoduje przypisanie jego wartoci do zmiennej GlobalStr. Jeeli jednak procedura SetShowStr() wykorzystywana jest przez kilka wtkw, to jej wykonywanie w ramach jednego wtku moe by przerwane przez inny wtek, ktry nieoczekiwanie zmieni warto zmiennej GlobalStr. W charakterze przykadu przeanalizujmy wspdziaanie dwch wtkw: niech pierwszy z nich wykonuje tak oto sekwencj:

SetShowStr('Biay'); SetShowStr('');

228

drugi za nastpujc:
SetShowStr('Czarny'); SetShowStr('');

Zamy teraz, i pierwszy wtek, po wykonaniu pierwszej instrukcji (SetShowStr('Biay')) zostanie wywaszczony przez system operacyjny z czasu procesora, po czym drugi wtek wykona sw pierwsz instrukcj SetShowStr('Czarny'). Jeeli teraz nastpi wywaszczenie drugiego wtku i pierwszy wtek wykona sw drug instrukcj (SetShowStr('')), wywietlony przeze komunikat informowa bdzie, i zawartoci zmiennej GlobalStr jest 'Czarny' gdy tak warto ustali przed chwil drugi wtek. Z punktu widzenia pierwszego wtku to kompletne zaskoczenie, z punktu widzenia wielowtkowoci zwyczajna rzecz, gdy nie kontroluje si dostpu do wsplnych zasobw. Za pomoc mechanizmu Win32, zwanego w skrcie TLS (thread-local storage), moliwe jest jednak takie zadeklarowanie zmiennej globalnej, by kady z wtkw posugiwa si jej oddzielnym egzemplarzem. Zadanie to spenia w Object Pascalu dyrektywa threadvar, zastpujca w takiej sytuacji tradycyjn dyrektyw var:
threadvar GlobalStr: String;

Wskazwka

Jeeli interesuje Ci realizacja pamici TLS w Win32 API, czyli to, co kryje si pod magiczn dyrektyw threadvar, zajrzyj na strony 884891 ksiki Delphi 3. Ksiga eksperta, wyd. HELION 1998 (przyp. tum.).

Opisywan sytuacj ilustruje projekt o nazwie TLS.dpr znajdujcy si na zaczonym krku CD-ROM; jego modu gwny prezentujemy na wydruku 5.3. Na formularzu projektu znajduje si pojedynczy przycisk. Kliknicie go powoduje dwukrotne wywoanie procedury SetShowStr() w celu nadania wartoci zmiennej GlobalStr, a nastpnie jej wywietlenia. W tym momencie startuje drugi wtek, wykonujc analogiczne czynnoci w odniesieniu do tej samej zmiennej GlobalStr. Gdy wykonana zostanie ostatnia instrukcja procedury Button1Click, wywietlona warto zmiennej GlobalStr niekoniecznie bdzie miaa warto nadan jej w pierwszej instrukcji (z powodw wczeniej opisanych). Jeeli jednak zadeklarujemy zmienn GlobalStr jako threadvar, opisany problem nie wystpi, bo kady z wtkw posugiwa si bdzie oddzielnym egzemplarzem tej zmiennej.

Wydruk 5.3. Ilustracja lokalnej pamici wtku


unit Main;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;

type TMainForm = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations }

229

end;

var MainForm: TMainForm;

implementation

{$R *.DFM}

{ NOTE: zmie dyrektyw "var" na "threadvar", by zobaczy rnic } var //threadvar GlobalStr: string;

type TTLSThread = class(TThread) private FNewStr: String; protected procedure Execute; override; public constructor Create(const ANewStr: String); end;

procedure SetShowStr(const S: String); begin if S = '' then MessageBox(0, PChar(GlobalStr), 'Warto zmiennej globalnej wynosi...', MB_OK) else GlobalStr := S; end;

constructor TTLSThread.Create(const ANewStr: String); begin FNewStr := ANewStr; inherited Create(False); end;

procedure TTLSThread.Execute; begin FreeOnTerminate := True; SetShowStr(FNewStr); SetShowStr(''); end;

procedure TMainForm.Button1Click(Sender: TObject);

230

begin SetShowStr('Cze'); SetShowStr(''); TTLSThread.Create('A kuku!'); Sleep(100); SetShowStr(''); end;

end.

Wskazwka

W powyszym przykadzie pomoglimy nieco przypadkowi, zwikszajc prawdopodobiestwo ingerencji drugiego wtku w prac pierwszego. Zastosowana przez nas procedura Sleep()

procedure Sleep(dwMilliseconds:DWORD); stdcall;

symuluje upienie wtku na wskazany interwa czasowy, a dokadniej stanowi ona informacj dla systemu, i nie naley w tym czasie przydziela wtkowi czasu procesora. W warunkach pracy wielowtkowej wprowadza to dodatkowy element losowoci poniewa niepodobna okreli, co dzieje si z pozostaymi wtkami w momencie obudzenia upionego wtku.

Czsto stosowan praktyk jest wywoywanie procedury Sleep() z zerow wartoci argumentu cho nie powoduje ono upienia wtku, to prawie na pewno spowoduje jego chwilowe wywaszczenie z czasu procesora na rzecz innego wtku o podobnym lub wyszym priorytecie.

Efekt wspomnianej losowoci uwidacznia si jeszcze wyraniej wwczas, gdy przeniesiemy dan aplikacj na (znaczco) wolniejszy lub (znaczco) szybszy komputer. Wypywa std wniosek, i procedura Sleep() nie zapewnia synchronizowania wtkw nawet jeeli wydaje si, i w czasie odpoczywania pierwszego wtku drugi wtek zdy ju co zrobi, zaoenie takie wcale nie musi by prawdziwe.

Synchronizacja wtkw
Jak wspominalimy na wstpie, niekontrolowany dostp do wsplnie wykorzystywanych zasobw moe dawa nieoczekiwane efekty; losowe zachowanie si ostatniej aplikacji byo tego dobrym przykadem. W przykadzie tym problem wsplnego wykorzystywania zasobu zosta nie tyle rozwizany, co usunity zastpienie deklaracji var przez threadvar spowodowao rozszczepienie zmiennej GlobalStr na dwa niezalene egzemplarze, bo celem aplikacji byo zademonstrowanie wykorzystania lokalnej pamici wtku, nie za wspdzielenia zasobu. Zajmiemy si teraz problemem synchronizacji wtkw w cisym tego sowa znaczeniu. Zamy mianowicie funkcjonowanie dwch wtkw, z ktrych pierwszy wczytuje zawarto pliku dyskowego do pamici, drugi natomiast zlicza wszystkie wystpienia (we wczytanym fragmencie) znaku o kodzie (na przykad) 128. Jest oczywiste, i zliczanie nie moe rozpocz si wczeniej, ni w momencie, gdy pierwszy wtek wczyta ca zawarto pliku; poniewa jednak obydwa wtki traktowane s przez system operacyjny cakowicie niezalenie, takiej gwarancji (domylnie) nie ma i synchronizacj obydwu wtkw naley zapewni w sposb jawny. Win32 API udostpnia cztery mechanizmy synchronizacji wtkw: sekcje krytyczne (critical sections), wykluczenia wzajemne zwane te muteksami (mutexes), semafory (semaphores) i zdarzenia (events). Aby zademonstrowa ich wykorzystanie, rozpatrzmy przykadowy projekt, w ktrym dwa wtki drugorzdne w sposb niekontrolowany zapeniaj tablic kolejnymi liczbami cakowitymi; po zakoczeniu obydwu wtkw

231

zawarto tablicy jest wywietlana na ekranie przez wtek gwny. Projekt ten znajduje si na zaczonym krku CD-ROM pod nazw NoSynch.dpr jego modu gwny prezentujemy na wydruku 5.4.

Wydruk 5.4. Niekontrolowane wspdziaanie wtkw


unit Main;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;

type TMainForm = class(TForm) Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private procedure ThreadsDone(Sender: TObject); end;

TFooThread = class(TThread) protected procedure Execute; override; end;

var MainForm: TMainForm;

implementation

{$R *.DFM}

const MaxSize = 128;

var NextNumber: Integer = 0; DoneFlags: Integer = 0; GlobalArray: array[1..MaxSize] of Integer;

function GetNextNumber: Integer; begin Result := NextNumber; Inc(NextNumber); end; // zwr warto zmiennej globalnej // i zwiksz j

232

procedure TFooThread.Execute; var i: Integer; begin OnTerminate := MainForm.ThreadsDone; for i := 1 to MaxSize do begin GlobalArray[i] := GetNextNumber; Sleep(3+random(12)); end; end; // przypisz warto elementowi tablicy // pozwl dziaa drugiemu wtkowi

procedure TMainForm.ThreadsDone(Sender: TObject); var i: Integer; begin Inc(DoneFlags); if DoneFlags = 2 then for i := 1 to MaxSize do { wypenij list elementami tablicy } Listbox1.Items.Add(IntToStr(GlobalArray[i])); end; // upewnij si, e obydwa wtki zakoczyy si

procedure TMainForm.Button1Click(Sender: TObject); begin // utwrz i uruchom obydwa wtki TFooThread.Create(False); TFooThread.Create(False); end;

end.

Kady z wtkw dokonuje wypeniania tablicy GlobalArray kolejnymi liczbami cakowitymi; poniewa jednak obydwa wtki wykorzystuj t tablic w sposb cakowicie niesynchroniczny, jej kocowa zawarto jest rna od oczekiwanej, o czym wiadczy lista na formularzu (rys. 5.4).

233

Rysunek 5.4. Rezultat niesynchronizowanego wypeniania tablicy I nie moe by inaczej, dopki nie wprowadzimy jakichkolwiek mechanizmw synchronizacyjnych. Istotnie, co najmniej dwa zasoby zmienna NextNumber oraz tablica GlobalArray wykorzystywane s tu w sposb niekontrolowany przez obydwa wtki, ktrych losowe zachowanie zostao dodatkowo spotgowane wplatanym oczekiwaniem (o losowej dugoci) po wpisaniu wartoci do kolejnego elementu.

Sekcje krytyczne
Opisany chaos w tablicy z poprzedniego przykadu z pewnoci udaoby si wyeliminowa, gdybymy pozwolili kademu z wtkw wykona swoj prac w spokoju, bez ingerencji ze strony drugiego wtku. Na przykad, pierwszy wtek mgby zapenia tablic liczbami od 0 do 127, drugi liczbami od 128 do 255; na formularzu ujrzelibymy oczywicie t drug zawarto. Najprostszym narzdziem Win32 zapewniajcym opisan wyczno s sekcje krytyczne (critical sections). Fragment kodu stanowicy sekcj krytyczn wykonywany jest w danej chwili przez co najwyej jeden wtek inne wtki nie maj w tym czasie dostpu do tego fragmentu. Wracajc do poprzedniego projektu, nietrudno skonstatowa, i w ramy sekcji krytycznej powinna zosta ujta ptla zapeniajca elementy tablicy, zawarta w procedurze TFooThreadExecute(). Sekcja krytyczna w Win32 ma posta rekordu TRTLCriticalSection. Jego szczegowa struktura nie jest tu istotna, znacznie waniejsze s natomiast cztery podstawowe operacje z jego udziaem. Pierwsz operacj wykonywan na sekcji krytycznej jest jej zainicjowanie, wykonywane przez nastpujc procedur API:
procedure InitializeCriticalSection(var lpCriticalSection:TRTLCriticalSection);stdcall;

Rozpoczcie wykonywania krytycznego fragmentu kodu musi by poprzedzone wejciem do sekcji krytycznej ( entering critical section). Jest ono realizowane przez procedur EnterCriticalSection():
procedure EnterCriticalSection(var lpCriticalSection:TRTLCriticalSection);stdcall;

W danej chwili wewntrz okrelonej sekcji krytycznej moe przebywa co najwyej jeden wtek pozostae wtki zamierzajce do niej wej (cilej te, ktre wywoaj w stosunku do niej procedur EnterCriticalSection()), zostan przez system zawieszone. Po zakoczeniu wykonywania krytycznego fragmentu kodu wtek znajdujcy si w sekcji krytycznej musi dokona wyjcia z niej (leaving critical section) przez wywoanie nastpujcej procedury:
procedure LeaveCriticalSection(var lpCriticalSection: TRTLCriticalSection);stdcall;

Umoliwi to wejcie do sekcji krytycznej ktremu z oczekujcych wtkw. Ostatni operacj dotyczc sekcji krytycznej jest jej zwolnienie, gdy nie jest ju duej potrzebna; suy do tego nastpujca procedura:
procedure DeleteCriticalSection(var lpCriticalSection: TRTLCriticalSection);stdcall;

234

Wskazwka

Microsoft konsekwentnie ukrywa struktur rekordu TRTLCriticalSection, poniewa zmienia si ona w zalenoci od platformy sprztowej, a wic uzalenienie aplikacji od jej konkretnej postaci mogoby powodowa problemy. W systemach intelowskich sekcja krytyczna zawiera licznik, pole przechowujce uchwyt przebywajcego w niej wtku i (ewentualnie) wskanik do procedury obsugi zdarze systemowych. Komputery serii Alpha posuguj si wasn postaci sekcji krytycznej o nazwie spinlock, znacznie efektywniejsz od intelowskiej.

Wykorzystanie sekcji krytycznej do synchronizacji zapeniania tablicy z poprzedniego przykadu ilustruje projekt CritSec.dpr znajdujcy si na zaczonym krku CD-ROM. Jego modu gwny prezentujemy na wydruku 5.5. Wydruk 5.5. Przykad synchronizacji z wykorzystaniem sekcji krytycznej
unit Main;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;

type TMainForm = class(TForm) Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private procedure ThreadsDone(Sender: TObject); end;

TFooThread = class(TThread) protected procedure Execute; override; end;

var MainForm: TMainForm;

implementation

{$R *.DFM}

const MaxSize = 128;

var

235

NextNumber: Integer = 0; DoneFlags: Integer = 0; GlobalArray: array[1..MaxSize] of Integer; CS: TRTLCriticalSection;

function GetNextNumber: Integer; begin Result := NextNumber; inc(NextNumber); end; // zwr warto zmiennej globalnej // zwiksz zmienn globaln

procedure TFooThread.Execute; var i: Integer; begin OnTerminate := MainForm.ThreadsDone; EnterCriticalSection(CS); for i := 1 to MaxSize do begin GlobalArray[i] := GetNextNumber; Sleep(3+Random(12)); end; LeaveCriticalSection(CS); end; // koniec sekcji krytycznej // ustaw element tablicy // pozwl dziaa innemu wtkowi // pocztek sekcji krytycznej

procedure TMainForm.ThreadsDone(Sender: TObject); var i: Integer; begin inc(DoneFlags); if DoneFlags = 2 then begin // upewnij si, e zwolniono obydwa wtki for i := 1 to MaxSize do { wypenij list zawartoci tablicy } Listbox1.Items.Add(IntToStr(GlobalArray[i])); DeleteCriticalSection(CS); end; end;

procedure TMainForm.Button1Click(Sender: TObject); begin InitializeCriticalSection(CS); // utwrz i uruchom wtki TFooThread.Create(False); TFooThread.Create(False); end;

236

end.

Krytycznym fragmentem kodu, stanowicym tre sekcji krytycznej jest ptla wypeniajca tablic:
for i := 1 to MaxSize do begin GlobalArray[i] := GetNextNumber; Sleep(3 + Random(12)); end;

Wtek, ktry zostanie wpuszczony do sekcji krytycznej jako pierwszy, zapeni tablic liczbami od 0 do 127; po jego wyjciu z sekcji krytycznej zostanie do niej wpuszczony drugi wtek, ktry niweczc prac wykonan przez poprzednika zapeni tablic liczbami od 128 do 255 i rwnie opuci sekcj krytyczn; efekt jego dziaa zostanie wywietlony na formularzu (rys. 5.5).

Rysunek 5.5. Efekt zapeniania tablicy synchronizowanego sekcj krytyczn

Wykluczenia wzajemne (muteksy)


Mechanizm wykluczenia wzajemnego (mutex od mutual exclusion) podobny jest do sekcji krytycznej, odrniaj go jednak dwie istotne wasnoci. Po pierwsze, w przeciwiestwie do sekcji krytycznej, bdcej lokalnym obiektem procesu, muteks jest globalnym obiektem Win32 API, reprezentowanym przez uchwyt (handle), po drugie jest on dostpny dla wszystkich wtkw wszystkich procesw poprzez sw nazw symboliczn.

Wskazwka

Poza oczywistymi rnicami semantycznymi, istotn rnic midzy muteksem a sekcj krytyczn jest ich efektywno. Sekcje krytyczne, jako obiekty w gruncie rzeczy nieskomplikowane, s niesamowicie efektywne wejcie do sekcji krytycznej lub wyjcie z niej, przy braku wtkw kolidujcych, trwa zaledwie kilkanacie cykli zegara! Gdy jednak prba wejcia wtku do sekcji krytycznej musi zakoczy si jego wstrzymaniem, system tworzy zwizany z tym obiekt zdarzeniowy (zazwyczaj muteks). Uywanie obiektw zdarzeniowych jest znacznie bardziej czasochonne, gdy wie si z wywoywaniem procedur jdra systemu, co z kolei wymaga przeczenia kontekstu procesu i zmiany (sprztowego) poziomu ochrony. Trwa to zazwyczaj kilkaset cykli zegarowych, rwnie wtedy, gdy aplikacja nie wykorzystuje aktualnie wtkw pobocznych i gdy nie ma konkurencyjnych da w stosunku do chronionego zasobu.

237

Utworzenie muteksu nastpuje w wyniku wywoania funkcji CreateMutex():


function CreateMutex(lpMutexAttributes: PSecurityAttributes; bInitialOwner: BOOL; lpName: PCHar): THandle; stdcall;

Parametr lpMutexAttributes jest wskanikiem do struktury okrelajcej tzw. atrybuty bezpieczestwa; podanie pustego wskanika (NIL) spowoduje przyjcie atrybutw domylnych. Parametr bInitialOwner okrela, czy wtek tworzcy muteks ma by uwaany za jego waciciela; warto False oznacza, e utworzony muteks nie posiada waciciela. Parametr lpName jest globaln nazw identyfikujc muteks; warto NIL powoduje utworzenie muteksu nienazwanego. Globalny charakter nazwy muteksu przejawia si w tym, i funkcja CreateMutex() poszukuje w systemie ewentualnie istniejcego ju muteksu o podanej nazwie i w przypadku jego znalezienia tworzy do niego dodatkowy uchwyt, w przeciwnym wypadku tworzy nowy muteks. Zwolnienie muteksu sprowadza si do zamknicia (za pomoc CloseHandle()) jego uchwytu zwracanego jako wynik funkcji CreateMutex(). Prezentowany na wydruku 5.6 kod moduu rdowego kolejnego projektu Mutex.dpr ilustruje wykorzystanie muteksw do synchronizacji wtkw zapeniajcych tablic. Wydruk 5.6. Synchronizacja dostpu do tablicy za pomoc wykluczenia wzajemnego

unit Main;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;

type TMainForm = class(TForm) Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private procedure ThreadsDone(Sender: TObject); end;

TFooThread = class(TThread) protected procedure Execute; override; end;

var MainForm: TMainForm;

implementation

238

{$R *.DFM}

const MaxSize = 128;

var NextNumber: Integer = 0; DoneFlags: Integer = 0; GlobalArray: array[1..MaxSize] of Integer; hMutex: THandle = 0;

function GetNextNumber: Integer; begin Result := NextNumber; Inc(NextNumber); end; // zwr warto zmiennej globalnej // zwiksz zmienn globaln

procedure TFooThread.Execute; var i: Integer; begin FreeOnTerminate := True; OnTerminate := MainForm.ThreadsDone; if WaitForSingleObject(hMutex, INFINITE) = WAIT_OBJECT_0 then begin for i := 1 to MaxSize do begin GlobalArray[i] := GetNextNumber; Sleep(3 + Random(12)); end; end; ReleaseMutex(hMutex); end; // ustaw element tablicy // pozwl dziaa innemu wtkowi

procedure TMainForm.ThreadsDone(Sender: TObject); var i: Integer; begin Inc(DoneFlags); if DoneFlags = 2 then begin for i := 1 to MaxSize do { wypenij list zawartoci tablicy } Listbox1.Items.Add(IntToStr(GlobalArray[i])); CloseHandle(hMutex); end; // upewnij si, e zwolniono obydwa wtki

239

end;

procedure TMainForm.Button1Click(Sender: TObject); begin hMutex := CreateMutex(nil, False, nil); // Utwrz i uruchom wtki TFooThread.Create(False); TFooThread.Create(False); end;

end.

Istot mechanizmu wzajemnego wykluczenia jest w powyszym przykadzie oczekiwanie wtku na dostp do zasobu, realizowane przez nastpujc funkcj API:
function WaitForSingleObject( hHandle: THandle; dwMilliseconds : DWORD):DWORD;stdcall;

Funkcja powoduje, e wtek czeka tak dugo, a obiekt reprezentowany przez uchwyt hHandle znajdzie si w tzw. stanie sygnalnym (signaled state) nie duej jednak, ni przez czas okrelony przez parametr dwMilliseconds. Okrelenie stan sygnalny ma rne znaczenia w stosunku do rnych typw obiektw Win32, na przykad muteks znajduje si w stanie sygnalnym, gdy aden wtek nie jest jego wacicielem, za proces wchodzi w stan sygnalny z chwil swego zakoczenia. Limit czasu oczekiwania, okrelony przez drugi parametr, wyraony jest w milisekundach; podanie wartoci INFINITE oznacza oczekiwanie do skutku. Podanie zerowego limitu oczekiwania powoduje jedynie sprawdzenie statusu obiektu i natychmiastowy powrt do wtku wywoujcego. Wynikiem funkcji moe by jedna z trzech wartoci, ktrych znaczenie wyjania tabela 5.3.

Tabela 5.3. Znaczenie wyniku zwracanego przez funkcj WaitForSingleObject()

Warto
WAIT_ABANDONED

Znaczenie Przedmiotem oczekiwania jest obiekt muteks. Wtek bdcy jego wacicielem zakoczy si, nie dokonujc jednak jego zwolnienia; mamy wic do czynienia z muteksem porzuconym (abandoned). Kolejnym wacicielem wtku staje si wtek wywoujcy (tj. wtek, ktry utworzy wtek wanie zakoczony). W rezultacie muteks nie znajduje si wic w stanie sygnalnym.

WAIT_OBJECT_0 WAIT_TIMEOUT

Przedmiotowy obiekt znalaz si w stanie sygnalnym. Zosta wyczerpany limit czasu oczekiwania, przedmiotowy obiekt nie znajduje si w stanie sygnalnym.

W procedurze FooThread.Execute() realizowane jest oczekiwanie do skutku. Po doczekaniu si wtek staje si wacicielem muteksu, zwalnianego nastpnie za pomoc procedury ReleaseMutex(). Tworzony muteks nie posiada pocztkowo ani waciciela, ani nazwy i jest identyfikowany jedynie przez uchwyt. Jego wacicielem staje si wtek, ktry wykonuje w stosunku do niego funkcj WaitForSingleObject(), 240

uywajc jego uchwytu jako pierwszego parametru. w stosunek wasnoci ustaje w momencie wywoania przez wtek funkcji ReleaseMutex() muteks wchodzi wwczas w stan sygnalny.
Wskazwka

Win32 oferuje take bardziej skomplikowany wariant oczekiwania oczekiwanie na stan sygnalny jednego lub wikszej liczby obiektw z zadanego ich zbioru. Do jego realizacji su funkcje WaitForMultipleObjects() oraz MsgWaitForMultipleObjects(), opisane w systemie pomocy Win32 API.

Semafory
Pod wzgldem funkcjonalnym semafor (semaphore) podobny jest do muteksu, posiada jednak dodatkowe wyposaenie w postaci licznika, ktrego warto zalena jest od liczby synchronizowanych wtkw (pokaemy za chwil, w jaki sposb). Niezerowa (dodatnia) warto tego licznika oznacza stan sygnalny semafora. Do utworzenia semafora suy funkcja CreateSemaphore():
function CreateSemaphore (lpSemaphoreAttributes:PSecurityAttributes; lInitialCount, lMaximumCount: Longint; lpName: PChar): THandle; stdcall;

Parametry lpSemaphoreAttributes i lpName maj takie samo znaczenie, jak w przypadku funkcji CreateMutex(). Parametr lInitialCount okrela pocztkow warto licznika semafora nie moe ona wykracza poza zakres 0lMaximumCount. Kadorazowe wystpienie semafora jako parametru wywoania funkcji WaitForSingleObject() (lub analogicznej funkcji powodujcej oczekiwanie) zmniejsza o jeden jego licznik (chyba e ma on w tym momencie warto zero o tym za chwil) i vice versa licznik ten jest zwikszany po kadym wywoaniu procedury ReleaseSemaphore(). Nietrudno si wic domyli, i warto lMaximumCount okrela maksymaln liczb zasobw, ktre zostan przepuszczone przez semafor, czyli w momencie uycia semafora jako parametru wywoania funkcji synchronizujcej (np. WaitForSingleObject()) zastan go z dodatni wartoci licznika. Wyjania to jednoczenie typowe zastosowanie semafora suy on do synchronizacji dostpu do zasobu, ktry moe by wspdzielony przez co najwyej zadan a priori liczb procesw; liczba ta jest jednoczenie wartoci pocztkow licznika semafora. Powracajc do naszego przykadu z inicjowaniem tablicy moe by ona obsugiwana w danej chwili przez co najwyej jeden wtek jeeli wic uyjemy do synchronizacji semafora, jego warto pocztkowa (lInitialCount) powinna wynosi wanie 1.

Poniszy wydruk, pochodzcy z projektu Sema4.dpr, przedstawia ostatni ju wersj dwuwtkowego inicjowania tablicy, oczywicie z wykorzystaniem semafora.

Wydruk 5.7. Synchronizacja dostpu do tablicy za pomoc semafora


unit Main;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;

type

241

TMainForm = class(TForm) Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private procedure ThreadsDone(Sender: TObject); end;

TFooThread = class(TThread) protected procedure Execute; override; end;

var MainForm: TMainForm;

implementation

{$R *.DFM}

const MaxSize = 128;

var NextNumber: Integer = 0; DoneFlags: Integer = 0; GlobalArray: array[1..MaxSize] of Integer; hSem: THandle = 0;

function GetNextNumber: Integer; begin Result := NextNumber; Inc(NextNumber); end; // zwr warto zmiennej globalnej // zwiksz zmienn globaln

procedure TFooThread.Execute; var i: Integer; WaitReturn: DWORD; begin OnTerminate := MainForm.ThreadsDone; WaitReturn := WaitForSingleObject(hSem, INFINITE); if WaitReturn = WAIT_OBJECT_0 then begin for i := 1 to MaxSize do begin

242

GlobalArray[i] := GetNextNumber; Sleep(3 + Random(12)); end; end; ReleaseSemaphore(hSem, 1, nil); end;

// ustaw element tablicy // pozwl dziaa innym wtkom

procedure TMainForm.ThreadsDone(Sender: TObject); var i: Integer; begin Inc(DoneFlags); if DoneFlags = 2 then begin for i := 1 to MaxSize do { wypenij list zawartoci tablicy } Listbox1.Items.Add(IntToStr(GlobalArray[i])); CloseHandle(hSem); end; end; // upewnij si, e obydwa wtki zostay zwolnione

procedure TMainForm.Button1Click(Sender: TObject); begin hSem := CreateSemaphore(nil, 1, 1, nil); // utwrz i uruchom obydwa wtki TFooThread.Create(False); TFooThread.Create(False); end;

end.

Utworzenie semafora nastpuje bezporednio przed utworzeniem obydwu wtkw (wartoci pocztkow licznika semafora jest 1, co przed chwil ju wyjanilimy):
hSem := CreateSemaphore(nil, 1, 1, nil); TFooThread.Create(False); TFooThread.Create(False);

Przed rozpoczciem krytycznej ptli inicjujcej elementy tablicy wywoywana jest funkcja synchronizujca WaitForSingleObject(), odwoujca si do semafora za porednictwem jego uchwytu hSem:
WaitReturn := WaitForSingleObject(hSem, INFINITE);

Rozpoczcie realizacji ptli odbywa si tylko wtedy, gdy oczekiwanie zakoczyo si na skutek stanu sygnalnego semafora:
if WaitReturn = WAIT_OBJECT_0 then

243

Wtek, opuszczajc krytyczn ptl (stanowic w tym wypadku rodzaj zasobu o synchronizowanym dostpie) sygnalizuje ten fakt przez wywoanie funkcji ReleaseSemaphore():
Function ReleaseSemaphore(hSEmaphore: THandle; lReleaseCount: Longint; lpPreviousCount: Pointer): BOOL; stdcall;

Pierwszym parametrem wywoania jest oczywicie uchwyt semafora uywanego do synchronizacji. Parametr lReleaseCount okrela, o ile naley zwikszy warto licznika semafora musi to by warto dodatnia, niekoniecznie rwna 1 (cho akurat w tym przypadku jest). Moliwo zwikszenia licznika semafora o wicej ni 1 w pojedynczym wywoaniu funkcji
ReleaseSemaphore() okazuje si czasem niezwykle przydatna. Jako przykad rozpatrzmy aplikacj, w ramach

ktrej wiksza liczba wtkw drugorzdnych wykorzystuje wspbienie jaki zasb, zdolny obsuy jednoczenie co najwyej 10 z nich. Jedenasty wtek, zgaszajc danie dostpu do zasobu, napotka na zerow warto semafora i bdzie musia poczeka. Zamy teraz, i podczas tego oczekiwania wszystkie 10 wtkw zakoczyo si w sposb awaryjny i aden z nich nie zdy podnie semafora, tj. wywoa funkcji ReleaseSemaphore(). Efekt jest taki, i aktualnie aden wtek z zasobu nie korzysta, lecz dostp do niego jest w dalszym cigu zablokowany, gdy licznik semafora ma warto 0. W tej sytuacji wtek gwny mgby (po wykonaniu ewentualnych czynnoci naprawczych) zadecydowa o ponownym udostpnieniu zasobu musiaby w tym celu zwikszy odpowiednio warto licznika, wanie o 10. Z powyszego opisu wynika jednoczenie kosmopolityczny charakter semaforw aden semafor nie jest przynaleny do adnego szczeglnego wtku, a wic wszystkie wtki mog uywa go bez ogranicze. Innym ciekawym wykorzystaniem procedury ReleaseSemaphore() sugerowanym przez system pomocy Delphi jest zablokowanie dostpu do chronionego zasobu na czas wykonywania przez wtek lub aplikacj pewnych czynnoci inicjujcych. Semafor jest tworzony z zerow wartoci pocztkow licznika, co chwilowo blokuje dostp do zasobu; ostateczne nadanie licznikowi semafora danej (dodatniej) wartoci pocztkowej nastpuje wanie za pomoc funkcji ReleaseSemaphore(). Prba zwikszenia licznika semafora ponad ustalony limit (okrelony przez trzeci parametr funkcji
CreateSemaphore()) nie powiedzie si funkcja ReleaseSemaphore() zwrci warto FALSE.

Ostatni parametr wywoania funkcji ReleaseSemaphore() lpPreviousCount umoliwia wskazanie zmiennej typu longint, do ktrej wpisana zostanie poprzednia warto licznika tj. ta sprzed wywoania funkcji; podanie wartoci NIL oznacza rezygnacj z tej moliwoci. Moe to dziwne, ale nie istnieje w Win32 moliwo odczytania biecej wartoci licznika semafora; mona j ustali jedynie post factum, w ten wanie sposb. Jak kady obiekt Win32 identyfikowany przez uchwyt, rwnie semafor podlega zwolnieniu za pomoc procedury CloseHandle().

Przykad zastosowania wielowtkowoci: zaawansowane wyszukiwanie tekstu


Opisane mechanizmy pracy wielowtkowej zilustrujemy na przykadzie rzeczywistej aplikacji, sucej do wyszukiwania podanego cigu znakw w grupie plikw tekstowych. Jej kompletny projekt o nazwie DelSrch.dpr znajduje si na zaczonym krku CD-ROM; formularz gwny tego projektu przedstawiamy na rysunku 5.6.

244

Rysunek 5.6. Formularz gwny projektu DelSrch.dpr

Waciwe wyszukiwanie odbywa si w ramach wtku drugorzdnego, wtek gwny zajmuje si natomiast obsug interfejsu uytkownika, pozwalajcego ustali rnorodne aspekty przeszukiwania lokalizacj przeszukiwanych plikw, ich mask, posta raportu wyszukiwania itd. Rozpoczcie wyszukiwania nastpuje w momencie kliknicia przycisku Szukaj; przeszukane zostaj wszystkie pliki, ktrych nazwa pasuje do podanego wzorca, zlokalizowane w podanym katalogu oraz (jeeli uytkownik sobie tego zayczy) rwnie w jego podkatalogach. Stwierdzenie obecnoci poszukiwanego wzorca w danym pliku spowoduje dodanie nazwy tego pliku do listy, opcjonalnie wraz z wykazem linii zawierajcych wzorzec. Wygld listy jest na bieco uaktualniany w oknie raportu. Dwukrotne kliknicie linii zawierajcej nazw pliku spowoduje uruchomienie skojarzonej z tym plikiem aplikacji; przy braku skojarzenia uruchamiany jest notatnik (notepad). Przyjrzyjmy si teraz moduom realizujcym obydwa wtki aplikacji: za interfejs uytkownika odpowiedzialny jest modu Main.Pas, za implementacj wtku przeszukujcego zawiera modu SrchU.Pas.

Interfejs uytkownika
Modu implementujcy interfejs uytkownika zawiera wiele interesujcych elementw ilustruje m.in. zarzdzanie listami wyboru, uruchamianie aplikacji skojarzonych z plikami, przegldanie plikw, tworzenie i uruchamianie wtkw pobocznych, drukowanie wynikw, zapis i odczyt pliku .ini itp. Jego tre zostaa przedstawiona na wydruku 5.8. Wydruk 5.8. Modu gwny aplikacji realizujcy interfejs uytkownika
unit Main;

interface

245

uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons, ExtCtrls, Menus, SrchIni, SrchU, ComCtrls, AppEvnts;

type TMainForm = class(TForm) lbFiles: TListBox; StatusBar: TStatusBar; pnlControls: TPanel; PopupMenu: TPopupMenu; FontDialog: TFontDialog; pnlOptions: TPanel; gbParams: TGroupBox; LFileSpec: TLabel; LToken: TLabel; lPathName: TLabel; edtFileSpec: TEdit; edtToken: TEdit; btnPath: TButton; edtPathName: TEdit; gbOptions: TGroupBox; cbCaseSensitive: TCheckBox; cbFileNamesOnly: TCheckBox; cbRecurse: TCheckBox; cbRunFromAss: TCheckBox; pnlButtons: TPanel; btnSearch: TBitBtn; btnClose: TBitBtn; btnPrint: TBitBtn; btnPriority: TBitBtn; Font1: TMenuItem; Clear1: TMenuItem; Print1: TMenuItem; N1: TMenuItem; Exit1: TMenuItem; ApplicationEvents: TApplicationEvents; procedure btnSearchClick(Sender: TObject); procedure btnPathClick(Sender: TObject); procedure lbFilesDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); procedure Font1Click(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormCreate(Sender: TObject);

246

procedure btnPrintClick(Sender: TObject); procedure btnCloseClick(Sender: TObject); procedure lbFilesDblClick(Sender: TObject); procedure FormResize(Sender: TObject); procedure btnPriorityClick(Sender: TObject); procedure edtTokenChange(Sender: TObject); procedure Clear1Click(Sender: TObject); procedure ApplicationEventsHint(Sender: TObject); private procedure ReadIni; procedure WriteIni; public Running: Boolean; SearchPri: Integer; SearchThread: TSearchThread; procedure EnableSearchControls(Enable: Boolean); end;

var MainForm: TMainForm;

implementation

{$R *.DFM}

uses Printers, ShellAPI, FileCtrl, PriU, StrUtils;

procedure PrintStrings(Strings: TStrings); { Wydruk wszystkich pozycji z listy Strings } var Prn: TextFile; I: Integer; begin if Strings.Count = 0 then // czy jest co drukowa? raise Exception.Create('Brak tekstu do wydruku!'); AssignPrn(Prn); try Rewrite(Prn); try for I := 0 to Strings.Count - 1 do WriteLn(Prn, Strings.Strings[I]); finally CloseFile(Prn); end; except on EInOutError do // zamknij plik drukarki // iteracja po pozycjach listy // pisz na drukark // otwrz plik drukarki // przypisz Prn do drukarki

247

MessageDlg('Bd drukowania.', mtError, [mbOk], 0); end; end;

procedure TMainForm.EnableSearchControls(Enable: Boolean); { Sterowanie dostpnoci poszczeglnych elementw interfejsu uytkownika } begin btnSearch.Enabled := Enable; cbRecurse.Enabled := Enable; cbFileNamesOnly.Enabled := Enable; cbCaseSensitive.Enabled := Enable; btnPath.Enabled := Enable; edtPathName.Enabled := Enable; edtFileSpec.Enabled := Enable; edtToken.Enabled := Enable; Running := not Enable; edtTokenChange(nil); with btnClose do begin if Enable then begin Caption := 'Zamknij'; Hint := 'Zamknij aplikacj'; end else begin Caption := 'Zatrzymaj'; Hint := 'Zatrzymaj przeszukiwanie'; end; end; end; // ustaw flag Running

procedure TMainForm.btnSearchClick(Sender: TObject); { Wywoanie wtku wyszukujcego } begin EnableSearchControls(False); lbFiles.Clear; { uruchom wtek wywoujcy } SearchThread := TSearchThread.Create(cbCaseSensitive.Checked, cbFileNamesOnly.Checked, cbRecurse.Checked, edtToken.Text, edtPathName.Text, edtFileSpec.Text); end; // zablokuj kontrolki // wyczy list

procedure TMainForm.edtTokenChange(Sender: TObject); begin btnSearch.Enabled := not Running and (edtToken.Text <> ''); end;

248

procedure TMainForm.btnPathClick(Sender: TObject); { Wybr lokalizacji plikw } var ShowDir: string; begin ShowDir := edtPathName.Text; if SelectDirectory('Wybierz katalog...', '', ShowDir) then edtPathName.Text := ShowDir; end;

procedure TMainForm.lbFilesDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); { rysowanie specyficzne listy } var CurStr: string; begin with lbFiles do begin CurStr := Items.Strings[Index]; Canvas.FillRect(Rect); if not cbFileNamesOnly.Checked then // jeli nie tylko nazwy plikw...

{ jeeli bieca linia zawiera nazw pliku } if (Pos('Plik ', CurStr) = 1) and (CurStr[Length(CurStr)] = ':') then with Canvas.Font do begin // podkrelone i na czerwono Style := [fsUnderline]; Color := clRed; end else Rect.Left := Rect.Left + 15; // w przeciwnym razie wcicie

DrawText(Canvas.Handle, PChar(CurStr), Length(CurStr), Rect, DT_SINGLELINE); end; end;

procedure TMainForm.Font1Click(Sender: TObject); { Wybr czcionki } begin if FontDialog.Execute then lbFiles.Font := FontDialog.Font; end;

249

{ Odczyt/zapis pliku .INI podczas tworzenia/zwalniania formularza }

procedure TMainForm.FormCreate(Sender: TObject); begin ReadIni; end;

procedure TMainForm.FormDestroy(Sender: TObject); begin WriteIni; end;

procedure TMainForm.btnPrintClick(Sender: TObject); { uytkownik wyraa ch drukowania } begin if MessageDlg('Czy wydrukowa wyniki poszukiwania??', mtConfirmation, [mbYes, mbNo], 0) = mrYes then PrintStrings(lbFiles.Items); end;

procedure TMainForm.btnCloseClick(Sender: TObject); begin // jeli trwa przeszukiwanie, zakocz wtek przeszukujcy if Running then SearchThread.Terminate // w przeciwnym razie zakocz aplikacj else Close; end;

procedure TMainForm.lbFilesDblClick(Sender: TObject); { obsuga dwukrotnego kliknicia linii zawierajcej nazw pliku } var ProgramStr, FileStr: string; RetVal: THandle; begin { jeli kliknito lini zawierajc nazw pliku.. } if (Pos('Plik ', lbFiles.Items[lbFiles.ItemIndex]) = 1) then begin { zaaduj edytor tekstowy zgodnie z plikiem INI - domylnie Notepad } ProgramStr := SrchIniFile.ReadString('Defaults', 'Editor', 'notepad'); FileStr := lbFiles.Items[lbFiles.ItemIndex]; // wybrany plik

FileStr := Copy(FileStr, 6, Length(FileStr) - 5); // usu prefiks if FileStr[Length(FileStr)] = ':' then // usu ":"

250

DecStrLen(FileStr, 1); if cbRunFromAss.Checked then { uruchom skojarzony program } RetVal := ShellExecute(Handle, 'open', PChar(FileStr), nil, nil, SW_SHOWNORMAL) else { uruchom edytor } RetVal := ShellExecute(Handle, 'open', PChar(ProgramStr), PChar(FileStr), nil, SW_SHOWNORMAL); { sprawd poprawno wykonania } if RetVal < 32 then RaiseLastWin32Error; end; end;

procedure TMainForm.FormResize(Sender: TObject); { Obsuga zdarzenia OnResize. Wyrodkowuje kontrolki na formularzu } begin { podziel pasek statusu na dwa panele w stosunku 1:2 } with StatusBar do begin Panels[0].Width := Width div 3; Panels[1].Width := Width * 2 div 3; end; end;

procedure TMainForm.btnPriorityClick(Sender: TObject); { Wywietl formularz priorytetw } begin ThreadPriWin.Show; end;

procedure TMainForm.ReadIni; { Odczytaj ustawienia z pliku INI } begin with SrchIniFile do begin edtPathName.Text := ReadString('Defaults', 'LastPath', 'C:\'); edtFileSpec.Text := ReadString('Defaults', 'LastFileSpec', '*.*'); edtToken.Text := ReadString('Defaults', 'LastToken', ''); cbFileNamesOnly.Checked := ReadBool('Defaults', 'FNamesOnly', False); cbCaseSensitive.Checked := ReadBool('Defaults', 'CaseSens', False); cbRecurse.Checked := ReadBool('Defaults', 'Recurse', False); cbRunFromAss.Checked := ReadBool('Defaults', 'RunFromAss', False); Left := ReadInteger('Position', 'Left', Left); Top := ReadInteger('Position', 'Top', Top); Width := ReadInteger('Position', 'Width', Width);

251

Height := ReadInteger('Position', 'Height', Height); end; end;

procedure TMainForm.WriteIni; { zapisz ustawienia w pliku INI } begin with SrchIniFile do begin WriteString('Defaults', 'LastPath', edtPathName.Text); WriteString('Defaults', 'LastFileSpec', edtFileSpec.Text); WriteString('Defaults', 'LastToken', edtToken.Text); WriteBool('Defaults', 'CaseSens', cbCaseSensitive.Checked); WriteBool('Defaults', 'FNamesOnly', cbFileNamesOnly.Checked); WriteBool('Defaults', 'Recurse', cbRecurse.Checked); WriteBool('Defaults', 'RunFromAss', cbRunFromAss.Checked); WriteInteger('Position', 'Left', Left); WriteInteger('Position', 'Top', Top); WriteInteger('Position', 'Width', Width); WriteInteger('Position', 'Height', Height); end; end;

procedure TMainForm.Clear1Click(Sender: TObject); begin lbFiles.Items.Clear; end;

procedure TMainForm.ApplicationEventsHint(Sender: TObject); { Wywietlenie podpowiedzi na pasku statusu } begin StatusBar.Panels[0].Text := Application.Hint; end;

end.

Co najmniej dwa elementy powyszego wydruku zasuguj na szczegln uwag. Pierwszym jest prosta procedura PrintStrings(), drukujca wszystkie zawarte na licie acuchy. Procedura ta wykorzystuje drukark jako plik tekstowy, przypisujc j wpierw do zmiennej Prn typu TextFile, a nastpnie wykonujc instrukcj Writeln dla kadego acucha na licie. Po wydrukowaniu acuchw drukarka jest zwalniana za pomoc instrukcji CloseFile(). Drugim interesujcym elementem jest sposb uruchomienia programu skojarzonego z zarejestrowanym typem plikw; suy do tego funkcja API o nazwie ShellExecute(). W Windows skojarzenia oparte s na rozszerzeniach plikw: jeeli, na przykad, odnony plik bdzie posiada rozszerzenie .PAS, jego wybranie spowoduje uruchomienie Delphi.

252

Wskazwka

W sytuacji, gdy wywoanie funkcji ShellExecute() nie powiedzie si (zwrcony wynik bdzie mniejszy od 32), aplikacja wywouje procedur RaiseLastWin32Error(). Procedura ta, zlokalizowana w module SYSUTILS.PAS, pobiera (za pomoc funkcji GetLastError()) kod ostatniego bdu i na jego podstawie wywietla czytelny komunikat:: procedure RaiseLastWin32Error; var LastError: DWORD; Error: EWin32Error; begin LastError := GetLastError; if LastError <> ERROR_SUCCESS then Error := EWin32Error.CreateFmt(SWin32Error, [LastError, SysErrorMessage(LastError)]) else Error := EWin32Error.Create(SUnkWin32Error); Error.ErrorCode := LastError; raise Error; end; Czyni to z niej uyteczne narzdzie do czytelnego informowania uytkownika aplikacji o zaistniaych bdach Win32 API i tym samym rekomenduje j jako narzdzie dla projektantw.

Proces przeszukiwania
Wtek realizujcy przeszukiwanie rwnie obfituje w ciekawostki. Demonstruje on m.in. zastosowanie rekursji do obsugi podkatalogw oraz komunikacj z wtkiem gwnym. Tre jego moduu SrchU.Pas przedstawiono na wydruku 5.9.

Wydruk 5.9. Modu realizujcy wtek przeszukujcy


unit SrchU;

interface

uses Classes, StdCtrls;

type TSearchThread = class(TThread) private LB: TListbox; CaseSens: Boolean; FileNames: Boolean; Recurse: Boolean; SearchStr: string; SearchPath: string;

253

FileSpec: string; AddStr: string; FSearchFile: string; procedure AddToList; procedure DoSearch(const Path: string); procedure FindAllFiles(const Path: string); procedure FixControls; procedure ScanForStr(const FName: string; var FileStr: string); procedure SearchFile(const FName: string); procedure SetSearchFile; protected procedure Execute; override; public constructor Create(CaseS, FName, Rec: Boolean; const Str, SPath, FSpec: string); destructor Destroy; override; end;

implementation

uses SysUtils, StrUtils, Windows, Forms, Main;

constructor TSearchThread.Create(CaseS, FName, Rec: Boolean; const Str, SPath, FSpec: string); begin CaseSens := CaseS; FileNames := FName; Recurse := Rec; SearchStr := Str; SearchPath := AddBackSlash(SPath); FileSpec := FSpec; inherited Create(False); end;

destructor TSearchThread.Destroy; begin FSearchFile := ''; Synchronize(SetSearchFile); Synchronize(FixControls); inherited Destroy; end;

procedure TSearchThread.Execute; begin FreeOnTerminate := True; LB := MainForm.lbFiles; // zwolnij wtek po zakoczeniu

254

Priority := TThreadPriority(MainForm.SearchPri); if not CaseSens then SearchStr := UpperCase(SearchStr); FindAllFiles(SearchPath); if Recurse then DoSearch(SearchPath); end; // biecy katalog // // i podkatalogi

procedure TSearchThread.FixControls; // Odblokowuje kontrolki na formularzu; musi by wywoywana przez Synchronize() begin MainForm.EnableSearchControls(True); end;

procedure TSearchThread.SetSearchFile; { Uaktualnia nazw pliku na pasku statusu; musi by wywoywana przez Synchronize() } begin MainForm.StatusBar.Panels[1].Text := FSearchFile; end;

procedure TSearchThread.AddToList; { Dodaje pozycj do listy; musi by wywoywana przez Synchronize() } begin LB.Items.Add(AddStr); end;

procedure TSearchThread.ScanForStr(const FName: string; var FileStr: string); { Skanuje FileStr w celu znalezienia nazwy pliku } var Marker: string[1]; FoundOnce: Boolean; FindPos: integer; begin FindPos := Pos(SearchStr, FileStr); FoundOnce := False; while (FindPos <> 0) and not Terminated do begin if not FoundOnce then begin { uyj ":" , jeli nie "tylko nazwy plikw" } if FileNames then Marker := '' else Marker := ':'; { dodaj lini z nazw pliku do listy }

255

AddStr := Format('Plik %s%s', [FName, Marker]); Synchronize(AddToList); FoundOnce := True; end; { nie poszukuj dalszych wystpie wzorca, jeli "tylko nazwy plikw" } if FileNames then Exit;

{ Dodaj lini, jeli nie "tylko nazwy plikw"} AddStr := GetCurLine(FileStr, FindPos); Synchronize(AddToList); FileStr := Copy(FileStr, FindPos + Length(SearchStr), Length(FileStr)); FindPos := Pos(SearchStr, FileStr); end; end;

procedure TSearchThread.SearchFile(const FName: string); var DataFile: THandle; FileSize: Integer; SearchString: string; begin FSearchFile := FName; Synchronize(SetSearchFile); try DataFile := FileOpen(FName, fmOpenRead or fmShareDenyWrite); if DataFile = 0 then raise Exception.Create(''); try { ustaw dugo przeszukiwanego acucha } FileSize := GetFileSize(DataFile, nil); SetLength(SearchString, FileSize); { Kopiuj zawarto pliku do acucha } FileRead(DataFile, Pointer(SearchString)^, FileSize); finally CloseHandle(DataFile); end; if not CaseSens then SearchString := UpperCase(SearchString); ScanForStr(FName, SearchString); except on Exception do begin AddStr := Format('Bd odczytu pliku: %s', [FName]); Synchronize(AddToList); end; end; end;

256

procedure TSearchThread.FindAllFiles(const Path: string); { wyszukuje pliki we wskazanym katalogu } var SR: TSearchRec; begin // rozpocznij szukanie plikw if FindFirst(Path + FileSpec, faArchive, SR) = 0 then try repeat SearchFile(Path + SR.Name); until (FindNext(SR) <> 0) or Terminated; finally SysUtils.FindClose(SR); end; end; // zakocz szukanie plikw // przetwrz plik // nastpny plik

procedure TSearchThread.DoSearch(const Path: string); { rekursywne przetwarzanie katalogw } var SR: TSearchRec; begin { rozpocznij szukanie katalogw } if FindFirst(Path + '*.*', faDirectory, SR) = 0 then try repeat { jeli katalog rny od '.' i '..' }

if ((SR.Attr and faDirectory) <> 0) and (SR.Name[1] <> '.') and not Terminated then begin FindAllFiles(Path + SR.Name + '\'); DoSearch(Path + SR.Name + '\'); end; until (FindNext(SR) <> 0) or Terminated; finally SysUtils.FindClose(SR); end; end; // zakocz szukanie // znajd nastpny katalog // przetwrz katalog // i jego podkatalogi

end.

Zasadnicz akcj wtku przeszukujcego mona podzieli na dwa etapy. W pierwszym zostaj przeszukane pliki biecego katalogu, ktrych nazwa pasuje do zadanej maski; czynno t wykonuje procedura FindAllFiles(), tote cay pierwszy etap sprowadza si do jej wywoania. Drugi etap, wykonywany tylko wtedy, gdy zadano przeszukiwania podkatalogw, polega na wykonaniu procedury FindAllFiles() we wszystkich podkatalogach katalogu biecego; okrelenie wszystkie podkatalogi rozumiane jest w sposb rekurencyjny i obejmuje rwnie podkatalogi dalszych rzdw. Rekurencja ta znajduje swoje odzwierciedlenie

257

w kodzie programu wykonanie procedury DoSearch() w stosunku do danego katalogu polega na wykonaniu najpierw w stosunku do niego samego procedury FindAllFiles(), a nastpnie procedury DoSearch() w stosunku do wszystkich jego (i tylko jego) podkatalogw.

Wskazwka

Rekurencyjny algorytm stosowany przez procedur DoSearch() jest standardow technik przetwarzania drzewa katalogw. Poniewa algorytmy rekurencyjne s z natury trudne w ledzeniu, szczeglnie cennymi algorytmami tej kategorii s algorytmy ju przetestowane i pracujce bez zarzutu jak procedura DoSearch(), ktr mona poleci jako narzdzie uniwersalne.

Naley zwrci uwag na pewn subtelno kryjc si w rekurencyjnym przeszukiwaniu katalogw. Ot funkcje FindFirst()i FindNext(), wrd znalezionych nazw katalogw udostpniaj rwnie pozycje '.' oraz '..' nie stanowice podkatalogw w zwykym tego sowa znaczeniu jak wiadomo, pierwsza z nich oznacza dany katalog jako samego siebie, druga za reprezentuje jego katalog nadrzdny. Uwzgldnienie tych pozycji na rwni z prawdziwymi podkatalogami podczas rekurencyjnego ich przeszukiwania doprowadzioby do zaptlenia si procedury DoSearch() ju na pierwszym katalogu. Std te drugi warunek w instrukcji
if ((SR.Attr and faDirectory) <> 0) and (SR.Name[1] <> '.') and

Kompletny algorytm przeszukujcy realizowany jest w nastpujcych etapach:

1. 2.

Gdy procedura FindAllFiles() znajdzie kolejny plik, ktrego nazwa pasuje do zadanej maski, wywoywana jest dla niego metoda SearchFile(). Metoda SearchFile() wczytuje zawarto pliku do acucha (za pomoc funkcji FileRead()); dla tego acucha przydzielana jest uprzednio pami o rozmiarze rwnym rozmiarowi pliku (za pomoc procedury SetLength()). Jeeli podczas przeszukiwania nieistotna jest wielko liter, acuch jest normalizowany przez zamian jego znakw na due litery. Metoda SearchFile() wywouje metod ScanForStr(). Metoda ScanForStr() przeszukuje acuch w celu znalezienia danego wzorca. Jeeli wzorzec zostanie znaleziony, do wynikowej listy dodawana jest pozycja zawierajca nazw pliku; jeeli na formularzu nie jest zaznaczone pole Tylko nazwy plikw, do listy dodawane s rwnie pozycje reprezentujce poszczeglne wystpienia wzorca w acuchu.

3.

Zwr uwag, i wikszo metod wtku dokonuje okresowo sprawdzenia waciwoci Terminate w celu ewentualnego przerwania (na danie) realizacji wtku. Nie zapominaj, i wszelkie odwoania wtku przeszukujcego do komponentw formularza musz odbywa si za porednictwem metody Synchronize(); przekonaj si, i jest tak istotnie.

Zmiana priorytetu wtku przeszukujcego


Dodatkow opcj programu DelSrch jest moliwo dynamicznej zmiany priorytetu wzgldnego wtku przeszukujcego. Formularz speniajcy to zadanie przedstawiony jest na rysunku 5.7, a na wydruku 5.10 prezentujemy tre odpowiadajcego mu moduu rdowego.

258

Rysunek 5.7. Formularz ustalania priorytetu wtku przeszukujcego

Wydruk 5.10. Dynamiczna zmiana priorytetu wtku


unit PriU;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, Buttons, ExtCtrls;

type TThreadPriWin = class(TForm) tbrPriTrackBar: TTrackBar; Label1: TLabel; Label2: TLabel; Label3: TLabel; btnOK: TBitBtn; btnRevert: TBitBtn; Panel1: TPanel; procedure tbrPriTrackBarChange(Sender: TObject); procedure btnRevertClick(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure FormShow(Sender: TObject); procedure btnOKClick(Sender: TObject); procedure FormCreate(Sender: TObject); private { Private declarations } OldPriVal: Integer; public { Public declarations } end;

var ThreadPriWin: TThreadPriWin;

implementation

259

{$R *.DFM}

uses Main, SrchU;

procedure TThreadPriWin.tbrPriTrackBarChange(Sender: TObject); begin with MainForm do begin SearchPri := tbrPriTrackBar.Position; if Running then SearchThread.Priority := TThreadPriority(tbrPriTrackBar.Position); end; end;

procedure TThreadPriWin.btnRevertClick(Sender: TObject); begin tbrPriTrackBar.Position := OldPriVal; end;

procedure TThreadPriWin.FormClose(Sender: TObject; var Action: TCloseAction); begin Action := caHide; end;

procedure TThreadPriWin.FormShow(Sender: TObject); begin OldPriVal := tbrPriTrackBar.Position; end;

procedure TThreadPriWin.btnOKClick(Sender: TObject); begin Close; end;

procedure TThreadPriWin.FormCreate(Sender: TObject); begin tbrPriTrackBarChange(Sender); end; // ustaw pocztkowy priorytet wtku

end.

Tre powyszego moduu nie jest skomplikowana sprowadza si do uaktualniania priorytetu wtku stosownie do jednego z piciu moliwych pooe suwaka; zadanie to wykonywane jest w procedurze PriTrackBarChange(). Biecy priorytet jest dodatkowo zapisywany w polu SearchPri formularza gwnego, moliwe jest wic zachowanie priorytetu przeszukiwania pomidzy kolejnymi uruchomieniami wtku przeszukujcego.

260

Wielowtkowy dostp do BDE


Problematyk obsugi baz danych w Delphi zajmiemy si szczegowo w dalszych rozdziaach niniejszej ksiki, obecnie chcielibymy jednak zademonstrowa wyjtkow uyteczno pracy wielowtkowej w trakcie operowania bazami danych, a cilej podczas wyszukiwania informacji. Jeeli niektre uywane tu pojcia bd dla Ciebie niezrozumiae, moesz znale ich wyjanienie m.in. w rozdziale 7. Najbardziej cenn wasnoci baz danych w Win32 jest niewtpliwie moliwo realizacji zoonych zapyta i procedur zapamitanych (stored procedures) w tle, niezalenie od dialogu prowadzonego z uytkownikiem, przez co aplikacja wyranie zyskuje na mobilnoci. Jest to moliwe dziki 32-bitowej bibliotece obsugi baz danych, zwanej Borland Database Engine (w skrcie BDE). Wykorzystanie tej moliwoci jest jednak obwarowane dwoma ograniczeniami: Kade zapytanie formuowane w ramach odrbnego wtku musi dokonywa si w odrbnej sesji. Wymg ten realizuje si za pomoc komponentu TSession, ktrego nazwa (waciwo Name) powinna by przypisana do waciwoci SessionName komponentu TQuery. Oznacza to jednoczenie, i jeeli komponent TQuery komunikuje si z komponentem TDatabase, to kada sesja musi uywa odrbnych komponentw tej klasy. Nie wolno kojarzy komponentu TQuery z komponentem TDataSource, jeeli w ramach wtku drugorzdnego otwarte zostao zapytanie; kojarzenie takie moe odbywa si wycznie w kontekcie wtku gwnego. Staje si to zrozumiae, jeeli uwiadomimy sobie rol komponentu TDataSource stanowi on rodek komunikacji pomidzy zbiorem danych a komponentami interfejsu uytkownika; te ostatnie mog by obsugiwane jedynie w kontekcie wtku gwnego.

Na zaczonym krku CD-ROM znajduje si projekt o nazwie BdeThrd.dpr, ilustrujcy wielowtkowe zapytania; jego formularz gwny jest przedstawiony na rysunku 5.8.

Rysunek 5.8. Formularz gwny projektu BdeThrd.dpr

Po wybraniu konkretnego aliasu bazy danych, zalogowaniu si, wpisaniu zapytania i klikniciu przycisku Wykonaj nastpi uruchomienie nowego wtku, z dynamicznie tworzonym formularzem klasy TQueryForm, zawierajcym po jednym komponencie TQuery, TSession, TDatabase, TDBGrid i TDataSource. Kade zapytanie obsugiwane jest wic przez odrbny zestaw komponentw, moliwe jest zatem otwarcie kilku zapyta jednoczenie. Przykad trzech formularzy, wywietlajcych wyniki trzech rnych zapyta jest przedstawiony na rysunku 5.9.

261

Rysunek 5.9. Formularze wywietlajce wyniki trzech niezalenych zapyta

Tre moduu gwnego projektu zostaa przedstawiona na wydruku 5.11.

Wydruk 5.11. Modu gwny projektu BdeThrd.dpr


unit Main;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Grids, StdCtrls, ExtCtrls;

type TMainForm = class(TForm) pnlBottom: TPanel; pnlButtons: TPanel; btnGo: TButton; btnExit: TButton; memQuery: TMemo; pnlTop: TPanel; Label1: TLabel; cbAlias: TComboBox; Label3: TLabel; edUserName: TEdit; Label4: TLabel;

262

edPassword: TEdit; Label2: TLabel; procedure btnExitClick(Sender: TObject); procedure btnGoClick(Sender: TObject); procedure FormCreate(Sender: TObject); private { Private declarations } public { Public declarations } end;

var MainForm: TMainForm;

implementation

{$R *.DFM}

uses QryU, DB, DBTables;

var FQueryNum: Integer = 0;

procedure TMainForm.btnExitClick(Sender: TObject); begin Close; end;

procedure TMainForm.btnGoClick(Sender: TObject); begin Inc(FQueryNum); // unikatowy numer zapytania

{ wygeneruj nowe zapytanie } NewQuery(FQueryNum, memQuery.Lines, cbAlias.Text, edUserName.Text, edPassword.Text); end;

procedure TMainForm.FormCreate(Sender: TObject); begin { wypenij list dostpnymi aliasami } Session.GetAliasNames(cbAlias.Items); end;

end.

Nie dzieje si tu zbyt wiele: po utworzeniu formularza lista rozwijalna AliasCombo wypeniana jest zarejestrowanymi w systemie aliasami baz danych za pomoc metody GetAliasNames komponentu TSession. Po klikniciu przycisku Wykonaj nastpuje wywoanie procedury NewQuery(), wykonujcej kompletn obsug nowego zapytania; zauwa, i poszczeglne zapytania s zliczane (za pomoc licznika

263

FQueryNum), a kolejny numer zapytania przekazywany jest jako parametr do procedury, ktra wykorzystuje go do stworzenia unikatowej nazwy sesji. Procedura NewQuery() jest centraln czci moduu QryU.PAS, zawierajcego ponadto definicj formularza QueryForm. Jego kod rdowy jest przedstawiony na wydruku

5.12. Wydruk 5.12. Kod rdowy formularza TQueryForm


unit QryU;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Grids, DBGrids, DB, DBTables, StdCtrls;

type TQueryForm = class(TForm) Query: TQuery; DataSource: TDataSource; QuerySession: TSession; QueryDatabase: TDatabase; dbgQueryGrid: TDBGrid; memSQL: TMemo; procedure FormClose(Sender: TObject; var Action: TCloseAction); private { Private declarations } public { Public declarations } end;

procedure NewQuery(QryNum: integer; Qry: TStrings; const Alias, UserName, Password: string);

implementation

{$R *.DFM}

type TDBQueryThread = class(TThread) private FQuery: TQuery; FDataSource: TDataSource; FQueryException: Exception; procedure HookUpUI; procedure QueryError; protected procedure Execute; override; public

264

constructor Create(Q: TQuery; D: TDataSource); virtual; end;

constructor TDBQueryThread.Create(Q: TQuery; D: TDataSource); begin inherited Create(True); FQuery := Q; FDataSource := D; FreeOnTerminate := True; Resume; end; // uruchom wtek // utwrz zawieszony wtek // ustaw parametry

procedure TDBQueryThread.Execute; begin try FQuery.Open; Synchronize(HookUpUI); // otwrz zapytanie // uaktualnij interfejs uytkownika // w kontekcie wtku gwnego except FQueryException := ExceptObject as Exception; Synchronize(QueryError); // poinformuj o wyjtku, w ramach // wtku gwnego end; end;

procedure TDBQueryThread.HookUpUI; begin FDataSource.DataSet := FQuery; end;

procedure TDBQueryThread.QueryError; begin Application.ShowException(FQueryException); end;

procedure TQueryForm.FormClose(Sender: TObject; var Action: TCloseAction); begin Action := caFree; end;

procedure NewQuery(QryNum: integer; Qry: TStrings; const Alias, UserName, Password: string); begin { Stwrz nowy formularz dla wywietlenia wynikw zapytania } with TQueryForm.Create(Application) do begin

265

{ Wygeneruj unikatow nazw sesji } QuerySession.SessionName := Format('Sess%d', [QryNum]); with QueryDatabase do begin { ustaw unikatow nazw bazy danych } DatabaseName := Format('DB%d', [QryNum]); { ustaw alias } AliasName := Alias; { przycz TDatabase do TSession } SessionName := QuerySession.SessionName; { nazwa uytkownika i haso } Params.Values['USER NAME'] := UserName; Params.Values['PASSWORD'] := Password; end; with Query do begin { przycz TQuery do TDatabase i TSession } DatabaseName := QueryDatabase.DatabaseName; SessionName := QuerySession.SessionName; { ustaw tre zapytania } SQL.Assign(Qry); end; { wywietl tre zapytania w MEMO } memSQL.Lines.Assign(Qry); { wywietl formularz zapytania } Show; { otwrz zapytanie w ramach odrbnego wtku } TDBQueryThread.Create(Query, DataSource); end; end;

end.

Procedura NewQuery() tworzy nowy egzemplarz formularza TQueryForm, nadaje wymagane wartoci waciwociom jego komponentw, w szczeglnoci przypisuje unikatowe nazwy komponentom TDatabase i TSession. Waciwo SQL komponentu TQuery wypeniana jest treci zapytania pobieran z komponentu TMemo na formularzu gwnym (w postaci listy acuchw przekazywanej jako parametr Qry). Ostatnia instrukcja procedury tworzy nowy wtek realizujcy zapytanie. Na kilka sw komentarza zasuguje te metoda Execute() wtku TDBQueryThread. W jej treci wywoywana jest metoda HookUpUI(), dokonujca kojarzenia komponentu TDataSource z komponentami TQuery i TDBGrid zgodnie z tym, co napisalimy nieco wczeniej, przypisanie to musi si odby w kontekcie wtku gwnego, dlatego te obudowane zostao metod Synchronize(). To samo tyczy si metody QueryError(), wywoywanej w przypadku wystpienia wyjtku podczas realizacji metody Execute().

266

Wielowtkowe operacje graficzne


Wspominalimy ju wczeniej, i dostp do formularza i jego komponentw zarezerwowany jest dla wtku gwnego std wanie wynika konieczno opracowania metody Synchronize() dla klasy TThread. Poczwszy od Delphi 3, regua ta zostaa nieco zagodzona, mianowicie: wtek poboczny ma prawo wykonywa operacje graficzne na ptnie (Canvas) komponentu pod warunkiem, i przed ich rozpoczciem zapewni sobie wyczno dostpu do tego ptna, wywoujc jego metod Lock(). Po zakoczeniu rysowania powinien on natomiast wywoa metod UnLock(), zwalniajc w ten sposb naoon na ptno blokad dostpu dla innych wtkw. Dziaanie metod Lock() i UnLock() przypomina do zudzenia mechanizm sekcji krytycznych; istotnie, zostay one zrealizowane za pomoc tego wanie mechanizmu:
procedure TCanvas.Lock; begin EnterCriticalSection(CounterLock); Inc(FLockCount); LeaveCriticalSection(CounterLock); EnterCriticalSection(FLock); end;

procedure TCanvas.UnLock; begin LeaveCriticalSection(FLock); EnterCriticalSection(CounterLock); Dec(FLockCount); LeaveCriticalSection(CounterLock); end;

Jak atwo zauway, szeregowanie dostpu do ptna odbywa si za pomoc sekcji krytycznej FLock. Tajemniczy licznik FLockCount zwizany jest z jeszcze jedn metod ptna dotyczc jego rezerwacji, mianowicie TryLock():
function TCanvas.TryLock: Boolean; begin EnterCriticalSection(CounterLock); try Result := FLockCount = 0; if Result then Lock; finally LeaveCriticalSection(CounterLock); end; end;

Zgodnie ze sw nazw, funkcja ta usiuje dokona rezerwacji ptna; sprawdza jednak wpierw, czy ptno nie jest aktualnie zarezerwowane, to znaczy, czy warto licznika FLockCount rwna jest zero. Jeeli tak, dokonuje rzeczywistej rezerwacji (Lock) i zwraca warto True. Niezerowa warto licznika FLockCount oznacza, i ktry z wtkw znajduje si aktualnie w sekcji krytycznej i prba rezerwacji spowodowaaby oczekiwanie; w takiej sytuacji metoda rezygnuje z rezerwacji i zwraca warto False.

267

Metoda TryLock() realizuje wic polecenie zarezerwuj ptno pod warunkiem, e aktualnie jest ono wolne. Zwr uwag, e testowanie i zmiana licznika FLockCount s chronione przez inn sekcj krytyczn CounterLock. Prezentacja wielowtkowego dostpu do ptna jest przedmiotem przykadowego projektu MTgraph.dpr, znajdujcego si na zaczonym krku CD-ROM. Kod rdowy jego moduu gwnego przedstawiamy na wydruku 5.13.

Wydruk 5.13. Ilustracja wielowtkowych operacji graficznych na ptnie komponentu


unit Main;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Menus, Dialogs;

type TMainForm = class(TForm) MainMenu1: TMainMenu; Options1: TMenuItem; AddThread: TMenuItem; RemoveThread: TMenuItem; ColorDialog1: TColorDialog; Add10: TMenuItem; RemoveAll: TMenuItem; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure AddThreadClick(Sender: TObject); procedure RemoveThreadClick(Sender: TObject); procedure Add10Click(Sender: TObject); procedure RemoveAllClick(Sender: TObject); private ThreadList: TList; public { Public declarations } end;

TDrawThread = class(TThread) private FColor: TColor; FForm: TForm; public constructor Create(AForm: TForm; AColor: TColor); procedure Execute; override; end;

var

268

MainForm: TMainForm;

implementation

{$R *.DFM}

{ TDrawThread }

constructor TDrawThread.Create(AForm: TForm; AColor: TColor); begin FColor := AColor; FForm := AForm; inherited Create(False); end;

procedure TDrawThread.Execute; var P1, P2: TPoint;

procedure GetRandCoords; var MaxX, MaxY: Integer; begin // ulokuj punkty P1 i P2 w losowym pooeniu w granicach formularza MaxX := FForm.ClientWidth; MaxY := FForm.ClientHeight; P1.x := Random(MaxX); P2.x := Random(MaxX); P1.y := Random(MaxY); P2.y := Random(MaxY); end;

begin FreeOnTerminate := True; // wtek powinien skoczy si razem z zakoczeniem aplikacji while not (Terminated or Application.Terminated) do begin GetRandCoords; with FForm.Canvas do begin Lock; // zablokuj ptno // ulokuj losowo punkty P1 i P2

// w danej chwili co najwyej jeden wtek moe realizowa // poniszy fragment:

Pen.Color := FColor;

// ustaw kolor pira

269

MoveTo(P1.X, P1.Y); LineTo(P2.X, P2.Y);

// narysuj odcinek aczcy punkty P1 i P2 //

// koniec krytycznego fragmentu

UnLock; end; end; end;

// odblokuj ptno

{ TMainForm }

procedure TMainForm.FormCreate(Sender: TObject); begin ThreadList := TList.Create; end;

procedure TMainForm.FormDestroy(Sender: TObject); begin RemoveAllClick(nil); ThreadList.Free; end;

procedure TMainForm.AddThreadClick(Sender: TObject); begin // dodaj nowy wtek do listy; pozwl uytkownikowi wybra kolor pira if ColorDialog1.Execute then ThreadList.Add(TDrawThread.Create(Self, ColorDialog1.Color)); end;

procedure TMainForm.RemoveThreadClick(Sender: TObject); begin // zakocz ostatni wtek listy i usu go z listy TDrawThread(ThreadList[ThreadList.Count - 1]).Terminate; ThreadList.Delete(ThreadList.Count - 1); end;

procedure TMainForm.Add10Click(Sender: TObject); var i: Integer; begin // utwrz 10 wtkw, przypisujc im losowe kolory pira for i := 1 to 10 do ThreadList.Add(TDrawThread.Create(Self, Random(MaxInt))); end;

270

procedure TMainForm.RemoveAllClick(Sender: TObject); var i: Integer; begin Cursor := crHourGlass; try for i := ThreadList.Count - 1 downto 0 do begin TDrawThread(ThreadList[i]).Terminate; // zakocz wtek TDrawThread(ThreadList[i]).WaitFor; // upewnij si, e wtek faktycznie // si zakoczy end; ThreadList.Clear; finally Cursor:= crDefault; end; end;

initialization Randomize; end. // uruchom generator liczb pseudolosowych

Formularz projektu jest przedstawiony na rysunku 5.10. Pierwsze polecenie z menu Opcje umoliwia uruchomienie nowego wtku (klasy TDrawThread) dokonujcego krelenia na ptnie formularza odcinkw linii prostych w sposb losowy; uytkownik ma moliwo wybrania koloru krelonych linii. Uywajc tej opcji wielokrotnie moemy uruchomi dowoln liczb wtkw. Drugie polecenie menu umoliwia usunicie wtku ostatnio uruchomionego. Dwa pozostae polecenia umoliwiaj (odpowiednio) uruchomienie dziesiciu nowych wtkw (posugujcych si losowo wybranymi kolorami) i usunicie wszystkich uruchomionych wtkw pobocznych. Wygld formularza na rysunku 5.10 to efekt rwnoczesnego dziaania dziesiciu wtkw.

Rysunek 5.10. Efekt wielowtkowego krelenia linii prostych na ptnie formularza

271

Tak wic dziki prostym metodom TCanvas.Lock() oraz TCanvas.UnLock() wtki poboczne mog wykonywa operacje na ptnach komponentw formularza, unikajc przy tym kosztownej czasowo metody Synchronize(). Co wicej, wszystkie metody Paint() komponentw VCL, a take obsuga wszystkich zdarze OnPaint, odbywaj si z udziaem metod Lock()/UnLock() zatem bezporednie rysowanie po ptnie komponentu nie koliduje nawet ze standardowymi operacjami biblioteki VCL. Zastanwmy si na koniec, co staoby si, gdybymy zezwolili na ywioowe rysowanie po ptnach komponentw bez adnej synchronizacji. Rozpatrzmy w tym celu dwa wtki, z ktrych pierwszy narysowa chce lini w kolorze niebieskim, drugi za okrg w kolorze czerwonym. Przypumy wic, i pierwszy wtek ustawi ju niebieski kolor pira i zdy narysowa cz linii, gdy zosta wywaszczony na rzecz drugiego wtku; ten ustawi czerwony kolor pira i narysowa cz okrgu, lecz sterowanie wrcio tymczasem do wtku pierwszego. Nietrudno skonstatowa, i pozostaa cz linii narysowana zostanie w kolorze czerwonym. To tylko jeden z moliwych scenariuszy przy wikszej liczbie wtkw (oczywicie puszczonych na ywio) i bardziej skomplikowanych operacjach graficznych mog dzia si rzeczy jeszcze ciekawsze. Notabene w celu spowodowania opisanego wyej chaosu wystarczy, by tylko jeden z wtkw wyama si z przyjtego protokou i nie przejmowa si koniecznoci blokowania i odblokowywania ptna.

Wkna
Wkna (fibers) s obiektami Win32 stanowicymi (koncepcyjnie) co na ksztat miniatur wtkw. Podobnie jak wtki, wkna dysponuj wasnym kontekstem wykonawczym, posiadajc odrbne obszary stosu i chronion zawarto rejestrw procesora. Jednak, w przeciwiestwie do wtkw, nie s one wywaszczane przez system operacyjny z czasu procesora wic za przeczanie wkien odpowiedzialna jest sama aplikacja. Z punktu widzenia projektanta aplikacji przypadki, w ktrych wkna okazuj si bardziej uyteczne od rasowych wtkw, s raczej rzadkie; wkna maj nad wtkami t przewag, i oferujc oddzielny kontekst wykonawczy nie wymagaj jednoczenie stosowania zaawansowanych mechanizmw synchronizacyjnych, bo przeczanie wkien odbywa si cakowicie pod kontrol aplikacji.

Wskazwka

Wkna dostpne s w Windows NT 3.51 SP3 i wyszym, Windows NT 4.0, Windows 2000/XP oraz Windows 98/Me.

Nadzorowaniem pracy wkien zajmuje si sam wtek, naley jednak wpierw utworzy rwnowane mu wkno; czynno t wykonuje nastpujca funkcja API:

function ConvertThreadToFiber(lpParameter: Pointer): BOOL; stdcall;

Jedynym jej parametrem jest wskanik do danych specyficznych dla wkna, przekazywanych do niego przez wtek wywoujcy i nie majcych znaczenia dla Win32 API. Powysza deklaracja zaczerpnita z moduu windows.pas jest jednak, niestety, niepoprawna: w rzeczywistoci wynikiem funkcji jest bowiem wskanik do wkna (jako obiektu Win32), nie za warto boolowska.

Kiedy wtek utworzy ju rwnowane mu wkno, moe tworzy i usuwa nowe wkna oraz oczywicie dokonywa przeczania pomidzy nimi. Do utworzenia nowego wkna suy funkcja CreateFiber():

function CreateFiber(dwStackSize: DWORD; lpStartAddress: TFNFiberStartRoutine; lpParameter: Pointer): BOOL; stdcall;

272

Parametr dwStackSize oznacza pocztkowy rozmiar stosu przydzielanego dla wkna; podanie wartoci zerowej spowoduje przydzielenie stosu o rozmiarze domylnym, rwnym rozmiarowi stosu wtku nadrzdnego. Parametr lpStartAddress jest wskanikiem do bezparametrowej procedury realizujcej tre wkna. Parametr lpParameter umoliwia przekazanie do tworzonego wkna dodatkowych danych. Podobnie jak w przypadku funkcji ConvertThreadToFiber(), deklaracja wyniku rwnie jest niepoprawna, gdy i tym razem wynik jest wskanikiem do obiektu reprezentujcego utworzone wkno. Przeczaniem pomidzy dziaajcymi wknami zajmuje si nastpujca funkcja:
function SwitchToFiber(lpFiber: Pointer): BOOL; stdcall;

Rwnie i ta deklaracja jest bdna, poniewa w rzeczywistoci jest to procedura nie zwracajca adnego wyniku. Jedynym parametrem jej wywoania jest wskanik do obiektu wkna. Wywoanie procedury SwitchToFiber() powoduje automatyczne przeczenie kontekstu wykonawczego czyli przeczenie stosu i rejestrw procesora. Usuwaniem wkien zajmuje si funkcja DeleteFiber():
function DeleteFiber(lpFiber: Pointer): BOOL; stdcall;

W rzeczywistoci jest ona podobnie jak SwitchToFiber() procedur nie zwracajc wyniku, bdnie zadeklarowan jako funkcja. Jej jedynym parametrem wywoania jest oczywicie wskanik do obiektu wkna.

Wskazwka

Wywoanie procedury DeleteFiber() z parametrem okrelajcym wkno wywoujce czyli swoiste samobjstwo wkna powoduje automatyczne wywoanie funkcji ExitThread(), koczcej wykonywanie caego wtku.

Zarzdzanie wknami sprowadza si wic do operowania czterema opisanymi funkcjami. Pliki nagwkowe Win32 deklaruj ponadto kilka pomocniczych funkcji i typw, nie wczonych jednak do Delphi. Na uytek projektu ilustrujcego wykorzystanie wkien stworzylimy wic may modu uzupeniajcy Fibers.pas, ktrego tre jest przedstawiona na wydruku 5.14.

Wydruk 5.14. Modu Fibers.pas


unit Fibers;

interface

uses Windows;

// typ reprezentujcy procedur startow wkna (na podstawie winbase.h) type PFIBER_START_ROUTINE = procedure (lpFiberParameter: Pointer); stdcall; LPFIBER_START_ROUTINE = PFIBER_START_ROUTINE; TFiberFunc = PFIBER_START_ROUTINE;

273

function GetCurrentFiber: Pointer; function GetFiberData: Pointer;

implementation

// specyficzne dla procesorw 80x86 funkcje pomocnicze (na podstawie winnt.h):

function GetCurrentFiber: Pointer; asm mov eax, fs:[$10] end;

function GetFiberData: Pointer; asm mov eax, fs:[$10] mov eax, [eax] end;

end.

Wspomniany projekt znajduje si na zaczonym krku CD-ROM i nosi nazw FibTest.dpr. Wygld jego formularza jest przedstawiony na rysunku 5.11, natomiast tre jego moduu gwnego prezentujemy na wydruku 5.15.

Rysunek 5.11. Formularz projektu ilustrujcego dziaanie wkien Wydruk 5.15. Modu gwny projektu Fibtest.dpr
unit FibMain;

interface

uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, AppEvnts;

type TForm1 = class(TForm) BtnWee: TButton; BtnStop: TButton;

274

Label1: TLabel; Label2: TLabel; Label3: TLabel; Label4: TLabel; AppEvents: TApplicationEvents; procedure BtnWeeClick(Sender: TObject); procedure AppEventsMessage(var Msg: tagMSG; var Handled: Boolean); procedure BtnStopClick(Sender: TObject); private { Private declarations } FThreadID: LongWord; FThreadHandle: Integer; public { Public declarations } end;

var Form1: TForm1;

implementation

uses Fibers;

{$R *.dfm}

const DDG_THREADMSG = WM_USER;

var FFibers: array[0..3] of Pointer; StopIt: Boolean;

procedure FiberFunc(Param: Pointer); stdcall; var J, FibNum, NextNum: Integer; I: Cardinal; Fiber: Pointer; begin try I := 0; FibNum := 1; // to tylko po to, by wyeliminowa // ostrzeenia ze strony kompilatora

Fiber := GetCurrentFiber; // zachowaj wskanik do obiektu biecego wkna

275

// odszukanie numeru biecego wkna w tablicy for J := Low(FFibers) to High(FFibers) do if FFibers[J] = Fiber then begin FibNum := J; Break; end;

// odliczanie od zera w gr while not StopIt do begin {A. Grayski} Application.ProcessMessages; // pozwl aplikacji na przetworzenie komunikatw

// po osigniciu kolejnej setki wylij komunikat do wtku gwnego if I mod 100 = 0 then PostMessage(Application.Handle, DDG_THREADMSG, Integer(GetFiberData), I);

// po osigniciu kolejnego tysica przecz wkna if I mod 1000 = 0 then begin if FibNum = High(FFibers) then NextNum := Low(FFibers) else NextNum := FibNum + 1; SwitchToFiber(FFibers[NextNum]); end; Inc(I); end; except // zignoruj nieobsuone wyjtki end; end;

function ThreadFunc(Param: Pointer): Integer; var I: Integer; begin Result := 0;

// przekszta biecy wtek we wkno FFibers[0] := Pointer(ConvertThreadToFiber(Pointer(1)));

// otwrz pozostae wkna

276

FFibers[1] := Pointer(CreateFiber(0, @FiberFunc, Pointer(2))); FFibers[2] := Pointer(CreateFiber(0, @FiberFunc, Pointer(3))); FFibers[3] := Pointer(CreateFiber(0, @FiberFunc, Pointer(4)));

// uruchom pierwsze wkno FiberFunc(Pointer(1));

// zwolnij wszystkie wkna; zwolnienie biecego wkna spowoduje // zakoczenie wtku for I := High(FFibers) downto Low(FFibers) do DeleteFiber(FFibers[I]); end;

procedure TForm1.BtnWeeClick(Sender: TObject); begin BtnWee.Enabled := False; // zapobiega wielokrotnemu naciniciu przycisku

FThreadHandle := BeginThread(nil, 0, @ThreadFunc, nil, 0, FThreadID); end;

procedure TForm1.AppEventsMessage(var Msg: tagMSG; var Handled: Boolean); begin if Msg.message = DDG_THREADMSG then begin // zalenie od tego, ktre wkno wysao komunikat, // zmieniona zostanie tre odpowiedniej etykiety

case Msg.wParam of 1: Label1.Caption := IntToStr(Msg.lParam); 2: Label2.Caption := IntToStr(Msg.lParam); 3: Label3.Caption := IntToStr(Msg.lParam); 4: Label4.Caption := IntToStr(Msg.lParam); end; Handled := True; end; end;

procedure TForm1.BtnStopClick(Sender: TObject); begin StopIt := True; end;

end.

277

Najistotniejszym fragmentem powyszego wydruku jest funkcja ThreadFunc(), wykonywana w ramach wtku pobocznego w rezultacie kliknicia przycisku Start. Tworzy ona wkno odpowiadajce biecemu wtkowi oraz trzy dodatkowe wkna. Przedmiotem realizacji dla kadego z wkien jest funkcja FiberFunc(), dokonujca monotonnego odliczania, wysyajca okresowo komunikat do wtku gwnego i dokonujca okresowego przeczania wkien. Wtek gwny, otrzymawszy wspomniany komunikat, odczytuje z jego treci numer wkna-nadawcy i stosownie do niego uaktualnia warto jednej z czterech etykiet na formularzu gwnym czego efekt ilustruje rysunek 5.12; zblione3 wartoci wszystkich czterech etykiet s porednio wiadectwem tego, i kade wkno dziaa we wasnym obszarze stosu.

Rysunek 5.12. Aplikacja FibTest w akcji

Podsumowanie
W niniejszym rozdziale opisalimy natur wtkw Win32 oraz zwizane z nimi mechanizmy Delphi. Przedstawilimy metody synchronizacji wtkw oraz reguy przydzielania priorytetw wtkom i procesom. Praktyczn ilustracj tych mechanizmw byy trzy przykadowe aplikacje: pierwsza z nich prowadzia proste wyszukiwanie w grupie plikw tekstowych, druga realizowaa rwnolegle zapytania SQL w odniesieniu do pojedynczej bazy danych, trzecia wreszcie wykonywaa nieskomplikowane rysunki na ptnie formularza, ilustrujc w ten sposb szeregowanie dostpu do ptna za pomoc jego metod Lock() i UnLock(). Na zakoczenie przedstawilimy przykad zastosowania wkien (fibers), bdcych w Win32 miniaturami wtkw, lecz w odrnieniu od nich kontrolowanych cakowicie przez aplikacj.

W tym przypadku wartoci wszystkich etykiet s identyczne, cho nie zawsze tak musi by (przyp. tum.).

278

Rozdzia 6.

Biblioteki DLL
Niniejszy rozdzia powicony bdzie bibliotekom DLL, ktre stanowi podstawowy element konstrukcyjny aplikacji dla Windows i w ogle caego systemu Windows. Zaprezentujemy tworzenie bibliotek DLL w Delphi oraz ich integracj z aplikacjami wywoujcymi. Pokaemy rwnie, jak w rodowisku Win32 wykorzysta bibliotek DLL w roli obszaru komunikacyjnego pomidzy procesami; praktyka taka bya dosy powszechna w 16-bitowych wersjach Windows. Stracia racj bytu w Win32 ze wzgldu na skrajnie odmienny sposb obsugi bibliotek DLL, mona j jednak zasymulowa za pomoc innych rodkw, co z pewnoci uatwi zadanie programistom przenoszcym do Delphi 6 aplikacje 16-bitowe.

Czym w istocie jest biblioteka DLL


Biblioteka DLL (Dynamic Link Library) jest moduem wykonywalnym, zawierajcym kod, dane, a take zasoby, ktre mog by udostpniane aplikacjom. Charakterystyczny jest sposb czenia bibliotek DLL z aplikacjami. czno aplikacji z wykorzystywan przez ni bibliotek DLL nawizywana jest dopiero w czasie jej wykonywania, co nazywane bywa popularnie pnym wizaniem (late binding) w przeciwiestwie do wczesnego wizania (early binding), wykonywanego przez konsolidator jeszcze na etapie tworzenia moduu wykonywalnego. Oprcz niewtpliwej elastycznoci w zakresie kompletowania kodu, stosownie do potrzeb wynikajcych z biecego stanu wykonywanej aplikacji podejcie takie posiada jeszcze jedn wana zalet: umoliwia wspdzielenie tego samego egzemplarza biblioteki DLL przez kilka (lub wszystkie!) wykonywalnych aplikacji, co pozwala unikn kosztownego nieraz dublowania obszernych fragmentw kodu w poszczeglnych moduach wykonywalnych .EXE. W ten wanie sposb wykorzystywane s podstawowe biblioteki DLL systemu Win32 Kernel32.dll, User32.dll i GDI32.dll. Biblioteka Kernel32 odpowiedzialna jest za zarzdzanie pamici, procesami i wtkami; biblioteka User32 obsuguje interfejs uytkownika i zarzdza tworzeniem okien oraz obsug komunikatw, natomiast w bibliotece GDI32 znajduj si procedury i funkcje podsystemu graficznego (GDI). Ponadto standardowe kontrolki Windows zaimplementowane s w bibliotece ComDlg32.dll, za nadzr nad rejestrem i bezpieczestwem systemu sprawuje biblioteka AdvAPI32.dll. Niezaleno bibliotek DLL od moduw wynikowych aplikacji wywoujcych rodzi jeszcze jedn niezmiernie istotn konsekwencj jest ni modularno tworzonych aplikacji (lub kompletnych systemw); w prawidowo zaprojektowanym systemie poszczeglne moduy (ktrymi s wanie biblioteki DLL) odpowiedzialne s za realizacj jego poszczeglnych elementw funkcjonalnych, midzy innymi obsug poszczeglnych urzdze, wykorzystywan czcionk, ksztat okien dialogowych itp. Unowoczenienie takiego systemu sprowadza si zazwyczaj do wymiany lub doczenia nowej biblioteki DLL.

275

Format wewntrzny biblioteki DLL jest niemale identyczny z formatem moduu wykonywalnego .EXE; jednak w przeciwiestwie do niego, biblioteka DLL nie peni nigdy roli samodzielnej, a jedynie suebn w stosunku do aplikacji nadrzdnych. Wikszo plikw bdcych bibliotekami DLL posiada (oczywiste) rozszerzenie .DLL, jednak pod wzgldem fizycznym bibliotekami DLL s take sterowniki urzdze *.drv, pliki *.ocx implementujce kontrolki ActiveX, pliki czcionek *.fon (te ostatnie nie zawieraj w ogle kodu wykonywalnego) i niektre pliki systemowe *.sys.

Notatka

Bibliotekami DLL s take pakiety (packages) Delphi i C++Buildera zajmiemy si nimi szczegowo w drugim tomie niniejszej ksiki.

Poczenie biblioteki DLL z korzystajc z niej aplikacj nastpuje w procesie tzw. czenia dynamicznego (dynamic linking), ktrym zajmiemy si dokadniej w dalszej czci rozdziau. Mwic oglnie, w momencie, gdy aplikacja wywoujca odwouje si do danej biblioteki DLL (nieobecnej jeszcze w pamici), system aduje bibliotek na sw globaln stert, posugujc si mechanizmem plikw odwzorowanych (memory-mapped files). Biblioteka ta jest nastpnie mapowana w przestrze adresow aplikacji wywoujcej. W ten sposb kada z aplikacji, korzystajc ze wsplnego egzemplarza biblioteki, zachowuje si tak, jak gdyby posugiwaa si oddzieln kopi jej kodu, danych i zasobw. Jest to sytuacja odmienna w stosunku do Win16, gdzie wszystkie aplikacje, dziaajc we wsplnej przestrzeni adresowej, mogy komunikowa si poprzez pojedyncz bibliotek DLL. Opisany powyej scenariusz jest jednak tylko wyidealizowan wersj rzeczywistoci wyidealizowan w tym sensie, i wspdzielenie pojedynczej kopii biblioteki DLL przez wiele procesw nie zawsze jest moliwe. Jego wykonalno zalena jest od jednego z parametrw biblioteki, mianowicie jej bazowego adresu adowania (preferred base address).

Bazowy adres adowania moduu


Jednym z elementw kadego moduu wykonywalnego s tzw. elementy relokowalne (relocatable items), to znaczy takie, ktrych warto zaley od pooenia moduu w pamici operacyjnej. Konsolidator, tworzc ostateczn (zapisywan na dysku) posta biblioteki DLL nadaje jej relokowalnym elementom tak warto, jak gdyby biblioteka ta ulokowana bya w obszarze rozpoczynajcym si od adresu okrelonego w parametrze Image base na karcie Linker opcji projektu (tworzcego t bibliotek). Jeeli w przestrzeni adresowej aplikacji istnieje wolny obszar rozpoczynajcy si od tego adresu i wystarczajco duy, by pomieci bibliotek, cay proces poczenia jej z aplikacj sprowadza si do pamiciowego odwzorowania jej pliku (mechanizm plikw odwzorowanych opisany jest szczegowo na stronach 580 598 ksiki Delphi 4. Vademecum profesjonalisty). Jeeli obszar taki nie istnieje, biblioteka ulokowana zostaje w innym miejscu przestrzeni adresowej aplikacji, wymaga to jednak utworzenia w pamici jej kopii i przeliczenia (fixup) jej elementw relokowalnych. Trafny dobr bazowego adresu adowania przyczynia si wic zarwno do oszczdnoci pamici (nie jest tworzona kopia pamiciowa biblioteki), jak i czasu (elementy relokowalne maj ju dan warto). Domyln wartoci bazowego adresu adowania w projekcie nowo tworzonej aplikacji, .EXE, jak rwnie biblioteki DLL jest $400000; jeeli nie zmienimy ktrego z tych ustawie, bazowy adres adowania biblioteki wypadnie w obszarze zajtym przez aplikacj i konieczne bdzie tworzenie kopii biblioteki. Zaleca si zmian bazowego adresu adowania biblioteki na warto z zakresu od $40000000 do $7FFFFFF0 w Windows 95/98/NT/2000 obszar ten nie jest nigdy wykorzystywany przez sam aplikacj.

Nieco terminologii
W dalszej czci rozdziau i w rozdziaach nastpnych wielokrotnie uywa bdziemy kilku poj, zwizanych z procesami i wykorzystywanymi przez nie moduami, gwnie bibliotekami DLL. Jako e ich

276

potoczne znaczenie nie jest z natury rzeczy tak precyzyjne, jak w cisej terminologii Win32, przedstawimy teraz t ostatni kategori znaczeniow. A wic: Aplikacja to program znajdujcy si w pliku z rozszerzeniem .EXE, nadajcy si do uruchomienia w systemie Windows. Plik wykonywalny to plik zawierajcy kod wykonywalny: do plikw wykonywalnych zaliczaj si pliki
*.EXE i biblioteki dynamiczne.

Instancja biblioteki to po prostu fakt jej obecnoci w ramach danego procesu, reprezentowany przez uchwyt (handle). Pojcie instancji odnosi si rwnie do uruchomionej aplikacji jeli uruchomimy j w kilku egzemplarzach, kady z nich jest osobn instancj. Modu wraz ze zmian sposobu korzystania z moduw w Win32 (w stosunku do 16-bitowych wersji Windows) ulega zatarciu rnica pomidzy moduem i jego instancj kade odwoanie si aplikacji do moduu wymaga utworzenia jego instancji (w pamici wirtualnej procesu), fizycznie reprezentowanej przez unikatowy uchwyt. Dla przypomnienia w rodowisku 16-bitowym kady modu zaadowany do pamici mg by rozpatrywany w oderwaniu od wykorzystujcych go procesw (a wic w oderwaniu od swych instancji), gdy posiada wasny adres w pamici wsplnej dla wszystkich procesw; w Win32 kady modu istnieje jedynie w kontekcie przestrzeni adresowej wykorzystujcego go procesu. Pomimo to Microsoft w dalszym cigu wykorzystuje pojcie moduu w swej dokumentacji, przy czytaniu ktrej naley by wiadomym tego, co napisano powyej1. Zadanie (task) Windows jest systemem wielozadaniowym z wywaszczaniem (preemptive multitasking), zatem kade zadanie dziaa niezalenie od pozostaych, take niezalenie ubiegajc si o zasoby systemowe. Co prawda obiektami rodowiska Windows 95/NT ubiegajcymi si o czas procesora s nie zadania, lecz ich wtki, jednak priorytet wtku zaley przede wszystkim od klasy priorytetowej zadania, do ktrego w wtek naley (pisalimy o tym w rozdziale 5.). Kade zadanie w Windows reprezentowane jest przez oddzielny uchwyt.

czenie statyczne kontra czenie dynamiczne


Podczas kompletowania przez konsolidator (linker) moduu wynikowego .EXE, wszystkie niezbdne procedury i funkcje znajdujce si w moduach (i ewentualnie w pliku *.DPR) zostaj wczone do jego kodu. Po zaadowaniu go do pamici, kada z tych procedur i funkcji posiada cile okrelone pooenie (w przestrzeni adresowej aplikacji), a ich wywoania odbywaj si bez ingerencji systemu. Ten rodzaj konsolidacji nosi nazw czenia statycznego (static linking), bo odbywa si ona bez jakiegokolwiek zwizku z faktycznym przebiegiem przyszego wykonania programu, ktrego przecie nie sposb (na og) przewidzie.
Wskazwka

Proces czenia moduw w aplikacj w Delphi obejmuje co prawda pewne czynnoci optymalizacyjne (tzw. smart linking) polegajce na niewczaniu do pliku wykonywalnego ewidentnie nieuywanych fragmentw kodu w tym procedur i funkcji, do ktrych nie istniej odwoania; nie zmienia to jednak w niczym opisanej idei czenia statycznego.

Zamy, e dwie aplikacje wykorzystuj jaki uniwersalny modu rdowy; po ich skompilowaniu i skonsolidowaniu wszystkie wykorzystywane procedury (funkcje) tego moduu zostan oczywicie wczone do obydwu plikw wykonywalnych; przy rwnoczesnym uruchomieniu obydwu aplikacji wiele funkcji i procedur bdzie obecnych w pamici w dwch egzemplarzach; efekt ten spotguje si przy uruchomieniu nastpnych aplikacji korzystajcych z tego moduu. Oprcz opisanego efektu powielenia procedur i funkcji naley by wiadomym tego, i niektre elementy aplikacji tworzone s niejako na zapas i istnieje maa szansa, i elementy te w ogle zostan wykorzystane; jako przykad mog tu posuy rnorodne procedury obsugi sytuacji wyjtkowych.
1

W jzyku polskim sprawa jest nieco bardziej skomplikowana, gdy termin modu jest oglnie przyjtym okreleniem moduu w sensie tu opisanym (module), jak rwnie np. moduu rdowego Delphi (unit); w tym rozdziale termin modu uywany wic bdzie tylko w tym ostatnim znaczeniu (przyp. tum.).

277

Przy czeniu dynamicznym (dynamic linking) kada z funkcji i procedur zawartych w bibliotece istnieje tylko w jednym egzemplarzu (przynajmniej teoretycznie patrz opis bazowego adresu adowania), za adowanie samej biblioteki nastpuje w momencie bd adowania do pamici samej aplikacji, bd dopiero na wyranie danie tej ostatniej. W pierwszym przypadku mamy do czynienia z tzw. adowaniem automatycznym lub niejawnym (implicit loading). Zamy, i biblioteka o nazwie MaxLib.dll zawiera funkcj zadeklarowan nastpujco:
function Max(i1, i2: integer): integer;

Wynikiem tej funkcji jest warto wikszej z dwch liczb podanych jako parametry wywoania. Najprostszym sposobem udostpnienia tej funkcji aplikacjom jest stworzenie moduu importujcego j z biblioteki, zwanego z tej racji moduem importowym. Oto przykad treci moduu importujcego funkcj Max:
unit MaxUnit; interface function Max(i1, i2: integer): integer; implementation function Max; external 'MAXLIB'; end.

Od zwykego moduu rni si on tym, i nie ma w nim implementacji funkcji Max(); funkcja ta jest zaimplementowana w bibliotece DLL, do ktrej odsya dyrektywa external. Wykorzystanie moduu jest natomiast jak najbardziej typowe wystarczy umieci jego nazw w dyrektywie uses. W momencie adowania aplikacji do pamici zostanie zaadowana take biblioteka MaxLib.dll, a przekazanie sterowania do funkcji Max() odbywa si bdzie automatycznie przy kadym jej wywoaniu. Drugim z omawianych wariantw czenia dynamicznego adowaniem jawnym (explicit loading) biblioteki na wyrane danie aplikacji zajmiemy si nieco pniej.

Korzyci pynce z uywania DLL


Rozpatrujc wykorzystanie bibliotek DLL w kategoriach technologicznych, natychmiast dostrzeesz wielorakie korzyci wynikajce z rnorodnych aspektw ich implementacji. W tym miejscu zajmiemy si dwoma najwaniejszymi wspdzieleniem zasobw przez aplikacje oraz ukryciem szczegw implementacyjnych.

Wspdzielenie kodu, zasobw i danych przez wiele aplikacji


Jak wczeniej wspominalimy, zastosowanie bibliotek DLL umoliwia na og zredukowanie zapotrzebowania na pami, gdy jeden egzemplarz kodu moe by jednoczenie wykorzystywany przez wiele aplikacji; bezdyskusyjna jest take korzy wynikajca z mniejszych rozmiarw moduw wykonywalnych. Jednak oprcz wspdzielenia kodu, moliwe jest rwnie wspdzielenie zawartych w DLL zasobw bitmap, czcionek, ikon itp. Problem wspdzielenia danych biblioteki DLL wymaga odrbnego komentarza. W rodowisku 16-bitowym kada biblioteka posiadaa swj wasny segment danych globalnych i zmiennych statycznych, tote jej dane mogy stanowi niekiedy nawet nieoczekiwanie dla projektanta i uytkownika obszar interferencji kilku

278

aplikacji, swego rodzaju skrzynk kontaktow. Byo to konsekwencj faktu, i wszystkie aplikacje funkcjonoway we wsplnej przestrzeni adresowej. W rodowisku Win32 sprawa ulega radykalnej zmianie: kady proces dziaa we wasnej przestrzeni adresowej, w ktr odwzorowywany jest rwnie obszar danych wykorzystywanej biblioteki DLL; poniewa przestrzenie adresowe poszczeglnych procesw s z zaoenia rozczne, wic nie jest moliwa wymiana danych przez jej obszar danych. Ponadto dwa rne procesy mog (chocia nie musz) posugiwa si dwiema odrbnymi kopiami biblioteki (w pamici wirtualnej) co wyjanilimy ju nieco wczeniej. Skoro jednak wszystkie wtki danego procesu dziaaj w tej samej przestrzeni adresowej, jest moliwa wymiana danych przez obszar stanowicy (uwaga) odwzorowanie globalnego segmentu danych biblioteki DLL w przestrze adresow procesu. Naley jednak pamita o tym, i niekontrolowany dostp do zmiennych globalnych moe doprowadzi do ich dezorganizacji, trzeba wic zastosowa w takiej sytuacji mechanizmy synchronizacyjne, ktre omwilimy ze szczegami w rozdziale 5. Dwie aplikacje (lub wiksza ich liczba) mog si jednak komunikowa ze sob poprzez wsplny obszar pamici (shared memory area), stanowicy odwzorowanie tego samego pliku dyskowego (w przestrzeniach adresowych poszczeglnych aplikacji); funkcje implementujce taki obszar wymiany mog znajdowa si wanie w bibliotece DLL. Zajmiemy si tym zagadnieniem w dalszej czci rozdziau.

Ukrycie szczegw implementacyjnych


Zgodnie z uksztatowanym przez dziesiciolecia standardem programowania, kada utworzona aplikacja posiada dwa oblicza: uytkowe, wynikajce po prostu z wykonywanych przez ni czynnoci oraz projektowe, przejawiajce si w jej kodzie rdowym. Dla kocowego uytkownika aplikacji zwykle dostpny jest jedynie aspekt uytkowy aplikacja sprzedawana jest w postaci pliku wykonywalnego .EXE, projektant za zatrzymuje dla siebie kod rdowy, bdcy czsto wynikiem olbrzymiej pracy i wysiku intelektualnego (i tym samym posiadajcy nieporwnanie wiksz warto ni kocowy modu wykonywalny). Motywy ukrycia szczegw implementacyjnych aplikacji s wic, jak wida, racjonalne, jednak jest to moliwe do zrealizowania zasadniczo tylko w przypadku udostpniania kompletnej aplikacji. Rozpowszechnianie binarnych fragmentw aplikacji nie zawsze byo moliwe bez wykorzystania czenia dynamicznego; najlepiej wiedz o tym programici rozpowszechniajcy np. moduy nowych komponentw jedynie w postaci plikw *.dcu nawet drobna zmiana w czci publicznej ktrego z moduw wymaganych przez odnony modu komponentu skutkuje wwczas protestami kompilatora z powodu niezgodnoci wersji moduw. Opisany problem nie wystpuje w przypadku czenia dynamicznego. Biblioteka DLL jest zamknit caoci, nie podlegajc ju kompilacji. Co wicej, biblioteki DLL maj uniwersaln posta, niezalen od jzyka, w ktrym zostay zaprogramowane, moliwe jest wic wykorzystanie w Delphi bibliotek DLL stworzonych np. w Turbo Asemblerze, C++, czy innym jzyku zdolnym generowa kod 32-bitowy i vice versa: biblioteki tworzone w Delphi mog by wykorzystywane w aplikacjach stworzonych w wymienionych jzykach. Dla uytkownika biblioteki DLL konieczna jest jedynie znajomo jej interfejsu, czyli uzewntrznionych procedur i funkcji. Jako przykad mona poda modu windows.pas, zawierajcy deklaracj funkcji ClientToScreen(): cz publiczna moduu zawiera deklaracj jej nagwka :
function ClientToScreen(hWnd: HWND; var lpPoint: TPoint):BOOL; stdcall;

za caa implementacja funkcji wyglda w sposb nastpujcy:


function ClientToScreen; external user32 name 'ClientToScreen';

a jej szczegy ukryte s, jak wida, w bibliotece User32.DLL.

Tworzenie i wykorzystywanie bibliotek DLL


Po opisaniu roli, jak speniaj biblioteki DLL w aplikacjach Windows, nadszed czas na szczegy zwizane z ich tworzeniem i wykorzystaniem przez Delphi i inne rodowiska zilustrujemy je konkretnymi przykadami.

279

Prosty przykad poznaj si swych pienidzy


Niniejszy przykad opiera si na sztandarowym pomyle dydaktyki wykorzystania DLL, ilustrujcym rozmienianie pewnej kwoty pienidzy na cztery rodzaje monet amerykaskich: 25-centowej (Quarter), 10centowej (Dime), 5-centowej (Nickel) i 1-centowej (Penny)2.

Biblioteka DLL
Funkcja realizujca rozmienianie pienidzy nosi nazw PenniesToCoins i znajduje si w bibliotece PenniesLib.dll. Kod rdowy gwnego pliku jej projektu PenniesLib.dpr prezentujemy na wydruku 6.1. Wydruk 6.1. Plik gwny projektu biblioteki PenniesLib
library PenniesLib; {$DEFINE PENNIESLIB} uses SysUtils, Classes, PenniesInt;

function PenniesToCoins(TotPennies: word; CoinsRec: PCoinsRec): word; StdCall; begin Result := TotPennies; { oblicz liczb monet poszczeglnych rodzajw } with CoinsRec^ do begin Quarters TotPennies Dimes TotPennies Nickels TotPennies Pennies end; end; := TotPennies div 25; := TotPennies - Quarters * 25; := TotPennies div 10; := TotPennies - Dimes * 10; := TotPennies div 5; := TotPennies - Nickels * 5; := TotPennies;

{ eksportuj funkcj przez nazw } exports PenniesToCoins; end.

Biblioteka PenniesLib wykorzystuje modu PenniesInt, ktry jednoczenie jest jej moduem importowym; w module tym znajduje si jednak definicja typu PCoinsRec, wykorzystywanego przez funkcj PenniesToCoins(). Dyrektywa exports suy do wskazania funkcji, ktre maj by dostpne na zewntrz biblioteki (czyli z niej wyeksportowane).

Zostay zachowane oryginalne nazwy monet (przyp. tum.).

280

Modu importowy biblioteki


Jak ju wspomnielimy, pomostem pomidzy bibliotek DLL a wykorzystujc j aplikacj s przy czeniu domylnym moduy importowe. Poza tym, i definiuj interfejs dla funkcji eksportowanych3 przez bibliotek DLL, zawieraj zazwyczaj definicje struktur danych wykorzystywanych zarwno przez bibliotek, jak i aplikacje wywoujce; w prezentowanym przykadzie struktur tak jest rekord TCoinsRec. Kod rdowy moduu importowego biblioteki PenniesLib przedstawiamy na wydruku 6.2.

Wydruk 6.2. PenniesInt.pas modu importowy biblioteki PenniesLib


unit PenniesInt; { Modu importowy biblioteki PENNIES.DLL }

interface type

{ poniszy rekord przechowuje liczb monet kadego rodzaju } PCoinsRec = ^TCoinsRec; TCoinsRec = record Quarters, Dimes, Nickels, Pennies: word; end;

{$IFNDEF PENNIESLIB}

function PenniesToCoins(TotPennies: word; CoinsRec: PCoinsRec): word; StdCall; {$ENDIF}

implementation

{$IFNDEF PENNIESLIB} { definicja importowanej funkcji } function PenniesToCoins; external 'PENNIESLIB.DLL' name 'PenniesToCoins'; {$ENDIF}

end.

Funkcja PenniesToCoins() posiada dwa parametry wywoania: rozmienian kwot pienidzy oraz wskanik do rekordu TCoinsRec reprezentujcego stan monet po rozmienieniu kwoty. Wynikowa liczba monet stanowi wynik funkcji. Na uwag zasuguje te symbol kompilacji warunkowej PENNIESLIB. Jak wyjanilimy wczeniej, modu PenniesInt spenia dwojakiego rodzaju rol: definiuje typ TCoinsRec oraz importuje z biblioteki funkcj PenniesToCoins(). W pierwszym przypadku jest on czci projektu tworzcego bibliotek i deklaracja oraz
3 Funkcje te s eksportowane przez bibliotek, rwnie dobrze mog by jednak uwaane jako importowane przez modu importowy (przyp. tum.).

281

definicja funkcji PenniesToCoins() s zupenie niepotrzebne. Elementy te nie s widoczne dla kompilatora, poniewa wspomniany symbol PENNIESLIB jest w tym przypadku zdefiniowany. Natomiast w sytuacji, gdy modu PenniesInt peni rol moduu importowego, jest on czci projektu aplikacji i istotna jest caa jego tre. Zwr take uwag na posta dyrektywy external w definicji funkcji PenniesToCoins(). Specyfikuje ona importowanie przez nazw w bibliotece o nazwie PENNIES.LIB poszukiwana jest funkcja o nazwie PenniesToCoins.
Wskazwka

Definiowanie symboli warunkowych obowizujcych w caym projekcie moe odbywa si na dwa sposoby: za pomoc dyrektywy {$DEFINE lub za pomoc opcji projektu, na karcie Directories/Conditionals. Pamitaj, i po zmianie opcji projektu naley skompilowa w projekt w trybie Build kompilacja w trybie Make nie uwzgldnia tych moduw, ktrych tre nie zostaa zmodyfikowana w sposb jawny.

Notatka

Modu PenniesInt ilustruje jeden ze sposobw importowania funkcji (procedury) na podstawie jej nazwy:

external nazwa_biblioteki name nazwa_funkcji

Alternatyw jest import oparty na indeksach przypisanych procedurom (funkcjom) w dyrektywie exports biblioteki DLL:

external nazwa_biblioteki index indeks_funkcji

Cho import na podstawie indeksu funkcji jest rozwizaniem efektywniejszym, jest zdecydowanie odradzany ze wzgldu na wygod uytkownika: zapamitanie indeksu konkretnej funkcji w konkretnej bibliotece DLL jest trudniejsze ni zapamitanie jej nazwy, ponadto pozycja funkcji moe zosta zmieniona podczas unowoczeniania moduu, natomiast nazwa jest znacznie mniej podatna na tego typu zmiany.

Dla uytkownika kocowego, wykorzystujcego funkcj PenniesToCoins() na potrzeby aplikacji tworzonych w Delphi, niezbdne s wic co najmniej dwa pliki: biblioteka PenniesLib.dll i skompilowany modu PenniesInt.dcu, bd te jego wersja rdowa PenniesInt.pas, najlepiej z usunitymi sekwencjami {$IFNDEF PENNIESLIB ENDIF}. Bibliotek PenniesLib.dll mona oczywicie wykorzystywa w innych jzykach programowania (np. w C++Builderze), sposoby importowania funkcji PenniesToCoins() bd jednak charakterystyczne dla tyche jzykw.

Formularze modalne w bibliotekach DLL


Pokaemy teraz, jak zamkn w bibliotece DLL utworzony w Delphi formularz, przeznaczony do wywietlenia w sposb modalny. Formularz stanie si dziki temu dostpny dla dowolnego 32-bitowego rodowiska programowania w Windows, na przykad C++Buildera, Visual Basica itp. Formularz ten zawiera komponent TCalendar. Aplikacja, wywoujc importowan z biblioteki funkcj ShowCalendar(), powoduje modalne wywietlenie formularza uytkownik ma wwczas moliwo wyboru konkretnej daty, ktra po zamkniciu formularza zwracana jest jako wynik wspomnianej funkcji. Na wydruku 6.3 prezentujemy modu rdowy wspomnianego formularza wywietlanego w sposb modalny; jest on czci projektu biblioteki o nazwie CalendarLib.dll.

282

Wydruk 6.3. Modu rdowy formularza wywietlanego w sposb modalny


unit DLLFrm;

interface

uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, Grids, Calendar;

type

TDLLForm = class(TForm) calDllCalendar: TCalendar; procedure calDllCalendarDblClick(Sender: TObject); end;

{ deklaracja eksportowanej funkcji } function ShowCalendar(AHandle: THandle; ACaption: String): TDateTime; StdCall;

implementation {$R *.DFM}

function ShowCalendar(AHandle: THandle; ACaption: String): TDateTime; var DLLForm: TDllForm; begin // kopiuj uchwyt aplikacji do obiektu Application biblioteki DLL Application.Handle := AHandle; DLLForm := TDLLForm.Create(Application); try DLLForm.Caption := ACaption; DLLForm.ShowModal; Result := DLLForm.calDLLCalendar.CalendarDate; // przeka wybran dat // jako wynik

finally DLLForm.Free; end; end;

procedure TDLLForm.calDllCalendarDblClick(Sender: TObject); begin Close; end;

end.

283

Zwr uwag, i obiekt formularza jest wewntrznym obiektem funkcji ShowCalendar() wskazujca go zmienna jest zmienn lokaln funkcji; tworzona automatycznie definicja globalnej zmiennej formularza zostaa rcznie usunita z treci moduu. Pierwsz czynnoci, ktr wykonuje funkcja ShowCalendar(), jest waciwe ustawienie tzw. uchwytu aplikacyjnego. Kady stworzony w Delphi modu wykonywalny, zarwno plik .EXE, jak i biblioteka .DLL, zawiera swj wasny obiekt Application. Waciwo Handle tego obiektu zawiera uchwyt reprezentujcy aplikacj w systemie; uchwyt ten wykorzystywany jest do komunikowania si aplikacji z niskopoziomowymi funkcjami Win32 API. Poniewa wywietlony formularz funkcjonuje jako modalne okno aplikacji, waciwo Handle obiektu Application biblioteki DLL musi zawiera uchwyt aplikacji gwnej, nie uchwyt biblioteki; ponadto obiekt Application biblioteki musi by wacicielem tworzonego obiektu formularza. Wyglda na to, i tworzony formularz jest niejako na si kojarzony z aplikacj macierzyst to prawda, lecz zaniedbanie tej czynnoci skutkowaoby jego bdnym zachowaniem si, szczeglnie w przypadku prby jego minimalizacji. Utworzony obiekt formularza jest nastpnie wywietlany w sposb modalny. Jest on zamykany (i zwalniany) wskutek dwukrotnego kliknicia komponentu kalendarza, za wybrana ostatnio data zwracana jest jako wynik funkcji.
Ostrzeenie

Jeeli ktrakolwiek z procedur (funkcji) eksportowanych z biblioteki uywa jako parametrw (i ew. w charakterze wyniku) dugich acuchw lub tablic dynamicznych, konieczne jest umieszczenie nazwy ShareMem na pierwszym miejscu dyrektywy uses pliku *.dpr zwizanego z bibliotek; dotyczy to wszystkich dugich acuchw i tablic dynamicznych, take tych zagniedonych w rekordach i klasach.

Modu ShareMem.Pas jest moduem importowym biblioteki Borlndmm.dll. Jej uycie jest konieczne w sytuacji, gdy przekazywany dugi acuch (lub tablica dynamiczna) zmienia swj modu-waciciela czyli np. definiowany jest w module .EXE, lecz obsugiwany w ramach funkcji znajdujcej si w bibliotece .DLL. Przekazywanie do innych moduw wskanikw do dugich acuchw, stanowicych np. wynik ich rzutowania na typ PChar, nie powoduje zmiany wasnoci (wywoywana funkcja nie wykonuje na otrzymanym wskaniku adnych operacji charakterystycznych dla dugich acuchw), nie wymaga wic uywania biblioteki Borlndmm.dll.

Wyjtkiem od opisanej zasady jest przekazywanie dugich acuchw i tablic dynamicznych pomidzy moduami tworzonymi z udziaem pakietw nie wymaga to uywania biblioteki Borlndmm.dll, bo alokator pamici jest wwczas wsplny dla wszystkich takich moduw.

Naley ponadto zaznaczy, i biblioteka Borlndmm.dll nadaje si do uycia jedynie przez moduy stworzone w Delphi i C++Builderze; biblioteki przeznaczone dla innych rodowisk nie powinny w ogle eksportowa procedur i funkcji uywajcych w charakterze parametrw (i wyniku) dugich acuchw i tablic dynamicznych, z prostej przyczyny s to obiekty charakterystyczne dla Delphi i C++Buildera i inne rodowiska nie maj pojcia o ich obsudze!

Formularze niemodalne w bibliotekach DLL


Pokaemy teraz, jak mona wykorzysta ten sam formularz w sposb niemodalny. Zawierajca go biblioteka DLL powinna posiada przynajmniej dwa podprogramy, wykonujce dwie podstawowe czynnoci, to jest tworzenie i zwalnianie formularza. Przykadowy projekt o nazwie CalendarMLlib.Dpr realizuje tak wanie bibliotek; wspomniane funkcje nosz nazwy (odpowiednio) ShowCalendar() oraz CloseCalendar(). Modu rdowy formularza, nieznacznie rnicego si od tego wywietlanego w wersji modalnej, przedstawiamy na wydruku 6.4.

284

Wydruk 6.4. Modu rdowy formularza wywietlanego w sposb niemodalny


unit DLLFrm;

interface

uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, Grids, Calendar;

type

TDLLForm = class(TForm) calDllCalendar: TCalendar; end;

{ deklaracja eksportowanych funkcji } function ShowCalendar(AHandle: THandle; ACaption: String): Longint; stdCall; procedure CloseCalendar(AFormRef: Longint); stdcall;

implementation {$R *.DFM}

function ShowCalendar(AHandle: THandle; ACaption: String): Longint; var DLLForm: TDllForm; begin // kopiuj uchwyt aplikacji do obiektu Application biblioteki DLL Application.Handle := AHandle; DLLForm := TDLLForm.Create(Application); Result := Longint(DLLForm); DLLForm.Caption := ACaption; DLLForm.Show; end;

procedure CloseCalendar(AFormRef: Longint); begin if AFormRef > 0 then TDLLForm(AFormRef).Release; end;

end.

Funkcja ShowCalendar() przypisuje uchwyt aplikacji wywoujcej waciwoci Application.Handle, tworzy egzemplarz formularza, nadaje mu dany tytu i w kocu wywietla go w sposb niemodalny.

285

Wynikiem zwracanym przez funkcj jest wskanik do utworzonego egzemplarza, dla uniwersalnoci rzutowany tutaj na liczb typu Longint (32-bitowa liczba cakowita jest w Win32 bardziej uniwersalna ni pascalowy pointer). Nie naley jednak utosamia tej liczby cakowitej z typowym uchwytem Windows uycie jej jako argumentu procedury CloseHandle() na pewno nie spowoduje zamknicia formularza, a dodatkowo moe powodowa inne niepodane efekty. Chcc zamkn formularz, musimy przekaza t liczb jako argument wywoania funkcji CloseCalendar(). Zwalnia ona obiekt formularza za pomoc jego metody Release() metoda ta wywouje destruktor Destroy(), uprzednio jednak doprowadza do obsuenia wszystkich zdarze i komunikatw zwizanych z formularzem.

Wykorzystywanie bibliotek DLL w aplikacjach Delphi


Na pocztku niniejszego rozdziau wspomnielimy o istnieniu dwch sposobw wykorzystywania bibliotek DLL w aplikacjach domylnego i jawnego. Obecnie zaprezentujemy obydwa te sposoby na przykadzie aplikacji testowych, dziaajcych na podstawie stworzonych przed chwil bibliotek.

Automatyczne adowanie biblioteki DLL


Pierwsza aplikacja Pennies.dpr wykorzystuje funkcj PenniesToCoins() znajdujc si w bibliotece PenniesLib.Dll. Formularz aplikacji zawiera jedn kontrolk edycyjn TMaskEdit, jeden przycisk TButton i dziewi etykiet TLabel. Uytkownik wprowadza dan kwot do kontrolki edycyjnej, po czym naciska wspomniany przycisk w odpowiedzi aplikacja oblicza liczb monet kadego rodzaju i wywietla wynik pod postaci czterech etykiet. Kod formularza gwnego aplikacji przedstawia wydruk 6.5.

Wydruk 6.5. Formularz gwny projektu wykorzystujcego bibliotek PenniesLib.dll


unit MainFrm;

interface

uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Mask;

type

TMainForm = class(TForm) lblTotal: TLabel; lblQlbl: TLabel; lblDlbl: TLabel; lblNlbl: TLabel; lblPlbl: TLabel; lblQuarters: TLabel; lblDimes: TLabel; lblNickels: TLabel;

286

lblPennies: TLabel; btnMakeChange: TButton; meTotalPennies: TMaskEdit; procedure btnMakeChangeClick(Sender: TObject); procedure meTotalPenniesChange(Sender: TObject); end;

var MainForm: TMainForm;

implementation uses PenniesInt; // modu importowy biblioteki

{$R *.DFM}

procedure TMainForm.btnMakeChangeClick(Sender: TObject); var CoinsRec: TCoinsRec; TotPennies: word; begin // Wywoaj funkcj zawart w bibliotece DLL TotPennies := PenniesToCoins(StrToInt(meTotalPennies.Text), @CoinsRec); with CoinsRec do begin { Wywietl informacj wynikow } lblQuarters.Caption := IntToStr(Quarters); lblDimes.Caption lblNickels.Caption lblPennies.Caption end end; := IntToStr(Dimes); := IntToStr(Nickels); := IntToStr(Pennies);

// A. Grayski procedure TMainForm.meTotalPenniesChange(Sender: TObject); // Usu z formularza informacj wynikow // na czas wprowadzania kwoty wejciowej begin lblQuarters.Caption := ''; lblDimes.Caption lblNickels.Caption lblPennies.Caption end; := ''; := ''; := '';

end.

287

Biblioteka DLL adowana jest automatycznie w momencie rozpoczcia aplikacji, a funkcja PenniesToCoins() osigalna jest za porednictwem moduu importowego PenniesInt.pas; modu ten zawiera ponadto definicj wykorzystywanego przez aplikacj rekordu TCoinsRec. Tak naprawd uywanie moduw importowych nie jest obowizkowe funkcj PenniesToCoins() mona by zaimportowa z biblioteki w sposb bezporedni, poprzez umieszczenie w sekcji implementation moduu MainFrm nastpujcej definicji:
function PenniesToCoins(TotPennies: word; CoinsRec: PCoinsRec): word; StdCall; external 'PENNIESLIB.DLL';

Naleaoby oczywicie umieci w module MainFrm take definicj rekordu TCoinsRec, gdy konsekwencj pominicia moduu importowego jest konieczno przeniesienia (do aplikacji wywoujcej) zawartych w nim deklaracji struktur danych. Opaca si to jednak tylko w przypadku bardzo prostych bibliotek, wykorzystywanych przez niewiele aplikacji.
Wskazwka

Wielu bibliotekom DLL, rozpowszechnianym przez niezalenych wytwrcw, towarzysz czsto nie moduy importowe Pascala, lecz biblioteki importowe (import libraries) dla jzyka C i C++. Przetumaczenie biblioteki importowej na rwnowany modu importowy nie jest zbyt skomplikowane, zwaszcza, jeeli posuymy si tabel 2.5 (z rozdziau 2.) zawierajc zestawienie rwnowanych typw danych.

adowanie biblioteki DLL na danie


Cho automatyczne adowanie bibliotek DLL jest bardzo wygodne i proste w obsudze, nie zawsze jest rozwizaniem najwaciwszym. Zamy, e aplikacja, do ktrej na wszelki wypadek przycza si domylnie kilkadziesit bibliotek DLL, zawierajcych ogem kilka tysicy funkcji, w typowym zastosowaniu ogranicza si do wywoania zaledwie kilku z nich powraca wic pod inn postaci podstawowy mankament czenia statycznego. Ponadto rzadko si zdarza, by wykorzystywane przez aplikacj biblioteki DLL potrzebne byy wszystkie na raz; zazwyczaj w danej chwili potrzebna jest tylko jedna z nich, a (potencjalnie) due obiekty i struktury definiowane w pozostaych bibliotekach niepotrzebnie zajmuj pami. Najwaciwszym wyjciem z tej sytuacji jest z pewnoci odoenie adowania biblioteki do czasu, kiedy stanie si ona faktycznie potrzebna a wic zastosowanie czenia jawnego, bardziej co prawda elastycznego, ale wymagajcego bardziej zaawansowanej obsugi. Na zaczonym krku CD-ROM, w podkatalogu ModalDLL, znajduje si projekt CalendarTest.dpr dokonujcy modalnego wywietlenia formularza znajdujcego si w bibliotece DLL. Projekt ten stanowi jednoczenie ilustracj jawnego czenia biblioteki DLL z aplikacj; kod jego moduu gwnego przedstawiamy na wydruku 6.6. Wydruk 6.6. Ilustracja jawnego czenia biblioteki DLL
unit MainFfm;

interface

uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;

type

288

// zdefiniuj typ odpowiadajcy typowi importowanej funkcji TShowCalendar = function (AHandle: THandle; ACaption: String): TDateTime; StdCall;

// zdefiniuj now klas wyjtku zwizan z bdem adowania biblioteki EDLLLoadError = class(Exception);

TMainForm = class(TForm) lblDate: TLabel; btnGetCalendar: TButton; procedure btnGetCalendarClick(Sender: TObject); end;

var MainForm: TMainForm;

implementation

{$R *.DFM}

procedure TMainForm.btnGetCalendarClick(Sender: TObject); var LibHandle : THandle;

ShowCalendar: TShowCalendar; begin

{ sprbuj zaadowa bibliotek } LibHandle := LoadLibrary('CALENDARLIB.DLL'); try { zerowa warto LibHandle oznacza, e adowanie nie powiodo si; wygeneruj wyjtek } if LibHandle = 0 then raise EDLLLoadError.Create('Bd adowania biblioteki DLL');

{ jeeli adowanie powiodo si, wykonanie programu jest kontynuowane; uzyskaj adres danej funkcji } @ShowCalendar := GetProcAddress(LibHandle, 'ShowCalendar'); { jeeli udao si uzyska adres funkcji, wywoaj j i wywietl zwracany przez ni wynik; jeeli nie udao si uzyska adresu funkcji, wygeneruj wyjtek }

if not (@ShowCalendar = nil)

289

then lblDate.Caption := DateToStr(ShowCalendar(Application.Handle, Caption)) else RaiseLastWin32Error; finally FreeLibrary(LibHandle); // zwolnij bibliotek DLL end; end;

end.

Pierwsz czynnoci podczas jawnego czenia jest zaadowanie biblioteki. Dokonuje tego funkcja API
LoadLibrary():
Function LoadLibrary(lpLibFileName: PChar): HMODULE; stdcall;

Jej niezerowy wynik oznacza pomylne zaadowanie biblioteki i jest jednoczenie uchwytem jej instancji. Kolejna czynno to uzyskanie adresu procedury ShowCalendar(): czynno t wykonuje funkcja GetProcAddress() na podstawie uchwytu instancji biblioteki oraz nazwy szukanej funkcji:

function GetProcAddress(Module: HMODULE; lpProcName: LPCSTR):FARPROC; stdcall;

Wynik zwracany przez funkcj GetProcAddress() jest wskanikiem amorficznym (FARPROC znaczy tyle samo co pointer), wic wywoanie funkcji wymaga jego rzutowania na typ zgodny z deklaracj wywoywanej funkcji:

TShowCalendar = function (AHandle: THandle; ACaption: String): TDateTime; StdCall;

Zwolnienie (rozadowanie) biblioteki DLL nastpuje w wyniku wywoania funkcji FreeLibrary():


function FreeLibrary(Module: HMODULE):BOOL; stdcall;

Wspominalimy ju wczeniej, i system operacyjny stara si minimalizowa liczb zaadowanych egzemplarzy biblioteki DLL, dzielc jej pojedyncz kopi pomidzy kilka aplikacji zawsze, gdy jest to moliwe. W zwizku z tym wywoanie funkcji LoadLibrary() niekoniecznie musi skutkowa fizycznym adowaniem biblioteki, lecz moe sprowadza si do zwikszenia (o 1) licznika odwoa zwizanego z jej zaadowanym egzemplarzem; na podobnej zasadzie funkcja FreeLibrary() zmniejsza o 1 warto wspomnianego licznika; fizyczne zwolnienie biblioteki nastpuje tylko wwczas, gdy wynikiem dekrementacji licznika jest jego zerowa warto. Opisany scenariusz (zaadowanie biblioteki, uzyskanie adresu funkcji, wywoanie funkcji, zwolnienie biblioteki) realizowany jest w ramach procedury wywoywanej klikniciem jedynego przycisku znajdujcego si na formularzu. Niemono zaadowania biblioteki lub uzyskania adresu funkcji powoduje wygenerowanie wyjtku; bezwarunkowe zwolnienie (ewentualnie) zaadowanej biblioteki zapewnione zostao przez umieszczenie wywoania funkcji FreeLibrary() w ramach bloku finally. Zwr uwag, i kade wywoanie funkcji ShowCalendar() wie si z adowaniem i zwalnianiem biblioteki (dokadniej wywoaniem LoadLibrary() i FreeLibrary()). Nie stanowi to problemu w przypadku wywoania jednokrotnego, lecz przy wywoaniu wielokrotnym skutkowa moe pewnym wydueniem czasu realizacji programu.

290

Procedura inicjujco-koczca biblioteki DLL


Kada biblioteka doczana do procesu lub odczana od niego moe by o tym fakcie powiadamiana przez Win32; ponadto w czasie, gdy jest ona przyczona do procesu, moe by powiadamiana o utworzeniu albo zwolnieniu kadego wtku. Owo powiadamianie realizowane jest za porednictwem tzw. procedury inicjujcokoczcej (DLL Entry/Exit Function). Mechanizm ten moe by z wielu wzgldw uyteczny, gdy uatwia wykonywanie typowych operacji inicjujcych i koczcych, zwizanych z obecnoci biblioteki w procesie np. nadawanie wartoci zmiennym globalnym, rejestracj klas, tworzenie i usuwanie plikw roboczych itp. Sprawowanie przez bibliotek kontroli nad gospodark wtkami procesu moe by natomiast pomocne ze wzgldu na moliwo wielodostpnego jej wykorzystywania.

Definiowanie procedury inicjujco-koczcej


Z kad instancj biblioteki DLL zwizana jest w Delphi globalna zmienna o nazwie DLLProc, zawierajca wskazanie na zdefiniowan przez uytkownika procedur inicjujco-koczc. Pocztkow wartoci tej zmiennej jest NIL, co oznacza, e biblioteka realizuje jedynie standardowy dla Delphi sposb powiadamiania, czyli wykonanie bloku beginend pliku .DPR projektu tworzcego bibliotek. Przypisujc wspomnianej zmiennej funkcj zdefiniowan przez uytkownika, moemy poszerzy owo powiadamianie na trzy pozostae przypadki. Procedura inicjujco-koczca powinna posiada pojedynczy parametr typu DWORD. Jego warto informuje o tym, ktra z czterech moliwych przyczyn powiadamiania spowodowaa wywoanie procedury, zgodnie z opisem w poniszej tabeli. Tabela 6.1. Przyczyny wywoania procedury inicjujco-koczcej Warto parametru
DLL_PROCESS_ATTACH

Przyczyna Biblioteka DLL zostaa wczona do przestrzeni adresowej procesu bd przez zaadowanie domylne, bd w wyniku pierwszego wywoania LoadLibrary(). Biblioteka zostaa odczona od procesu bd na skutek jego zakoczenia, bd te w wyniku wyzerowania licznika odwoa spowodowanego przez funkcj FreeLibrary().

DLL_PROCESS_DETACH

DLL_THREAD_ATTACH

Proces utworzy nowy wtek; procedura inicjujcokoczca wywoywana jest w kontekcie nowo utworzonego wtku. Zakoczy si wtek procesu; procedura inicjujcokoczca wywoywana jest w kontekcie koczcego si wtku.

DLL_THREAD_DETACH

Ostrzeenie

Zakoczenie wtku za pomoc wywoania procedury TerminateThread() nie gwarantuje wywoania z parametrem DLL_THREAD_DETACH.

Przykad zastosowania procedury inicjujco-koczcej przedstawia wydruk 6.7. Prezentowany plik znajduje si na zaczonym krku CD-ROM pod nazw DLLEntryLib.dpr. Wydruk 6.7. Zastosowanie procedury inicjujco-koczcej biblioteki DLL w Delphi
library DLLEntryLib;

291

uses SysUtils, Windows, Dialogs, Classes;

procedure DLLEntryExitProc(dwReason: DWord); begin case dwReason of DLL_PROCESS_ATTACH: begin ShowMessage('Przyczenie do procesu'); end;

DLL_PROCESS_DETACH: begin ShowMessage('Odczenie od procesu'); end;

DLL_THREAD_ATTACH:

begin ShowMessage('Uruchomienie wtku'); end;

DLL_THREAD_DETACH:

begin ShowMessage('Zatrzymanie wtku'); end;

end;

end;

begin { przypisz procedur inicjujco-koczc do odpowiedniej zmiennej } DllProc := @DLLEntryExitProc;

{ wywoaj procedur inicjujco-koczc z parametrem DLL_PROCESS_ATTACH } DllProc(DLL_PROCESS_ATTACH); end.

Zdefiniowana przez uytkownika procedura inicjujco-koczca przypisywana jest zmiennej DLLProc; odbywa si to w ramach bloku inicjujcego beginend. Blok ten jest w Delphi wykonywany zamiast standardowego wywoania procedury inicjujco-koczcej z parametrem DLL_PROCESS_ATTACH, by wic pozosta w zgodzie ze standardami Win32, naley wywoanie to zrealizowa samodzielnie. Funkcjonowanie samej procedury inicjujco-koczcej sprowadza si tu do wypisania komunikatu informujcego o przyczynie wywoania. Aby zaobserwowa funkcjonowanie procedury inicjujco-koczcej jakiej biblioteki, naley bibliotek t wykorzysta w jakim projekcie realizujcym aplikacj wielowtkow. Projekt taki, o nazwie DllEntryTest.dpr, znajduje si na zaczonym krku CD-ROM; kod jego moduu gwnego przedstawiamy na wydruku 6.8. Wydruk 6.8. Modu gwny projektu aplikacji ilustrujcej funkcjonowanie procedury inicjujco-koczcej biblioteki DLL

292

unit MainFrm;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, Gauges;

type

{ zdefiniuj klas wtku } TTestThread = class(TThread) procedure Execute; override; procedure SetCaptionData; end;

TMainForm = class(TForm) btnLoadLib: TButton; btnFreeLib: TButton; btnCreateThread: TButton; btnFreeThread: TButton; lblCount: TLabel; procedure btnLoadLibClick(Sender: TObject); procedure btnFreeLibClick(Sender: TObject); procedure btnCreateThreadClick(Sender: TObject); procedure btnFreeThreadClick(Sender: TObject); procedure FormCreate(Sender: TObject); private LibHandle TestThread Counter GoThread end; : THandle; : TTestThread; : Integer; : Boolean;

var MainForm: TMainForm;

implementation

{$R *.DFM}

procedure TTestThread.Execute; begin FreeOnTerminate := TRUE; // A. Grayski while MainForm.GoThread do begin

293

Synchronize(SetCaptionData); Inc(MainForm.Counter); end; end;

procedure TTestThread.SetCaptionData; begin MainForm.lblCount.Caption := IntToStr(MainForm.Counter); end;

procedure TMainForm.btnLoadLibClick(Sender: TObject); { zaadowanie biblioteki } begin if LibHandle = 0 then begin LibHandle := LoadLibrary('DLLENTRYLIB.DLL'); if LibHandle = 0 then raise Exception.Create('Bd adowania biblioteki'); end else MessageDlg('Biblioteka jest ju zaadowana', mtWarning, [mbok], 0); end;

procedure TMainForm.btnFreeLibClick(Sender: TObject); { zwolnienie biblioteki } begin if not (LibHandle = 0) then begin FreeLibrary(LibHandle); LibHandle := 0; end; end;

procedure TMainForm.btnCreateThreadClick(Sender: TObject); { tworzenie nowego wtku; powinno to powodowa wywoanie procedury inicjujco-koczcej KADEJ zaadowanej biblioteki DLL z parametrem DLL_THREAD_ATTACH }

begin if TestThread = nil then begin GoThread := True;

TestThread := TTestThread.Create(False); end; end;

294

procedure TMainForm.btnFreeThreadClick(Sender: TObject); { koczenie wtku; powinno to powodowa wywoanie procedury inicjujco-koczcej KADEJ zaadowanej biblioteki DLL z parametrem DLL_THREAD_DETACH }

begin if TestThread <> nil then begin GoThread := False; // A. Grayski

// TestThread.Free; TestThread := nil; Counter end; := 0;

end;

procedure TMainForm.FormCreate(Sender: TObject); begin LibHandle := 0;

TestThread := nil; end;

end.

Formularz projektu zawiera cztery przyciski, zwizane z czterema przyczynami wywoywania procedury inicjujco-koczcej. Procedura zdarzeniowa zwizana z pierwszym z przyciskw dokonuje zaadowania biblioteki za pomoc funkcji LoadLibrary(). Uchwyt biblioteki przechowywany jest w jednym z pl formularza (LibHandle); jego niezerowa warto oznacza, e biblioteka zostaa ju zaadowana i nastpne kliknicia wspomnianego przycisku naley po prostu zignorowa. Kliknicie drugiego ze wspomnianych przyciskw stanowi polecenie zwolnienia biblioteki; procedura zdarzeniowa przycisku sprawdza wwczas, czy pole LibHandle ma niezerow warto i jeeli tak, to przekazuje t warto jako parametr wywoania funkcji FreeLibrary(). Kolejne dwa przyciski zwizane s z tworzeniem i koczeniem wtku. Wtek reprezentowany jest przez klas
TTestThread. Jej metoda Execute() dokonuje nieustannej inkrementacji i wywietlania na formularzu wartoci licznika (bdcego polem Counter formularza) trwa to dopty, dopki pole GoThread nie osignie wartoci False; warto t nadaje mu procedura obsugi przycisku koczcego wtek. W aplikacji da si uruchomi co najwyej jeden wtek poboczny jego obiekt wskazywany jest przez pole TestThread

formularza. Zwr uwag, i zarwno inkrementacja licznika, jak i jego wywietlanie, realizowane s za pomoc metody Synchronize(). Nasza przykadowa procedura inicjujco-koczca ogranicza sw prac do wywietlania stosownych komunikatw, w rzeczywistych aplikacjach moe ona jednak wykonywa niebagatelne zadania, w rodzaju przydzielania i zwalniania zasobw przeznaczonych dla procesu oraz dla poszczeglnych wtkw.

295

Notatka

Wywoania procedury inicjujco-koczcej z parametrami DLL_THREAD_ATTACH i DLL_THREAD_DETACH maj miejsce tylko wtedy, gdy podczas odpowiednio tworzenia oraz zwalniania wtku dana biblioteka jest przyczona do procesu.

W poniszej sekwencji

1.

Utworzenie wtku

2.

Zaadowanie biblioteki DLL

3.

Zwolnienie wtku

4.

Zwolnienie biblioteki DLL

tylko zwolnienie wtku (3.) zauwaone bdzie przez bibliotek adowan w punkcie 2. Wynika std wany wniosek, i dla danej biblioteki DLL wywoania z parametrami DLL_THREAD_ATTACH oraz DLL_THREAD_DETACH wcale nie musz si bilansowa! Nie mog wic one peni roli inicjujco-koczcej w stosunku do poszczeglnych wtkw (przyp. tum.).

Obsuga wyjtkw w bibliotekach DLL


W 16-bitowym rodowisku Delphi 1 wyjtki stanowiy zjawisko specyficzne dla jzyka programowania i musiay zosta obsuone przed powrotem do moduu wywoujcego:
procedure SomeDLLProc; begin try except on Exception do // nie pozostawiaj wyjtkw nieobsuonych ani ich nie ponawiaj end; end;

Wydostanie si wyjtku na zewntrz biblioteki DLL powodowao, i aplikacja wywoujca zastawaa stos w nieprawidowym stanie, co prawie zawsze stanowio zagroenie dla aplikacji i systemu operacyjnego. Sytuacja ta zmienia si diametralnie wraz z pojawieniem si Delphi 2 wyjtki aplikacji s odtd mapowane w wyjtki Win32, s wic zjawiskami systemowymi, nie za specyficznymi dla konkretnego jzyka programowania. Niezbdne do tego czynnoci wykonywane s w czci inicjacyjnej moduu SysUtils, a wic jego wczenie do aplikacji jest konieczne do tego, by wyjtki zaistniae wewntrz biblioteki DLL mogy si z niej bezpiecznie wydostawa.

296

Ostrzeenie

Wikszo aplikacji Win32 nie zostaa jednak zaprojektowana w taki sposb, by obsugiwa wyjtki Win32, wic nawet bezpieczne wydostanie si wyjtku z biblioteki DLL moe spowodowa awaryjne zakoczenie aplikacji. Dla pewnoci, zalecane jest wic obsugiwanie przez bibliotek DLL wszystkich generowanych w ramach niej wyjtkw.

Ponadto najlepsza nawet aplikacja stworzona w rodowisku innym ni Delphi nie bdzie w stanie obsuy wyjtkw specyficznych dla klas Object Pascala; wyjtki te s najczciej sygnalizowane jako wyjtki Win32 o kodzie $0EEDFACE. Wyjtek Win32 reprezentowany jest przez nastpujc struktur zdefiniowan w module SysUtils: PExceptionRecord = ^TExceptionRecord; TExceptionRecord = record ExceptionCode: Cardinal; ExceptionFlags: Cardinal; ExceptionRecord: PExceptionRecord; ExceptionAddress: Pointer; NumberParameters: Cardinal; ExceptionInformation: array[0..14] of Cardinal; end; Pierwszy element tablicy ExceptionInformation zawiera wwczas adres wyjtku, drugi natomiast adres obiektu Delphi reprezentujcego wyjtek. Bardziej szczegowe informacje na temat struktury TExceptionRecord znajduj si w systemie pomocy Win32 pod hasem EXCEPTION_RECORD.

Wyjtki a klauzula Safecall


Opatrzenie procedury (funkcji) klauzul Safecall powoduje, i jakikolwiek nieobsuony w jej ramach wyjtek zostanie przekazany do obsugi przez program wywoujcy. Technologicznie odbywa si to przez otoczenie caej treci funkcji ukryt konstrukcj tryexcept, co spowoduje przejcie wyjtkw nieobsuonych na niszych poziomach zagniedenia. W bloku except tej konstrukcji przechwycony wyjtek konwertowany jest (przez procedur SafecallErrorProc()) na liczb cakowit (typ HRESULT) zwracan jako wynik funkcji. Klauzula Safecall implikuje take konwencj wywoania stdcall, tak wic przykadowa funkcja zadeklarowana jako
function Foo(i: integer): string; Safecall;

moe by (koncepcyjnie, nie w sensie skadni) traktowana na rwni z nastpujc funkcj:

function Foo(i: integer):string; HRESULT; stdcall;

Klauzula Safecall jest szczeglnie uyteczna w technologii COM; w sytuacji, gdy (nieobsuony) wyjtek nie powinien wydosta si z wywoywanej funkcji, jest on konwertowany na liczb cakowit, zawierajc informacj o bdzie. Swoj drog, jest to pewnego rodzaju sposb na nieobsugiwane wyjtki generowane w ramach bibliotek DLL.

297

Funkcje zwrotne
Funkcj zwrotn (callback function) nazywana jest funkcja (lub procedura) stanowica cz aplikacji, ale wywoywana asynchronicznie przez bibliotek DLL. Kierunek tego wywoania jest wic niejako odwrotny w stosunku do naturalnego wywoywania, przez aplikacj, funkcji zawartych w bibliotekach; wywoanie zwrotne musi by jednak poczone z normalnym wywoaniem, w ramach ktrego do biblioteki przekazywany jest adres funkcji zwrotnej. Biblioteka Win32 API naszpikowana jest wrcz funkcjami korzystajcymi z odwoa zwrotnych; jednym z przykadw wykorzystania odwoa zwrotnych s wszelkiego rodzaju enumeracje, czyli wywoania okrelonej funkcji zwrotnej w stosunku do kadego obiektu okrelonej grupy, na przykad w stosunku do wszystkich okien pierwszego poziomu (top level), bez uwzgldniania okien potomnych (child windows). Enumeracja po oknach pierwszego poziomu wykonywana jest przez funkcj EnumWindows():
function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;

Pierwszy parametr jest wskanikiem do funkcji zwrotnej, drugi natomiast niesie informacj dodatkow, nieistotn dla Win32 API, wykorzystywan wewntrznie przez funkcj zwrotn. Funkcja zwrotna, okrelona przez pierwszy parametr, wywoana zostanie jednokrotnie dla kadego okna. Powinna by ona dwuparametrow funkcj zwracajc wynik typu Boolean; jako pierwszy parametr przekazany zostanie do niej uchwyt odnonego okna, jako drugi warto okrelona przez drugi parametr wywoania funkcji EnumWindows():

type TFNWndEnumProc = function (Hw: Hwnd; lp: lParam): Boolean; stdcall;

Wynik zwracany przez funkcj zwrotn decyduje o tym, czy enumeracja ma by kontynuowana (True), czy te naley j zatrzyma (False). Ilustracj enumeracji prowadzonej po oknach jest projekt o nazwie CallBack.dpr znajdujcy si na zaczonym krku CD-ROM; jego modu gwny prezentujemy na wydruku 6.9.

Wydruk 6.9. Modu gwny projektu ilustrujcego enumeracj po oknach pierwszego poziomu
unit MainFrm;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls;

type

// zdefiniuj rekord zawierajcy informacj o oknie

TWindowInfo = class WindowName, // nazwa okna

WindowClass: String; // nazwa klasy okna end;

298

TMainForm = class(TForm) lbWinInfo: TListBox; btnGetWinInfo: TButton; hdWinInfo: THeaderControl; procedure btnGetWinInfoClick(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure lbWinInfoDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); procedure hdWinInfoSectionResize(HeaderControl: THeaderControl; Section: THeaderSection); end;

var MainForm: TMainForm;

implementation

{$R *.DFM} function EnumWindowsProc(Hw: HWnd; AMainForm: TMainForm): Boolean; stdcall; // niniejsza funkcja jest funkcj zwrotn wywoywan z wntrza // przez biblioteki User32.DLL dla kadego gwnego okna w systemie

var WinName, CName: array[0..144] of char; WindowInfo: TWindowInfo; begin // domylna warto, nakazujca kontynuowanie enumeracji Result := True;

GetWindowText(Hw, WinName, 144); // pobierz tytu okna GetClassName(Hw, CName, 144); // pobierz nazw klasy okna

{ stwrz obiekt klasy TWindowInfo zawierajcy informacj o oknie i dodaj go do listy Listbox1 }

WindowInfo := TWindowInfo.Create; with WindowInfo do begin SetLength(WindowName, strlen(WinName)); SetLength(WindowClass, StrLen(CName)); WindowName := StrPas(WinName); WindowClass := StrPas(CName); end;

299

MainForm.lbWinInfo.Items.AddObject('', WindowInfo);

end;

procedure TMainForm.btnGetWinInfoClick(Sender: TObject); begin { Wykonaj enumeracj po wszystkich oknach gwnych, wykorzystujc funkcj EnumWindowsProc() w charakterze funkcji zwrotnej }

EnumWindows(@EnumWindowsProc, 0); end;

procedure TMainForm.FormDestroy(Sender: TObject); var i: integer; begin { zwolnij wszystkie obiekty TWindowInfo } for i := 0 to lbWinInfo.Items.Count - 1 do TWindowInfo(lbWinInfo.Items.Objects[i]).Free end;

procedure TMainForm.lbWinInfoDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); begin { wyczy obszar ptna, na ktrym wypisywana bdzie informacja } lbWinInfo.Canvas.FillRect(Rect);

{ wypisz informacj zawart w rekordzie TWindowInfo zawartym w licie ListBox1 na pozycji "index". Szeroko poszczeglnych kolumn okrelaj separatory w nagwku HeaderControl1 }

with TWindowInfo(lbWinInfo.Items.Objects[Index]) do begin DrawText(lbWinInfo.Canvas.Handle, PChar(WindowName), Length(WindowName), Rect,dt_Left or dt_VCenter);

{ dostosuj pooenie i szeroko wywietlanej informacji do rubryki okrelonej przez nagwek }

300

Rect.Left := Rect.Left + hdWinInfo.Sections[0].Width; DrawText(lbWinInfo.Canvas.Handle, PChar(WindowClass), Length(WindowClass), Rect, dt_Left or dt_VCenter); end; end;

procedure TMainForm.hdWinInfoSectionResize(HeaderControl: THeaderControl; Section: THeaderSection); begin lbWinInfo.Invalidate; // wywietl ponownie zawarto listy end;

end.

Funkcja zwrotna, wywoywana w kontekcie konkretnego okna, pobiera jego tytu oraz nazw jego klasy i zapisuje te informacje w obiekcie TWindowInfo, dodawanym nastpnie do listy wywietlanej na formularzu. Poniewa posta wywietlanej informacji musi by dostosowana do ukadu kolumnowego narzuconego przez komponent THeaderControl, jest ona wypisywana w sposb specyficzny dla listy (owner drawing) w ramach zdarzenia OnDrawItem. Zajmiemy si najpierw dziaaniem samej funkcji zwrotnej, nastpnie wyjanimy szczegy wspomnianego rysowania specyficznego.

Dziaanie funkcji zwrotnej


Funkcja zwrotna nosi nazw EnumWindowsProc() i posiada dwa parametry. Pierwszy parametr jest uchwytem okna, ktrego dotyczy wywoanie, drugi natomiast ma warto 0 i nie jest do niczego wykorzystywany; warto ta przekazywana jest jako drugi parametr wywoania funkcji EnumWindows(). Jak kada funkcja zwrotna, funkcja EnumWindows() stosuje standardow dla Win32 konwencj wywoania stdcall. Na podstawie otrzymanego uchwytu okna funkcja pobiera jego tytu oraz nazw klasy i zapisuje te informacje w nowo tworzonym obiekcie klasy TWindowInfo. Wskanik tego obiektu jest nastpnie dodawany do tablicy Objects listy lbWinInfo, sucej do wywietlania informacji na formularzu gwnym. Zwr uwag na wany fakt, i destruktor listy TListBox nie zwalnia obiektw wskazywanych przez jego tablic Objects i uytkownik musi dokona tego zwolnienia w sposb jawny; w naszym przypadku zwolnienie to odbywa si w ramach zdarzenia OnDestroy formularza. Enumeracja rozpoczyna si w momencie kliknicia jedynego przycisku formularza gwnego obsuga tego kliknicia sprowadza si do wywoania funkcji EnumWindows() z odpowiednimi parametrami. Funkcja EnumWindowsProc() zwraca zawsze warto True, enumeracja jest wic wykonywana dla wszystkich okien pierwszego poziomu.

Specyficzne wywietlanie elementw listy


Jedyn informacj zawart w licie lbWinInfo s wskaniki do obiektw TWindowInfo. Waciwo Items nie zawiera adnego acucha, zatem lista musi by wywietlona w sposb specyficzny; ustawiamy wic jej waciwo Style na lbOwnerDrawFixed. Odpowiedzialno za wywietlenie konkretnego jej elementu spoczywa odtd na procedurze obsugujcej zdarzenie OnDrawItem. Obsuga zdarzenia OnDrawItem rozpoczyna si od wypisania pierwszego acucha zawierajcego tytu okna. W miejscu, gdzie rozpoczyna si druga kolumna, przeznaczona na nazw klasy okna, nadpisywany jest na niego drugi acuch, zawierajcy t nazw. Szeroko pierwszej rubryki pobierana jest z waciwoci Sections[0].Width komponentu nagwkowego THeaderControl.

301

Wywoywanie funkcji zwrotnych z bibliotek DLL


W poprzednim przykadzie organizacj iteracji i wywoywaniem funkcji zwrotnej zajmowa si sam system operacyjny. Obecnie zademonstrujemy przykad wywoania funkcji zwrotnej przez zewntrzn bibliotek DLL. Przykadem takiej biblioteki jest projekt StrSrchLib.dpr, ktrego tre przedstawiamy na wydruku 6.10.

Wydruk 6.10. Przykad biblioteki DLL dokonujcej odwoa zwrotnych


library StrSrchLib;

uses Wintypes, WinProcs, SysUtils, Dialogs;

type { zadeklaruj typ funkcji zwrotnej } TFoundStrProc = procedure(StrPos: PChar); StdCall;

function SearchStr(ASrcStr, ASearchStr: PChar;

AProc: TFarProc): Integer; StdCall;

{ Niniejsza funkcja szuka wystpienia podacucha ASearchStr w acuchu ASrcStr. W przypadku jego znalezienia wywoywana jest funkcja zwrotna identyfikowana przez AProc ze znalezionym wystpieniem acucha jako parametrem. Poszukiwanie jest nastpnie kontynuowane w celu znalezienia ewentualnych dalszych wystpie. }

var FindStr: PChar; begin FindStr := ASrcStr; FindStr := StrPos(FindStr, ASearchStr); while FindStr <> nil do begin if AProc <> nil then TFoundStrProc(AProc)(FindStr); FindStr := FindStr + 1; FindStr := StrPos(FindStr, ASearchStr); end; end;

exports SearchStr; begin

end.

302

Funkcja SearchStr() poszukuje wystpie okrelonego wzorca w acuchu i dla kadego wystpienia tego wzorca wywouje funkcj zwrotn okrelon przez parametr AProc; jedynym parametrem wywoania tej funkcji jest adres wystpienia wzorca w acuchu. Poniewa adres funkcji zwrotnej przekazywany jest w postaci amorficznego wskanika, musi on by rzutowany na typ zgodny z jej deklaracj; typ ten deklarowany jest jako TFoundStrProc. Kolejny projekt CallBackDemo.dpr jest ilustracj wykorzystania biblioteki StrSrchLib, gdy zawiera definicj wywoywanej przez ni funkcji zwrotnej; tre jego moduu gwnego przedstawiamy na wydruku 6.11.

Wydruk 6.11. Przykadowa aplikacja zawierajca funkcj zwrotn wywoywan z biblioteki DLL

unit MainFrm;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;

type TMainForm = class(TForm) btnCallDLLFunc: TButton; edtSearchStr: TEdit; lblSrchWrd: TLabel; memStr: TMemo; procedure btnCallDLLFuncClick(Sender: TObject); end;

var MainForm: TMainForm; Count: Integer;

implementation

{$R *.DFM}

{ zaimportuj funkcj SearchStr z biblioteki DLL } function external SearchStr(ASrcStr, ASearchStr: PChar; AProc: TFarProc): Integer; StdCall

'STRSRCHLIB.DLL';

{ zdefiniuj funkcj zwrotn wywoywan przez funkcj SearchStr } procedure StrPosProc(AStrPsn: PChar); StdCall; begin inc(Count); // zwiksz licznik wystpie

303

end;

procedure TMainForm.btnCallDLLFuncClick(Sender: TObject); var S: String; S2: String; begin Count := 0; // zainicjuj licznik

{ zapisz w zmiennej S acuch, w ktrym prowadzone bdzie poszukiwanie } SetLength(S, memStr.GetTextLen); memStr.GetTextBuf(PChar(S), memStr.GetTextLen);

{ zapisz w zmiennej S2 poszukiwany wzorzec } S2 := edtSearchStr.Text;

{ wywoaj funkcj z biblioteki, przekazujc acuch rdowy i poszukiwany wzorzec } SearchStr(PChar(S), PChar(S2), @StrPosProc); { wywietl liczb wystpie wzorca w acuchu, obliczon przez funkcj zwrotn }

ShowMessage(Format('%s%s%s Count, 'razy.'])); end;

%d

%s',

['acuch

"',

edtSearchStr.Text,

'"

wystpi',

end.

Przeszukiwanym acuchem jest tutaj zawarto komponentu TMemo, natomiast poszukiwany wzorzec pobierany jest za pomoc kontrolki TEdit. Procedura StrPosProc(), ktra peni rol funkcji zwrotnej, zlicza wystpienia wzorca w przeszukiwanym acuchu.

Wspdzielenie danych biblioteki DLL przez rne procesy


W rodowisku 16-bitowym wspdzielenie danych globalnych biblioteki DLL przez kilka aplikacji byo czym naturalnym i nieuniknionym wszystkie aplikacje dziaay w ramach tej samej przestrzeni adresowej, tote dany fragment biblioteki widziany by przez wszystkie pod tym samym adresem. Taki stan rzeczy powodowa, e biblioteki DLL stanowiy naturalny obszar wymiany danych midzy aplikacjami i niestety rwnie aren niepodanych interferencji, co projektanci zawsze musieli bra pod uwag. Wyjanialimy ju wczeniej zasady wykorzystywania bibliotek DLL w Win32 w warunkach, gdy kada biblioteka DLL istnieje wycznie pod postaci swoich instancji w przestrzeniach adresowych poszczeglnych procesw; wobec rozcznoci tych przestrzeni, tradycyjna technika rodem z Windows 3.x na nic si w tym wypadku nie przyda. Nie oznacza to bynajmniej, i komunikacja dwch aplikacji przez t sam bibliotek DLL jest zupenie niemoliwa; nadal moliwe jest komunikowanie si przez obszar danych biblioteki DLL (co moe by przydatne przy przenoszeniu aplikacji 16-bitowych do Delphi 6), realizowane jest jednak za pomoc zupenie innych 304

rodkw mianowicie mechanizmu plikw odwzorowanych, ktry opisalimy ze szczegami na stronach 580 598 ksiki Delphi 4. Vademecum profesjonalisty. W tym miejscu ograniczymy si tylko do jego wybranych elementw.

Tworzenie bibliotek DLL z pamici dzielon


Poniszy wydruk przedstawia plik projektu biblioteki ShareLib.dll, zawierajcy kod, ktry umoliwia wspdzielenie obszaru danych tej biblioteki. Obszar ten wskazywany jest przez zmienn o nazwie GlobalData.

Wydruk 6.12. Biblioteka DLL umoliwiajca wspdzielenie swego obszaru danych przez rne procesy
library ShareLib;

uses ShareMem, Windows, SysUtils, Classes; const

cMMFileName: PChar = 'SharedMapData';

{$I DLLDATA.INC}

var GlobalData : PGlobalDLLData; MapHandle : THandle;

{ fnkcja importowana z biblioteki DLL } procedure GetDLLData(var AGlobalData: PGlobalDLLData); StdCall; begin { pobierz wskazanie na obszar danych globalnych biblioteki } AGlobalData := GlobalData; end;

procedure OpenSharedData; var Size: Integer; begin { pobierz rozmiar mapowanych danych } Size := SizeOf(TGlobalDLLData);

{ Stwrz obiekt mapujcy; zwr uwag, i zamiast uchwytu odwzorowywanego pliku wystpuje $FFFFFFFF co oznacza, i plik ten nie bdzie widoczny na zewntrz pod konkretn nazw, lecz stanowi bdzie fragment pliku wymiany.

305

Wymaga to, by obiekt mapujcy posiada unikatow nazw }

MapHandle := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0, Size, cMMFileName);

if MapHandle = 0 then RaiseLastWin32Error;

{ dokonaj mapowania w obszar pamici i przypisz adres tego obszaru do zmiennej GlobalData }

GlobalData := MapViewOfFile(MapHandle, FILE_MAP_ALL_ACCESS, 0, 0, Size);

{ Zainicjuj dane globalne jak zawartoci } GlobalData^.S := 'ShareLib'; GlobalData^.I := 1; if GlobalData = nil then begin CloseHandle(MapHandle); RaiseLastWin32Error; end; end;

procedure CloseSharedData; { zwolnij obszar pamici odwzorowujcy zawarto pliku i obiekt mapujcy } begin UnmapViewOfFile(GlobalData); CloseHandle(MapHandle); end;

procedure DLLEntryPoint(dwReason: DWord); begin case dwReason of DLL_PROCESS_ATTACH: OpenSharedData; DLL_PROCESS_DETACH: CloseSharedData; end; end;

exports GetDLLData;

306

begin { przypisz procedur inicjujco-koczc } DllProc := @DLLEntryPoint;

DLLEntryPoint(DLL_PROCESS_ATTACH); end.

Wspdzielone dane posiadaj nastpujc struktur, zdefiniowan w doczonym pliku DLLDATA.INC:


PGlobalDLLData = ^TGlobalDLLData; TGlobalDLLData = record S: String[50]; I: Integer; end;

Kod biblioteki wykorzystuje mechanizm procedury inicjujco-koczcej; za jej porednictwem wywoywane s dwie procedury: OpenSharedData() (przy rozpoczynaniu programu) oraz CloseSharedData() (przy jego koczeniu). Mechanizm plikw odwzorowanych umoliwia mwic oglnie zarezerwowanie regionu w wirtualnej przestrzeni adresowej Win32 i zwizanie z nim rzeczywistego fragmentu pamici fizycznej. Przypomina to troch klasyczny przydzia pamici na stercie i odwoywanie si do niej za pomoc wskanika; mechanizm plikw odwzorowanych umoliwia jednak znacznie wicej mianowicie odwoywanie si za pomoc typowego wskanika (pointer) do fragmentu (lub caoci) pliku dyskowego tak, jakby stanowi on fragment pamici procesu. W ten wanie sposb biblioteki DLL wczane s do przestrzeni adresowej procesu w postaci oryginalnej, bd te w postaci relokowanej kopii, o czym pisalimy w zwizku z bazowym adresem adowania. Tworzc odwzorowanie pliku w pami aplikacji, musimy najpierw zwiza z plikiem obiekt realizujcy to odwzorowanie; niezbdnymi informacjami do jego utworzenia s m.in. uchwyt pliku, pocztek i wielko odwzorowywanego obszaru, tryb wykorzystywania pliku oraz nazwa, pod ktr obiekt ten bdzie identyfikowany w systemie. Rozpatrzmy nastpujcy scenariusz. Aplikacja, ktr nazwiemy umownie App1, dokonuje odwzorowania pliku dyskowego o nazwie (na przykad) MYFILE.DAT; od tej chwili moe ona zapisywa i odczytywa dane do i z pliku tak, jakby stanowi on fragment jej przestrzeni adresowej. Jeli teraz, w czasie wykonywania aplikacji App1 inna aplikacja nazwijmy j App2 dokona odwzorowania tego samego pliku, to zmiany dokonane w pliku przez jedn aplikacj bd natychmiast widoczne dla drugiej. Rozczno przestrzeni adresowych obydwu procesw w niczym tu nie przeszkadza w obydwu jest obecne odwzorowanie tego samego pliku. W opisanym scenariuszu konkretna nazwa pliku nie ma adnego znaczenia; wane, by obydwie aplikacje uyway tego samego pliku. Win32 API oferuje w zwizku z tym rozwizanie o wiele bardziej eleganckie: poniewa plik ten nie peni adnej samoistnej roli, moliwe jest uycie zamiast niego wewntrznych struktur pamici wirtualnej; zamiast uchwytu pliku, naley w tym celu poda warto $FFFFFFFF. Elementem wicym komunikujce si aplikacje bdzie wwczas nie nazwa pliku (bo tej po prostu nie ma), lecz nazwa obiektu mapujcego, w naszym projekcie ukrywajca si pod sta cMMFileName.
Notatka

Jeeli w miejsce uchwytu pliku podano warto $FFFFFFFF, to podanie nazwy obiektu (jako ostatniego parametru funkcji CreateFileMapping()) jest konieczne. Nazwa ta stanowi jedyny systemowy identyfikator obiektu odwzorowujcego i jednoczenie zarezerwowanego regionu pamici systemowej dwa obiekty odwzorowujce, pochodzce z rnych aplikacji, lecz posiadajce t sam nazw, bd uwaane za obiekty realizujce to samo odwzorowanie.

Opisany przed chwil scenariusz dzielenia danych midzy dwie aplikacje daje si take zastosowa do dzielenia danych midzy aplikacj a bibliotek DLL i, w konsekwencji wykorzystanie globalnych danych biblioteki

307

jako medium dzielonego midzy dwie aplikacje, co zgodnie z tytuem jest zasadniczym tematem tego podrozdziau. Procedura OpenSharedData()z wydruku 6.12 tworzy odwzorowanie pliku w pamici procesu. Pierwszym krokiem jest stworzenie, za pomoc funkcji CreateFileMapping(), obiektu reprezentujcego odwzorowywany plik; obiekt ten jest nastpnie odwzorowywany w obszar pamici operacyjnej za pomoc funkcji MapViewOfFile(), ktra tym samym zwraca wskanik do zawartoci pliku. Oczywicie dla dwch rnych aplikacji wartoci tego wskanika bd na og rne, poza tym obydwa wskaniki odnosi si bd do rnych przestrzeni adresowych, wane jest jednak to, e odwzorowuje si ten sam obszar pliku. Wynik funkcji MapViewOfFile() przypisywany zmiennej GlobalData stanowi wskanik do globalnego obszaru dzielonych danych; biblioteka udostpnia go aplikacji wywoujcej za porednictwem funkcji GetDLLData(). W ten oto sposb dwie rne aplikacje, importujce z biblioteki DLL funkcj GetDLLData(), mog uzyska wskaniki do tego samego globalnego obszaru pamici. Po zakoczeniu procesu, procedura CloseSharedData() zrywa poczenie pomidzy przestrzeni adresow procesu a globalnym obszarem systemowym (UnmapVievOfFile()) oraz likwiduje obiekt odwzorowujcy, zamykajc jego uchwyt (CloseHandle()).
Notatka

Niewtpliwie istot powyszego przykadu jest sam mechanizm plikw odwzorowanych, bez jakiegokolwiek zwizku z konkretnymi mechanizmami charakterystycznymi dla bibliotek DLL. Fakt, i przykad ten znalaz si w rozdziale dotyczcym bibliotek DLL wynika z roli bibliotek w aplikacjach 16-bitowych: programista przenoszcy do 32-bitowej wersji Delphi 16-bitow aplikacj traktujc jak bibliotek DLL na mod skrzynki kontaktowej zyskuje oto gotowe rozwizanie, uwalniajce go od gruntownego przeprogramowywania mechanizmw komunikacyjnych (przyp. tum.).

Dzielenie globalnych danych biblioteki przez aplikacje


Na zaczonym krku CD-ROM znajduj si projekty dwch aplikacji App1.dpr i App2.dpr. Obydwie te aplikacje uzyskuj dostp do globalnych danych biblioteki ShareLib.DLL wywoujc funkcj GetDLLData() zwracajc wskanik do tego obszaru. Formularz pierwszej z wymienionych aplikacji zawiera dwa pola edycyjne odpowiadajce polom S oraz I rekordu TGlobalData. Kadorazowa zmiana ktregokolwiek z tych pl powoduje stosowne uaktualnienie danych globalnych; uaktualnienie takie nastpuje rwnie w wyniku kliknicia (jedynego) przycisku formularza. Kod rdowy moduu gwnego aplikacji App1 zosta przedstawiony na wydruku 6.13. Wydruk 6.13. Kod rdowy formularza aplikacji modyfikujcej dane globalne
unit MainFrmA1;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, Mask;

{$I DLLDATA.INC}

type

TMainForm = class(TForm)

308

edtGlobDataStr: TEdit; btnGetDllData: TButton; meGlobDataInt: TMaskEdit; procedure btnGetDllDataClick(Sender: TObject); procedure edtGlobDataStrChange(Sender: TObject); procedure meGlobDataIntChange(Sender: TObject); procedure FormCreate(Sender: TObject); public GlobalData: PGlobalDLLData; end;

var MainForm: TMainForm;

{ zaimportuj procedur udostpniajc dane globalne } procedure GetDLLData(var AGlobalData: PGlobalDLLData); StdCall External 'SHARELIB.DLL';

implementation

{$R *.DFM}

procedure TMainForm.btnGetDllDataClick(Sender: TObject); begin { pobierz wskanik do danych globalnych } GetDLLData(GlobalData);

{ uaktualnij kontrolki tak, by odzwierciedlay zawarto danych globalnych } edtGlobDataStr.Text := GlobalData^.S; meGlobDataInt.Text end; := IntToStr(GlobalData^.I);

procedure TMainForm.edtGlobDataStrChange(Sender: TObject); begin { uaktualnij zawarto danych globalnych } GlobalData^.S := edtGlobDataStr.Text; end;

procedure TMainForm.meGlobDataIntChange(Sender: TObject); begin

{ uaktualnij zawarto danych globalnych } if meGlobDataInt.Text = EmptyStr then meGlobDataInt.Text := '0'; GlobalData^.I := StrToInt(meGlobDataInt.Text); end;

309

procedure TMainForm.FormCreate(Sender: TObject); begin btnGetDllDataClick(nil); end;

end.

Druga aplikacja odczytuje dane globalne i wywietla je na swym formularzu. Momenty odczytu wyznaczane s przez komponent zegarowy TTimer, za do wywietlenia globalnych danych su dwie etykiety TLabel. Gdy zmienisz zawarto kontrolek edycyjnych na formularzu aplikacji App1, zaobserwujesz konsekwencje tych zmian na formularzu aplikacji App2 z opnieniem wynikajcym z czstotliwoci tykania komponentu TTimer. Kod rdowy formularza aplikacji App2 jest przedstawiony na wydruku 6.14. Wydruk 6.14. Kod rdowy formularza aplikacji odczytujcej dane globalne
unit MainFrmA2;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, StdCtrls;

{$I DLLDATA.INC}

type

TMainForm = class(TForm) lblGlobDataStr: TLabel; tmTimer: TTimer; lblGlobDataInt: TLabel; procedure tmTimerTimer(Sender: TObject); public GlobalData: PGlobalDLLData; end;

{ zaimportuj procedur udostpniajc dane globalne } procedure GetDLLData(var AGlobalData: PGlobalDLLData); StdCall External 'SHARELIB.DLL';

var MainForm: TMainForm;

implementation

{$R *.DFM}

310

procedure TMainForm.tmTimerTimer(Sender: TObject); begin GetDllData(GlobalData); // uzyskaj dostp do danych

{ wywietl zawarto danych globalnych } lblGlobDataStr.Caption := GlobalData^.S; lblGlobDataInt.Caption := IntToStr(GlobalData^.I); end;

end.

Aby si przekona o tym, i opisana komunikacja rzeczywicie funkcjonuje, wystarczy uruchomi obydwie aplikacje z poziomu pulpitu ich pliki wykonywalne znajduj si na zaczonym krku CD-ROM.

Eksportowanie obiektw z bibliotek DLL


Nieco wczeniej zaprezentowalimy wykorzystanie formularzy definiowanych w bibliotekach DLL, obecnie pokaemy, w jaki sposb wykorzysta zawart w DLL definicj klasy. Opisywana tu technika ma raczej ograniczone zastosowanie podobny efekt osign mona za pomoc pakietw czy interfejsw prezentujemy j tu jednak jako jeszcze jedn moliwo wykorzystania bibliotek DLL. Skoro biblioteki DLL s z natury niezalene od konkretnego jzyka programowania, to mona si spodziewa, i eksportowanie obiektu z biblioteki DLL ograniczone bdzie jedynie do elementw na swj sposb uniwersalnych. Istotnie, istniej ograniczenia dotyczce samej klasy, jak i jej wykorzystywania oto najwaniejsze z nich: Jedynymi elementami klasy, do ktrych ma prawo odwoywa si aplikacja wywoujca, s jej metody wirtualne. Egzemplarze obiektu mog by tworzone jedynie wewntrz biblioteki na zewntrz udostpnia mona jedynie ich wskaniki lub inne identyfikujce je wielkoci. Aplikacji wywoujcej musi by znany zestaw i kolejno definicji metod wirtualnych klasy. Innymi sowy deklaracja klasy, ktr posuguje si aplikacja wywoujca, musi by zgodna z deklaracj klasy w bibliotece DLL co najmniej pod wzgldem kolejnoci i pocztkowego zestawu deklarowanych metod wirtualnych (niewykorzystane kocowe deklaracje mona pomin). Niedozwolone jest definiowanie (w aplikacji wywoujcej) klas pochodnych w stosunku do klasy zdefiniowanej w bibliotece DLL.

W charakterze przykadu zdefiniowalimy prost klas TStringConvert, ktrej moliwoci sprowadzaj si do konwersji zadanego acucha znakw na mae lub due litery. Deklaracj tej klasy umiecilimy w pliku StrConvert.Inc, prezentowanym na poniszym wydruku; deklaracja ta dostpna jest dziki temu zarwno dla biblioteki DLL, jak i aplikacji wywoujcej, za ewentualne jej modyfikacje dokonywane bd tylko jednokrotnie.

Wydruk 6.15. Deklaracja klasy eksportowanej z biblioteki DLL


type

TConvertType = (ctUpper, ctLower);

TStringConvert = class(TObject) {$IFDEF STRINGCONVERTLIB}

311

private FPrepend: String; FAppend : String; {$ENDIF} public function ConvertString(AConvertType: TConvertType; AString: String): String; virtual; stdcall; {$IFNDEF STRINGCONVERTLIB} abstract; {$ENDIF} {$IFDEF STRINGCONVERTLIB} constructor Create(APrepend, AAppend: String); destructor Destroy; override; {$ENDIF} end;

{ Z punktu widzenia aplikacji wywoujcej symbol STRINGCONVERTLIB nie jest zdefiniowany, deklaracja klasy jest wic rwnowana nastpujcej:

TStringConvert = class(TObject) public function ConvertString(AConvertType: TConvertType; AString: String): String; virtual; stdcall; abstract; end;

Zwr uwag na deklaracj jedynej metody wirtualnej:


function ConvertString(AConvertType: TConvertType; AString: String): String; virtual; stdcall; abstract;

Metoda ta zadeklarowana jest jako wirtualna nie dlatego, by mona j przedefiniowa w klasie pochodnej tej przecie nie wolno definiowa w aplikacji wywoujcej lecz po to, by bya dostpna za porednictwem tablicy VMT. Jak wiadomo, czci tablicy VMT jest lista adresw metod wirtualnych; kolejno tych adresw na licie wynika z kolejnoci deklarowania poszczeglnych metod. W caym acuchu klas pochodnych okrelona metoda posiada t sam pozycj na wspomnianej licie; aby odnale adres tej metody w konkretnej klasie, naley jeszcze tylko uzyska adres zwizanej z t klas tablicy VMT co jest spraw oczywist, jeeli dysponuje si wskanikiem do konkretnego obiektu tej klasy. Zrozumiae jest wic w tym kontekcie zarwno ograniczenie si do wirtualnych metod klasy, jak i zgodnoci ich deklaracji pod wzgldem struktury tablic VMT.
Wskazwka

Struktura tablicy VMT opisana jest szczegowo na stronach 44 46 ksiki Delphi 5. Vademecum profesjonalisty suplement.

Istnienie symbolu kompilacji warunkowej STRINGCONVERTLIB wynika z faktu, i niektre elementy deklaracji klasy powinny by widoczne jedynie w bibliotece DLL, nigdy za w aplikacji wywoujcej do elementw takich nale m.in. konstruktor i destruktor. Klauzula abstract przeznaczona jest natomiast wycznie dla 312

aplikacji wywoujcej zwalniajc j z koniecznoci definiowania zadeklarowanej klasy (z punktu widzenia aplikacji importowana z biblioteki DLL klasa jest klas czysto wirtualn pure virtual class). Definicja klasy (w projekcie realizujcym bibliotek DLL) znajduje si w module StringConvertImp.pas, ktrego tre przedstawia wydruk 6.16.

Wydruk 6.16. Definicja eksportowanej klasy


unit StringConvertImp; {$DEFINE STRINGCONVERTLIB}

interface uses SysUtils; {$I StrConvert.inc}

function InitStrConvert(APrepend, AAppend: String): TStringConvert; stdcall;

implementation

constructor TStringConvert.Create(APrepend, AAppend: String); begin inherited Create; FPrepend := APrepend; FAppend end; := AAppend;

destructor TStringConvert.Destroy; begin inherited Destroy; end;

function String; begin

TStringConvert.ConvertString(AConvertType:

TConvertType;

AString:

String):

case AConvertType of ctUpper: Result := Format('%s%s%s', [FPrepend, AnsiUpperCase(AString), FAppend]); ctLower: Result := Format('%s%s%s', [FPrepend, AnsiLowerCase(AString), FAppend]); end; end;

function InitStrConvert(APrepend, AAppend: String): TStringConvert; begin Result := TStringConvert.Create(APrepend, AAppend); end;

end.

Oprcz metod eksportowanej klasy modu ten definiuje take funkcj InitStrConvert(), tworzc egzemplarz obiektu i zwracajc jego wskanik. Przekazywane do konstruktora parametry tej funkcji umoliwiaj okrelenie przedrostka i przyrostka, ktrymi dodatkowo opatrywany bdzie konwertowany acuch.

313

Funkcja ta jest jedyn funkcj eksportowan z biblioteki, o czym atwo si przekona, zerknwszy na gwny plik projektu StringConvertLib.dpr:

Wydruk 6.17. Plik gwny projektu biblioteki DLL


library StringConvertLib; uses ShareMem, SysUtils, Classes, StringConvertImp in 'StringConvertImp.pas';

exports InitStrConvert; end.

Zwr uwag na obecno nazwy ShareMem na licie uses parametrami eksportowanej funkcji s dugie acuchy, wic uycie moduu ShareMem jest konieczne. Projekt aplikacji wywoujcej nosi nazw StrConvertTest.dpr. Jego formularz zawiera kontrolk edycyjn i dwa przyciski, powodujce zamian tekstu w kontrolce edycyjnej na (odpowiednio) due lub mae litery. Kod formularza gwnego projektu przedstawiamy na wydruku 6.18.

Wydruk 6.18. Aplikacja importujca klas z biblioteki DLL


unit MainFrm;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;

{$I strconvert.inc}

type

TMainForm = class(TForm) btnUpper: TButton; edtConvertStr: TEdit; btnLower: TButton; procedure btnUpperClick(Sender: TObject); procedure btnLowerClick(Sender: TObject); private public end;

var MainForm: TMainForm;

314

function InitStrConvert(APrepend, AAppend: String): TStringConvert; stdcall; external 'STRINGCONVERTLIB.DLL';

implementation

{$R *.DFM}

procedure TMainForm.btnUpperClick(Sender: TObject); var ConvStr: String; FStrConvert: TStringConvert; begin FStrConvert := InitStrConvert('Due : "', '"'); try ConvStr := edtConvertStr.Text; if ConvStr <> EmptyStr then edtConvertStr.Text := FStrConvert.ConvertString(ctUpper, ConvStr); finally FStrConvert.Free; end; end;

procedure TMainForm.btnLowerClick(Sender: TObject); var ConvStr: String; FStrConvert: TStringConvert; begin FStrConvert := InitStrConvert('Mae : "', '"'); try ConvStr := edtConvertStr.Text; if ConvStr <> EmptyStr then edtConvertStr.Text := FStrConvert.ConvertString(ctLower, ConvStr); finally FStrConvert.Free; end; end;

end.

Aplikacja rozpoczyna sw prac od utworzenia (wewntrz biblioteki DLL) odnonego obiektu i uzyskania jego wskanika. Procedury obsugujce kliknicie poszczeglnych przyciskw wywouj metod ConvertString() wskazywanego obiektu. Zwolnienie egzemplarza obiektu odbywa si przez wywoanie jego destruktora.

315

Podsumowanie
Biblioteki DLL stanowi podstawowy element aplikacji dla Windows i samego systemu Win32, gwnie dziki wielokrotnemu wykorzystaniu tego samego kodu i zasobw (reusability), dlatego te powicilimy im do obszerny rozdzia. Na pocztku opisalimy zasady tworzenia projektw generujcych biblioteki DLL. Nastpnie pokazalimy dwa sposoby czenia biblioteki z aplikacj domylny i jawny. Na zakoczenie zademonstrowalimy dzielenie globalnych danych biblioteki przez dwie aplikacje za pomoc techniki plikw odwzorowanych, a take wykorzystywanie klasy zdefiniowanej w bibliotece DLL.

316

Cz III Programowanie obsugi baz danych

315

Rozdzia 7.

Bazodanowa architektura Delphi


W niniejszym rozdziale zajmiemy si obsug zewntrznych baz danych przez aplikacje stworzone za pomoc Delphi. Mimo i do zrozumienia treci rozdziau konieczne jest pewne dowiadczenie w pracy z bazami danych, to jednak moe on stanowi wprowadzenie do tematyki tworzenia aplikacji bazodanowych wysokiej jakoci. Rwnie znawcy tematu znajd tu ciekawe przykady. Delphi 6 oferuje kilka mechanizmw dostpu do danych, ktre przedstawimy tu w zarysie, za w nastpnych rozdziaach zajmiemy si niektrymi z nich bardziej szczegowo.

Typy baz danych


Wzorujc si na systemie pomocy, moemy podzieli obsugiwane przez Delphi 6 technologie bazodanowe na ponisze cztery grupy, z ktrych kada zwizana jest z okrelon stron w palecie komponentw:
BDE zawiera komponenty wykorzystujce mechanizm Borland Database Engine (BDE). Mechanizm ten oferuje szerok gam funkcji API organizujcych wspprac z bazami danych. Znakomicie uatwia obsug baz danych Paradox i dBase, jest jednak najbardziej skomplikowany pod wzgldem procedury instalacyjnej aplikacji kocowych. ADO to grupa komponentw wykorzystujcych ActiveX Data Objects (ADO), zapewniajcych dostp do baz danych za porednictwem mechanizmu OLEDB. Obiekty ADO stanowi standard Microsoftu; dostpna jest bogata oferta sterownikw zapewniajcych ich wspprac z rozmaitymi serwerami baz danych. dbExpress technologia ta, posugujc si efektywnymi sterownikami, zapewnia najszybszy dostp do informacji przechowywanych w bazach danych. Komponenty tej grupy maj charakter uniwersalny, dostpne s take na platformie Linuksa. Uniwersalizm ten przesdza jednak o stosunkowo ubogim repertuarze moliwoci w zakresie manipulowania danymi. InterBase komponenty tej grupy umoliwiaj bezporedni dostp do serwera InterBase, z pominiciem

warstwy poredniej (engine layer).

317

Architektura bazy danych


Z punktu widzenia aplikacji Delphi, przepyw informacji zwizanej z zarzdzaniem bazami danych podzieli mona na cile zdefiniowane etapy. Kademu z nich odpowiada okrelona grupa komponentw realizujcych specyficzne funkcje. Podzia ten zosta schematycznie przedstawiony na rysunku 7.1.

Rysunek 7.1. Architektura bazy danych z punktu widzenia Delphi 6 Jak wida, interfejs uytkownika komunikuje si ze zbiorami danych poprzez poredni warstw rda danych (datasource), reprezentowan przez komponent TDataSource. Kady z omawianych na wstpie typw baz danych posuguje si innymi zbiorami danych, zaznaczonych w sposb symboliczny na rysunku 7.1; abstrakcyjnym komponentem reprezentujcym zbir danych jest komponent TDataSet1.

Poczenia z serwerami baz danych


Ostatnim ogniwem schematu na rysunku 7.1 jest poczenie z danymi, zapewniajce fizyczny dostp do informacji. Kady z czterech wymienionych typw baz danych posuguje si innym komponentem poczeniowym, wywodzcym si z klasy TCustomConnection:
TDataBase jest komponentem poczeniowym dla zbiorw danych opartych na technologii BDE TTable, TQuery i TStoredProc. Jego wykorzystanie opisane zostao w rozdziale 28. ksiki Delphi 4.

Vademecum profesjonalisty (Aplikacje bazodanowe typu klient-serwer).


TADOConnection realizuje poczenie z bazami danych ADO, jak MS Access czy MS SQL. Komponentami zbiorw danych ADO s TADODataSet, TADOTable, TADOQuery, i TADOStoredProc. Technologi ADO zajmiemy si dokadniej w rozdziale 9. TSQLConnection to komponent poczeniowy dla zbiorw danych opartych na technologii dbExpress.

S one efektywnymi, jednokierunkowymi (unidirectional) zbiorami danych reprezentowanymi przez komponenty TSQLDataSet, TSQLTable, TSQLQuery i TSQLStoredProc. Technologii dbExpress powicony jest rozdzia 8. niniejszej ksiki.
TIBDatabase jest komponentem poczeniowym dla zbiorw danych typu Interbase Express TIBDataSet, TIBTable, TIBQuery i TIBStoredProc. Zrezygnowalimy w niniejszej ksice z opisu

komponentw Interbase Express, gdy ich realizacja stanowi w duej czci naladownictwo innych metod poczeniowych.

Elementy wsplne dla wszystkich rodzajw pocze skadaj si na definicj klasy TCustomConnection. Zawiera ona metody, waciwoci i zdarzenia zwizane z: nawizywaniem i rozczaniem poczenia z repozytoriami danych, logowaniem i nawizywaniem bezpiecznego poczenia, zarzdzaniem danymi.

1 Sens istnienia poredniej warstwy rda danych wyjaniony zosta w rozdziale 16. ksiki Delphi 6 dla kadego, wyd. HELION 2001 (przyp. tum.).

318

Jest oczywiste, i mimo owych wsplnych elementw funkcjonalnych, kady z wymienionych komponentw poczeniowych posiada pewne charakterystyczne cechy, wynikajce ze specyfiki docelowego repozytorium danych. Poczenie realizowane dla komponentw ADO rni si wic pod wieloma wzgldami od poczenia realizowanego dla komponentw BDE o czym moesz si przekona studiujc rozdziay 8. i 9. niniejszej ksiki oraz rozdzia 28. Delphi 4. Vademecum profesjonalisty.

Zbiory danych
Zbir danych (dataset) moe by postrzegany jako dwuwymiarowa struktura kolumn (columns) i wierszy (rows). Kada kolumna, zwana te polem ( field) grupuje w sobie dane tego samego rodzaju, natomiast kolejne wiersze, zwane te rekordami (records), stanowi kolejne pozycje danych w zbiorze. Komponentem VCL, ujmujcym w sposb abstrakcyjny ide zbioru danych jest komponent TDataSet, zawierajcy waciwoci i metody niezbdne do nawigowania wrd danych i manipulowania nimi, i stanowicy tym samym klas bazow dla komponentw realizujcych konkretne zbiory danych zwizane z rnorodnymi technologiami bazodanowymi. Aby unikn niejednoznacznoci w dalszej czci lektury, musimy zdefiniowa kilka niezbdnych poj i uywa ich tylko w tym znaczeniu. Oto one: Zbir danych (dataset) jest zgodnie z tym, co powiedziano przed chwil uporzdkowan kolekcj rekordw; organizacj kadego rekordu, tak sam dla wszystkich rekordw, okrelaj jego pola, z ktrych kade reprezentuje dan okrelonego typu (liczb cakowit, acuch znakw, liczb w postaci znakowodziesitnej, grafik itp.). Tabela (table) jest specjalnym typem zbioru danych, najczciej posiadajcym fizyczn posta pliku dyskowego. Komponentami reprezentujcymi rnorodne tabele s m.in. TTable, TADOTable, TSQLTable i TIBTable. Zapytanie (query) rwnie stanowi konkretyzacj zbioru danych, jednak nie zmaterializowan w tak cisy sposb jak tabela, lecz stanowic swego rodzaju tabel tymczasow, ktra jest (zazwyczaj) wynikiem dania skierowanego pod adresem serwera. Zapytanie reprezentowane jest m.in. przez komponenty TQuery, TADOQuery, TSQLQuery i TIBQuery.

Notatka

Jak wspominalimy na wstpie, niniejszy rozdzia nie jest przeznaczony dla nowicjuszy i nie zawiera obszernego wprowadzenia w problematyk programowania obsugi baz danych. Jeeli okrelenia: baza danych, tabela czy indeks brzmi dla Ciebie raczej obco, powiniene uzupeni sw wiedz korzystajc z 2 podrcznikw bardziej podstawowych .

Otwieranie i zamykanie zbioru danych


Jakakolwiek operacja na zbiorze danych musi by poprzedzona jego otwarciem. Zadanie to wykonuje metoda Open() klasy TDataSet:
Table1.Open;

Wywoanie to jest rwnowane ustawieniu na True waciwoci Active:

jak np. ksika wymieniona w poprzednim przypisie (przyp. tum.).

319

Table1.Active := True;

Czynnoci wieczc wszystkie operacje na zbiorze danych jest jego zamknicie, dokonywane przez wywoanie metody Close()
Table1.Close;

rwnowane ustawieniu waciwoci Active na False:


Table1.Active := False;

Wskazwka

Jeeli wykorzystujesz zbiory danych zlokalizowane na serwerze SQL, otwarcie pierwszego ze zbiorw tego serwera poprzedzone jest nawizaniem poczenia (connection) z serwerem, za zamknicie ostatniego z tych zbiorw powoduje rozczenie si z serwerem (disconnection). Poniewa operacje poczenia i rozczenia wymagaj zawsze nieco czasu, korzystne moe okaza si uycie komponentu TDatabase w celu nawizania permanentnego poczenia z serwerem w sytuacji, kiedy opisane przed chwil poczenia (rozczenia) zdarzayby si nazbyt czsto. Niebawem powrcimy do tego zagadnienia.

O tym, jak podobne jest otwieranie (zamykanie) rnych typw zbiorw danych, zawiadczy moe kod prezentowany na wydruku 7.1.

Wydruk 7.1. Otwieranie i zamykanie przykadowych zbiorw danych


unit MainFrm; interface

uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, FMTBcd, DBXpress, IBDatabase, ADODB, DBTables, DB, SqlExpr, IBCustomDataSet, IBQuery, IBTable, StdCtrls;

type TForm1 = class(TForm) SQLDataSet1: TSQLDataSet; SQLTable1: TSQLTable; SQLQuery1: TSQLQuery;

ADOTable1: TADOTable; ADODataSet1: TADODataSet; ADOQuery1: TADOQuery;

IBTable1: TIBTable; IBQuery1: TIBQuery; IBDataSet1: TIBDataSet;

Table1: TTable; Query1: TQuery;

SQLConnection1: TSQLConnection;

320

Database1: TDatabase; ADOConnection1: TADOConnection; IBDatabase1: TIBDatabase; Button1: TButton; Label1: TLabel; Button2: TButton; IBTransaction1: TIBTransaction; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure Button2Click(Sender: TObject); private { Private declarations } procedure OpenDatasets; procedure CloseDatasets; public { Public declarations } end;

var Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject); begin IBDatabase1.Connected := True;

ADOConnection1.Connected := True; Database1.Connected := True;

SQLConnection1.Connected := True; end;

procedure TForm1.Button1Click(Sender: TObject); begin OpenDatasets; end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin CloseDatasets; IBDatabase1.Connected := false;

ADOConnection1.Connected := false; Database1.Connected := false;

SQLConnection1.Connected := false;

321

end;

procedure TForm1.CloseDatasets; begin

// rozczenie ze zbiorami dbExpress SQLDataSet1.Close; SQLTable1.Close; SQLQuery1.Close; // lub Active := false; // lub Active := false; // lub Active := false;

// rozczenie ze zbiorami ADO ADOTable1.Close; ADODataSet1.Close; ADOQuery1.Close; // lub Active := false; // lub Active := false; // lub Active := false;

// rozczenie ze zbiorami Interbase Express IBTable1.Close; IBQuery1.Close; IBDataSet1.Close; // lub Active := false; // lub Active := false; // lub Active := false;

// rozczenie ze zbiorami BDE Table1.Close; Query1.Close; // lub Active := false; // lub Active := false;

Label1.Caption := 'Zbiory danych s zamknite.' end;

procedure TForm1.OpenDatasets; begin

// poczenie ze zbiorami dbExpress SQLDataSet1.Open; SQLTable1.Open; SQLQuery1.Open; // lub Active := true; // lub Active := true; // lub Active := true;

// poczenie ze zbiorami ADO ADOTable1.Open; ADODataSet1.Open; ADOQuery1.Open; // lub Active := true; // lub Active := true; // lub Active := true;

// poczenie ze zbiorami Interbase Express IBTable1.Open; IBQuery1.Open; IBDataSet1.Open; // lub Active := true; // lub Active := true; // lub Active := true;

// poczenie ze zbiorami BDE

322

Table1.Open; Query1.Open;

// lub Active := true; // lub Active := true;

Label1.Caption := 'Zbiory danych s otwarte.'; end;

procedure TForm1.Button2Click(Sender: TObject); begin CloseDatasets; end;

end.

Powyszy modu jest czci projektu DatasetCnct.dpr znajdujcego si na doczonym do ksiki krku CD-ROM. Przed jego uruchomieniem naley odpowiednio ustawi cieki okrelajce lokalizacj baz danych (ustawienia zawarte w projekcie pochodz z oryginalnej wersji ksiki).

Nawigowanie wrd rekordw zbioru danych


Komponent TDataSet oferuje kilka podstawowych metod umoliwiajcych nawigowanie wrd rekordw zbioru danych. Metody First()i Last()powoduj przejcie do (odpowiednio) pierwszego i ostatniego rekordu, natomiast metody Next()i Prior()powoduj przesunicie si o jeden rekord (odpowiednio) w przd i w ty. Przemieszczanie si o wiksz liczb rekordw (w przd lub w ty) umoliwia metoda MoveBy() argument wywoania okrela dany dystans (warto ujemna oznacza przesunicie w ty).

Waciwoci BOF i EOF i niebezpieczestwo zaptlenia


Waciwoci BOF i EOF (typu Boolean) umoliwiaj wykrycie pocztku i koca tabeli. Oto przykadowa sekwencja, powodujca wykonanie pewnej operacji kolejno na wszystkich rekordach zbioru danych:
Table1.First; While not Table1.EOF do begin ... Table1.Next; end; // jaka operacja na rekordzie // ta instrukcja jest bardzo wana // przejd do pierwszego rekordu // sprawd, czy nie wystpi koniec zbioru

Ostrzeenie:

Nie zapomnij o wywoywaniu metody Next() po przetworzeniu biecego rekordu, inaczej program wpadnie w nieskoczon ptl.

Ptli While nie mona tu zastpi ptl Repeat ponisza sekwencja nie bdzie funkcjonowa poprawnie dla pustego zbioru danych, gdy, jak wiadomo, ptla RepeatUntil wykonuje si co najmniej raz:
Table1.First; Repeat

323

.... // Jaka operacja na rekordzie Table1.Next; Until Table1.EOF;

Przykad nawigowania wrd rekordw rnych zbiorw danych ilustruje kolejny projekt (DatasetNav.dpr), ktrego modu gwny przedstawiamy na wydruku 7.2.

Wydruk 7.2. Nawigowanie wrd rekordw zbioru danych


unit MainFrm;

interface

uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, FMTBcd, DBXpress, IBDatabase, ADODB, DBTables, DB, SqlExpr, IBCustomDataSet, IBQuery, IBTable, StdCtrls, Grids, DBGrids, ExtCtrls;

type TForm1 = class(TForm) SQLTable1: TSQLTable; ADOTable1: TADOTable; IBTable1: TIBTable; Table1: TTable;

SQLConnection1: TSQLConnection; Database1: TDatabase; ADOConnection1: TADOConnection; IBDatabase1: TIBDatabase; Button1: TButton; Label1: TLabel; Button2: TButton; IBTransaction1: TIBTransaction; DBGrid1: TDBGrid; DataSource1: TDataSource; RadioGroup1: TRadioGroup; btnFirst: TButton; btnLast: TButton; btnNext: TButton; btnPrior: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure Button2Click(Sender: TObject); procedure RadioGroup1Click(Sender: TObject); procedure btnFirstClick(Sender: TObject);

324

procedure btnLastClick(Sender: TObject); procedure btnNextClick(Sender: TObject); procedure btnPriorClick(Sender: TObject); procedure DataSource1DataChange(Sender: TObject; Field: TField); private { Private declarations } procedure OpenDatasets; procedure CloseDatasets; public { Public declarations } end;

var Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject); begin IBDatabase1.Connected := True;

ADOConnection1.Connected := True; Database1.Connected := True;

SQLConnection1.Connected := True;

Datasource1.DataSet := IBTable1; OpenDatasets; end;

procedure TForm1.Button1Click(Sender: TObject); begin OpenDatasets; end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin CloseDatasets; IBDatabase1.Connected := false;

ADOConnection1.Connected := false; Database1.Connected := false;

SQLConnection1.Connected := false; end;

procedure TForm1.CloseDatasets; begin

325

// rozczenie ze zbiorem dbExpress SQLTable1.Close; // lub Active := false;

// rozczenie ze zbiorem ADO ADOTable1.Close; // lub Active := false;

// rozczenie ze zbiorem Interbase Express IBTable1.Close; // lub Active := false;

// rozczenie ze zbiorem BDE Table1.Close; // lub Active := false;

Label1.Caption := 'Zbiory danych s zamknite.' end;

procedure TForm1.OpenDatasets; begin

// poczenie ze zbiorem dbExpress SQLTable1.Open; // lub Active := true;

// poczenie ze zbiorem ADO ADOTable1.Open; // lub Active := true;

// poczenie ze zbiorem Interbase Express IBTable1.Open; // lub Active := true;

// poczenie ze zbiorem Table1.Open;

BDE

// lub Active := true;

Label1.Caption := 'Zbiory danych s otwarte.'; end;

procedure TForm1.Button2Click(Sender: TObject); begin CloseDatasets; end;

procedure TForm1.RadioGroup1Click(Sender: TObject); begin case RadioGroup1.ItemIndex of 0: Datasource1.DataSet := IBTable1; 1: Datasource1.DataSet := Table1; 2: Datasource1.DataSet := ADOTable1; end; // case

326

end;

procedure TForm1.btnFirstClick(Sender: TObject); begin DataSource1.DataSet.First; end;

procedure TForm1.btnLastClick(Sender: TObject); begin DataSource1.DataSet.Last; end;

procedure TForm1.btnNextClick(Sender: TObject); begin DataSource1.DataSet.Next; end;

procedure TForm1.btnPriorClick(Sender: TObject); begin DataSource1.DataSet.Prior; end;

procedure TForm1.DataSource1DataChange(Sender: TObject; Field: TField); begin btnLast.Enabled := not DataSource1.DataSet.Eof; btnNext.Enabled := not DataSource1.DataSet.Eof; btnFirst.Enabled := not DataSource1.DataSet.Bof; btnPrior.Enabled := not DataSource1.DataSet.Bof; end;

end.

Formularz projektu przedstawia rysunek 7.2. Za pomoc przyciskw z grupy Zbir danych uytkownik moe wybra jeden z trzech rodzajw zbiorw danych. Obecne na formularzu przyciski su do otwierania i zamykania wybranego zbioru oraz poruszania si po jego rekordach.

327

Rysunek 7.2. Formularz gwny projektu DatasetNav.dpr


Notatka

Jak zapewne zauwaye, nie wczylimy do powyszego projektu zbiorw danych typu dbExpress. Nie zrobilimy tego z prostej przyczyny s one zbiorami jednokierunkowymi (unidirectional), przeznaczonymi tylko do odczytu; prba doczenia do takiego zbioru nawigowalnego komponentu w rodzaju TDBGrid spowoduje wyjtek. Nawigacja w zbiorach jednokierunkowych musi by prowadzona za pomoc specjalnych rodkw zajmiemy si tym w rozdziale 8.

Manipulowanie zawartoci zbioru danych


Dostp do zbiorw danych niewiele byby wart, gdyby nie moliwo manipulowania jego danymi rozumianego jako wstawianie, edycja i usuwanie rekordw. Komponent TDataSet posiada zwizane z tym metody Insert(), Edit() i Delete(), za przykad ich uycia przedstawia wydruk 7.3, stanowicy cz projektu DataManip.dpr.

Wydruk 7.3. Manipulowanie zawartoci zbioru danych


unit MainFrm;

interface

uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Mask, DBCtrls, DB, Grids, DBGrids, ADODB;

type

328

TMainForm = class(TForm) ADOConnection1: TADOConnection; adodsCustomer: TADODataSet; dtsrcCustomer: TDataSource; DBGrid1: TDBGrid; adodsCustomerCustNo: TAutoIncField; adodsCustomerCompany: TWideStringField; adodsCustomerAddress1: TWideStringField; adodsCustomerAddress2: TWideStringField; adodsCustomerCity: TWideStringField; adodsCustomerStateAbbr: TWideStringField; adodsCustomerZip: TWideStringField; adodsCustomerCountry: TWideStringField; adodsCustomerPhone: TWideStringField; adodsCustomerFax: TWideStringField; adodsCustomerContact: TWideStringField; Label1: TLabel; dbedtCompany: TDBEdit; Label2: TLabel; dbedtAddress1: TDBEdit; Label3: TLabel; dbedtAddress2: TDBEdit; Label4: TLabel; dbedtCity: TDBEdit; Label5: TLabel; dbedtState: TDBEdit; Label6: TLabel; dbedtZip: TDBEdit; Label7: TLabel; dbedtPhone: TDBEdit; Label8: TLabel; dbedtFax: TDBEdit; Label9: TLabel; dbedtContact: TDBEdit; btnAdd: TButton; btnEdit: TButton; btnSave: TButton; btnCancel: TButton; Label10: TLabel; dbedtCountry: TDBEdit; btnDelete: TButton; procedure btnAddClick(Sender: TObject); procedure btnEditClick(Sender: TObject); procedure btnSaveClick(Sender: TObject); procedure btnCancelClick(Sender: TObject); procedure FormCreate(Sender: TObject);

329

procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure btnDeleteClick(Sender: TObject); private { Private declarations } procedure SetButtons; public { Public declarations } end;

var MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.btnAddClick(Sender: TObject); begin adodsCustomer.Insert; SetButtons; end;

procedure TMainForm.btnEditClick(Sender: TObject); begin adodsCustomer.Edit; SetButtons; end;

procedure TMainForm.btnSaveClick(Sender: TObject); begin adodsCustomer.Post; SetButtons; end;

procedure TMainForm.btnCancelClick(Sender: TObject); begin adodsCustomer.Cancel; SetButtons; end;

procedure TMainForm.SetButtons; begin btnAdd.Enabled := adodsCustomer.State = dsBrowse;

btnEdit.Enabled := adodsCustomer.State = dsBrowse; btnSave.Enabled := (adodsCustomer.State = dsInsert) or (adodsCustomer.State = dsEdit);

330

btnCancel.Enabled := (adodsCustomer.State = dsInsert) or (adodsCustomer.State = dsEdit); btnDelete.Enabled := adodsCustomer.State = dsBrowse; end;

procedure TMainForm.FormCreate(Sender: TObject); begin adodsCustomer.Open; SetButtons;

end;

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction); begin adodsCustomer.Close; ADOConnection1.Connected := False; end;

procedure TMainForm.btnDeleteClick(Sender: TObject); begin adodsCustomer.Delete; end;

end.

Formularz projektu przedstawia rysunek 7.3. Projekt realizuje manipulowanie danymi w najprostszej postaci. Analizujc kod rdowy formularza gwnego bez trudu zauwaysz wykorzystanie nastpujcych metod klasy TDataSet:
Insert() przygotowuje zbir danych do wstawienia nowego rekordu; Edit() przygotowuje zbir danych do edycji biecego rekordu; Post() zatwierdza zmiany poczynione w rekordzie (edytowanym lub wstawianym); Cancel() anuluje zmiany wprowadzone do rekordu; Delete() usuwa biecy rekord ze zbioru.

331

Rysunek 7.3. Formularz gwny projektu DataManip.dpr

Wydruk 7.3 ilustruje ponadto zastosowanie waciwoci TDataset.State, reprezentujcej stan zbioru; jest ona wykorzystywana do selektywnego udostpniania poszczeglnych przyciskw i tak, np. przycisk Dodaj jest niedostpny, gdy zbir danych znajduje si w stanie wstawiania (dsInsert) bd edytowania (dsEdit) rekordu. Znaczenie poszczeglnych wartoci, ktre przyjmowa moe waciwo State, wyjanione zostao w tabeli 7.1.

Tabela 7.1. Wartoci waciwoci TDataSet.State Warto


dsBrowse dsCalcFields

Znaczenie Stan przegldania. Realizowana


OnCalcFields

jest

procedura obsugi zdarzenia trwa ustalanie wartoci pl

obliczanych.
dsEdit dsInactive dsInsert

Stan edycji wywoano metod Edit(), lecz zmiany nie zostay jeszcze utrwalone. Zbir danych jest zamknity. Stan wstawiania lub doczania nowego rekordu wywoano metod Insert() lub Append(), lecz zmiany nie zostay jeszcze utrwalone. Wywoano metod SetKey(), lecz nie wywoano jeszcze metody GotoKey(). Zbir danych znajduje si w tymczasowym stanie, w ktrym odczytywana jest waciwo NewValue.

dsSetKey dsNewValue

332

dsUpdateOld dsFilter dsBlockRead

Zbir danych znajduje si w tymczasowym stanie, w ktrym odczytywana jest waciwo OldValue. Trwa wykonywanie jakiej operacji zwizanej z filtrowaniem. Z powodu buforowania danych, przy zmianie biecego rekordu zawarto kontrolek bazodanowych nie zostanie uaktualniona, nie bd te generowane zdarzenia odpowiadajce zmianie pozycji. Zbir danych znajduje si w tymczasowym stanie zwizanym z obliczaniem pola, ktrego waciwo FieldKind rwna jest fkInternalCalc. Trwa otwieranie zbioru danych; stan ten pojawia si podczas otwierania do odczytu asynchronicznego.

dsInternalCalc

dsOpening

Pola rekordu bazy danych


Dostp do poszczeglnych pl rekordu w zbiorze danych umoliwia komponent TField i jego komponenty pochodne. Oprcz odczytywania i zapisywania pl moliwa jest zmiana ich waciwoci, jak rwnie tworzenie nowych pl obliczanych (calculated fields) i przegldowych (lookup fields).

Wartoci pl
Bezporednim narzdziem sucym do odczytu i modyfikacji pl rekordu jest tablicowa waciwo FieldValues[] komponentu TDataSet kady element tej tablicy ma typ Variant. Poniewa waciwo FieldValues[] jest domyln waciwoci tablicow klasy TDataSet, mona opuci jej nazw i stosowa operator indeksowania wprost do zmiennej obiektowej, jak w poniszym przykadzie:

S := Table1['CustName']; // to samo, co S := Table1.FieldValues['CustName']

Jak przed chwil wspomnielimy, wartoci otrzymywane za porednictwem waciwoci FieldValues[] s typu Variant, co stanowi niejako dodatkow atrakcj moliwe jest midzy innymi zapamitanie zawartoci wszystkich pl rekordu w pojedynczej tablicy wariantowej, co ilustruje poniszy przykad:
const Astr = 'Pracownik %s pracuje na stanowisku nr %s i posiada stawk podstawow %m' ;

var VarArr: Variant F: Currency; begin VarArr := VarArrayCreate([0, 2], varVariant); VarArr := Table1['Nazwisko;Stanowisko;Stawka']; F := VarArr[2]; ShowMessage(Format(Astr, [VarArr[0], VarArr[1], F])); end;

333

Wartoci pl rekordu dostpne s take (porednio) za porednictwem waciwoci Fields[] i metody FieldsByName(). Pierwsza z nich jest indeksowan od zera tablic komponentw TField reprezentujcych poszczeglne pola Fields[0] oznacza pierwsze pole w rekordzie; indeksem moe by rwnie nazwa pola. Metoda FieldsByName() udostpnia komponent TField skojarzony z polem o danej nazwie, stanowicej parametr jej wywoania na przykad:
Nazwisko := Table1.FieldByName('Nazwisko');

Majc ju konkretny obiekt TField, moemy odczyta lub zapisa warto reprezentowanego przez niego pola suy do tego szereg waciwoci, zwizanych z rnymi typami wartoci pl. Przedstawia je tabela 7.2. Tabela 7.2. Waciwoci komponentu TField udostpniajce warto pola

Waciwo
AsBoolean AsFloat AsInteger AsString AsDateTime Value

Typ wartoci pola


Boolean Double Longint String TDateTime Variant

I tak, na przykad, jeeli pierwsze pole zbioru danych ma nazw Nazwisko i warto typu String, jego warto mona otrzyma za pomoc jednej z poniszych instrukcji
S := Table1.Fields[0].AsString; S := Table1.FieldsByName('Nazwisko').AsString;

Typy pl
Informacja o typie pola zawarta jest we waciwoci DataType odnonego komponentu TField. Waciwo ta przyjmowa moe wartoci nastpujcego typu:

type TFieldType = (ftUnknown, ftString, ftSmallint, ftInteger, ftWord, ftBoolean, ftFloat, ftCurrency, ftBCD, ftDate, ftTime, ftDateTime, ftBytes, ftVarBytes, ftAutoInc, ftBlob, ftMemo, ftGraphic, ftFmtMemo, ftParadoxOle, ftDBaseOle, ftTypedBinary, ftCursor, ftFixedChar, ftWideString, ftLargeint, ftADT, ftArray, ftReference, ftDataSet, ftOraBlob, ftOraClob, ftVariant, ftInterface, ftIDispatch, ftGuid, ftTimeStamp, ftFMTBcd);

Rnorodne typy pl znajduj w Delphi jeszcze inne odzwierciedlenie w postaci rnorodnych klas pochodnych w stosunku do TField; niebawem powrcimy do tej kwestii.

334

Nazwy i numery pl
Nazwa pola kryje si pod waciwoci FieldName reprezentujcego je komponentu TField, tak wic nazw pola o numerze N (liczc od zera) uzyska mona za pomoc instrukcji

S := Table1.Fields[N].FieldName;

Funkcj odwrotn uzyskanie numeru pola na podstawie jego nazwy realizuje waciwo FieldNo:
N := Table1.FieldByName('Adres').FieldNo;

Liczb pl w rekordzie zbioru danych udostpnia (porednio) waciwo TDataSet.FieldList, reprezentujca spaszczon (flattened) list pl, wraz z polami zagniedonymi i abstrakcyjnymi polami danych (ADT). Dla kompatybilnoci zachowano rwnie waciwo TDataSet.FieldCount, nie uwzgldnia ona jednak pl ADT.

Operowanie zawartoci pl
Proces edycji jednego lub kilku pl rekordu realizowany jest w trzech nastpujcych etapach: 1. 2. 3. Wywoanie metody Edit(), przeczajcej zbir danych w tryb edycji. Przypisanie nowych wartoci wybranym polom rekordu. Utrwalenie wprowadzonych zmian, bd w sposb jawny przez wywoanie metody Post(), bd w sposb automatyczny, przez zmian biecej pozycji w zbiorze.

Oto prosty przykad:


Table1.Edit; Table1['Wiek'] := 44; Table1.Post;

Wskazwka

Niektre zbiory danych s przewidziane tylko do odczytu na przykad zbiory zapisane na krkach CD-ROM, czy te wirtualne zbiory danych bdce wynikami zapyta SQL. W takim wypadku modyfikacja pl jest oczywicie niewykonalna; o tym, czy pola zbioru danych mona modyfikowa, informuje jego waciwo CanModify warto True oznacza zezwolenie na modyfikacj.

W rwnie prosty sposb mona doda do zbioru danych nowy rekord. W tym celu naley: 1. 2. 3. Przeczy zbir danych w tryb wstawiania lub doczania przez wywoanie jednej z metod Insert()albo
Append().

Przypisa wartoci wybranym polom nowego rekordu. Utrwali zmian za pomoc wywoania metody Post() lub przemieszczenia si do innego rekordu.

Ostrzeenie:

Przy edycji, wstawianiu lub doczaniu rekordu do zbioru bd wiadom tego, e kada zmiana biecego rekordu powoduje utrwalenie wprowadzonych zmian, istniejcych dotychczas tylko na formatce edycyjnej. Uywaj wic metod First(), Next(), Prior() i Last() i MoveBy() ze szczegln ostronoci.

Wycofanie si z wprowadzonych zmian moliwe jest dziki metodzie Cancel(). Anuluje ona wszystkie zmiany i przecza baz w tryb przegldania. Oto prosty przykad:

335

Table1.Edit; Table1['Wiek'] := 44; if Potwierdzenie Then Table1.Post Else Table1.Cancel;

I wreszcie ostatnia metoda zwizana z operowaniem danymi jest ni metoda Delete(), dokonujca usunicia biecego rekordu zbioru. Oto przykad skrcenia zbioru o jeden rekord:
Table1.Last; Table1.Delete;

Edytor pl
Edytor pl (rysunek 7.4) jest narzdziem umoliwiajcym operowanie zestawem pl oraz ich atrybutami. Mona go uruchomi w dwojaki sposb: przez dwukrotne kliknicie komponentu reprezentujcego zbir danych (np. TTable, TQuery, TStoredProc), bd te przez wybranie pozycji Fields Editor z jego menu kontekstowego (uruchamianego klikniciem prawym przyciskiem myszy).

Rysunek 7.4. Edytor pl i jego menu kontekstowe Edytor pl umoliwia wybranie tych pl (spord faktycznie istniejcych w zbiorze danych), ktre widoczne bd dla reprezentujcego zbir danych komponentu. Domylnie widoczne s wszystkie pola okno edytora pl nie zawiera wwczas adnej pozycji. Edytor pl umoliwia ponadto definiowanie wasnych pl przegldowych i obliczanych su do tego polecenia menu kontekstowego (widocznego na rysunku 7.4). Aby samodzielnie poeksperymentowa z edytorem pl, zainicjuj now aplikacj, umie na formularzu komponent TTable i ustaw odpowiednio jego komponenty: TableName na orders.db i DatabaseName na DBDEMOS (ta ostatnia nazwa jest aliasem katalogu zawierajcego przykadowe bazy danych Delphi 6). Aby 336

uzyska wgld w struktur i zawarto tabeli, umie take na formularzu komponenty TDataSource i TDBGrid, ustawiajc odpowiednio ich waciwoci DBGrid1.DataSource na DataSource1 oraz DataSource1.DataSet na Table1. Po ustawieniu na True waciwoci Table1.Active w przegldarce DBGrid1 ukae si zawarto rekordw zbioru danych.

Dodawanie pl
Okno edytora pl pozostaje domylnie puste jest tak podczas jego pierwszego uruchomienia dla nowo doczonego zbioru danych; oznacza to, i widoczne s wszystkie pola, co atwo stwierdzi zerknwszy na przegldark. Sytuacja ta zmieni si jednak, gdy dodamy ktre pole do listy widocznej w oknie edytora wwczas to owa lista okrela bdzie zestaw pl widocznych. Dodanie nowego pola do wspomnianej listy nastpuje w wyniku wybrania opcji Add Fields z menu kontekstowego widocznego na rysunku 7.4; ukae si wtedy okno zawierajce list wszystkich pl tabeli. Wybierz z niej kilka pl na przykad OrderNo, CustNo i ItemsTotal i kliknij przycisk OK; wybrane pola (i tylko one) bd odtd widoczne w przegldarce i w oknie edytora pl.

Komponenty reprezentujce pola


Dla kadego pola wybranego przez edytor Delphi tworzy komponent bdcy pochodnym w stosunku do TField, o typie zalenym od typu pola; wacicielem tak utworzonego komponentu jest formularz. Dla wymienionych przed chwil pl OrderNo, CustNo i ItemsTotal utworzone zostan nastpujce komponenty:
Table1OrderNo: TFloatField; Table1CustNo: TFloatField; Table1ItemsTotal: TCurrencyField;

Jak atwo zauway, nazwa kadego z powyszych pl formularza jest konkatenacj nazwy komponentu reprezentujcego zbir danych i nazwy pola w tym zbiorze. Zestawienie (wikszoci) komponentw pochodnych do TField, wraz z ich klasami macierzystymi i pascalowymi odpowiednikami typw, przedstawia tabela 7.3.

Tabela 7.3. Typy pl baz danych w Delphi


Komponent TStringField TWideStringField TGuidField TNumericField TIntegerField TSmallIntField TLargeintField TWordField TAutoIncField TFloatField TCurrencyField TBCDField TBooleanField TDateTimeField TDateField Komponent macierzysty TField TStringField TStringField TField TNumericField TIntegerField TNumericField TIntegerField TIntegerField TNumericField TFloatField TNumericField TField TField TDateTimeField Typ pola ftString ftWideString ftGuid * ftInteger ftSmallint ftLargeint ftWord ftAutoInc ftFloat ftCurrency ftBCD ftBoolean ftDateTime ftDate Typ Object Pascala String WideString TGUID * Integer SmallInt Int64 Word Integer Double Currency Double Boolean TDateTime TDateTime

337

TTimeField TBinaryField TBytesField TVarBytesField TBlobField TMemoField TGraphicField TObjectField TADTField TArrayField TDataSetField TReferenceField TVariantField TInterfaceField TIDispatchField TAggregateField

TDateTimeField TField TBinaryField TBytesField TField TBlobField TBlobField TField TObjectField TObjectField TObjectField TDataSetField TField TField TInterfaceField TField

ftTime * ftBytes ftVarBytes ftBlob ftMemo ftGraphic * ftADT ftArray ftDataSet ftReference ftVariant ftInterface ftIDispatch nie istnieje

TDateTime * nie istnieje nie istnieje nie istnieje nie istnieje nie istnieje * nie istnieje nie istnieje TDataSet nie istnieje OleVariant IUnknown IDispatch nie istnieje

* oznacza abstrakcyjn klas bazow

Pola a inspektor obiektw


Wybierajc pojedyncze pole w oknie edytora pl, uytkownik uzyskuje dostp do jego waciwoci z poziomu inspektora obiektw. Umoliwia to ustalanie (na etapie projektowania) wielu cech pola, jak np. minimalna i maksymalna warto, format wywietlania, dostpno do modyfikacji itp. Przejcie na stron Events inspektora obiektw umoliwia z kolei przypisywanie procedur obsugi do poszczeglnych zdarze pola, jak OnChange, OnGetTExt, OnSetText, OnValidate itp. ich znaczenie opisane jest w systemie pomocy, wystarczy klikn nazw zdarzenia i nacisn F1. Bodaj najczciej wykorzystywanym zdarzeniem pola jest OnChange, zachodzce kadorazowo, gdy ulega zmianie zawarto pola, a wic np. podczas edycji rekordu lub zmiany biecej pozycji w zbiorze danych.

Pola obliczane
Za pomoc edytora pl moliwe jest utworzenie tzw. pola obliczanego (calculated field), ktre jest zaliczane do jednego z rodzajw pl wirtualnych, tj. nie istniejcych fizycznie w zbiorze danych. Zamy na przykad, e potrzebne jest nam pole o wartoci zmniejszonej o 32% w stosunku do pola ItemsTotal. Jego zdefiniowanie wymaga wykonania dwch czynnoci: utworzenia odpowiedniego komponentu pochodnego do TField oraz wskazania sposobu obliczania zawartoci. Pierwsz z tych czynnoci wykonuje si bardzo atwo za pomoc edytora pl. Po zamkniciu zbioru danych (Table1.Active ustawione na False) wystarczy wybra z menu kontekstowego polecenie New Field, aby otrzyma okno przedstawione na rysunku 7.5. Nastpnie w pole Name naley wpisa nazw nowego pola (WholesaleTotal), w polu Type naley wybra jego typ (Currency) i zaznaczy opcj Calculated. Po klikniciu przycisku OK i ponownym otwarciu zbioru, w przegldarce DBGrid pojawi si nowa, pusta kolumna.

338

Rysunek 7.5. Definiowanie pola obliczanego

Aby zapeni t kolumn poprawnymi danymi, naley zrealizowa drugi etap scenariusza zdefiniowa sposb obliczania pola. Ustalanie wartoci wszystkich pl obliczanych danego rekordu odbywa si w ramach zdarzenia OnCalcFields zbioru danych. Za pomoc inspektora obiektw naley wic utworzy szkielet procedury obsugujcej to zdarzenie i wpisa do jej wntrza odpowiedni instrukcj:
procedure TForm1.Table1CalcFields(DataSet: TDataSet); begin DataSet['WholeSalesTotal'] := DataSet['ItemsTotal'] * 0.68; end;

Po skompilowaniu i uruchomieniu projektu ujrzymy w przegldarce wyliczon warto pola w kadym rekordzie (rys. 7.6).

Rysunek 7.6. Pole obliczane widoczne w przegldarce

339

Pola przegldowe
Inn odmian pola wirtualnego jest pole przegldowe (lookup field). Pole takie czerpie sw warto z innego, skorelowanego zbioru danych. Wyjanimy to na prostym przykadzie. Zamy, e w zbiorze danych ORDERS.DB, zawierajcym rejestr zamwie, klient zamawiajcy towar identyfikowany jest przez numer umieszczony w polu CustNo. Trzeba jednak przyzna, e list zaadresowany jako CN 1384 miaby nike szanse trafi do waciwego adresata (chyba e zaprzyjaniony listonosz rwnie zaopatruje si w tej firmie); podane byoby wic pole, zawierajce nazw lub nazwisko klienta, czytelne nie tylko dla komputera, lecz take dla uytkownika. Aby to wykona, musimy dysponowa innym zbiorem danych, zawierajcym rekordy opisujce poszczeglnych klientw, identyfikowanych za pomoc tych samych numerw, ktre uywane s w zbiorze orders.db. Taki zbir oczywicie istnieje, jest nim customer.db, umieszczony w tym samym katalogu, identyfikowanym przez alias DBDEMOS. Umiemy wic na formularzu drugi element TTable (otrzyma on nazw Table2) i skojarzmy go ze wspomnianym zbiorem customer.db. Wzajemne przyporzdkowanie rekordw pomidzy zbiorami orders.db oraz customer.db nastpuje w ramach tzw. dialogu poczeniowego, uruchamianego za porednictwem edytora pl. Otwieramy edytor pl (dla komponentu Table1), z jego menu kontekstowego wybieramy polecenie New Field i nadajemy nowemu polu nazw CustName, typ String i szeroko 15 znakw. W sekcji Field type wybieramy opcj Lookup. Kryterium odpowiednioci rekordw w obydwu zbiorach jest rwno wskazanych pl; w zbiorze orders.db jest to pole CustNo, w zbiorze customer.db pole CustNo (nazwy nie musz by identyczne). Wskazujemy te pola jako (odpowiednio) Key Fields i Lookup Keys. Naley jeszcze tylko wskaza komponent reprezentujcy pomocniczy zbir danych (Table2) i pole w tym zbiorze, zawierajce warto dla definiowanego pola przegldowego (Contact). Wypenione okienko edytora pocze przedstawia rysunek 7.7.

Rysunek 7.7. Definiowanie pola przegldowego

Po otwarciu obydwu zbiorw (Table1 i Table2) ujrzymy zawarto nowego pola CustName (rys. 7.8):

340

Rysunek 7.8. Pole przegldowe CustName w przegldarce

Przeciganie pl
Jak ju wspominalimy, dla kadego uwzgldnianego pola edytor pl automatycznie tworzy komponent pochodny do TField. To jednak nie koniec udogodnie: mniej popularnym, lecz niezwykle porcznym mechanizmem jest co, co wizualnie mona okreli jako przeciganie (drag-and-drop) pl z okna edytora pl wprost na formularz. Aby to zaobserwowa, umie na (pustym) formularzu komponent TTable i skojarz go ze zbiorem biolife.db (w bazie DBDEMOS); umie take komponent TDataSource i skojarz go z komponentem TTable. W edytorze pl wybierz wszystkie pola (Add all fields), a nastpnie przecignij niektre z nich (lub wszystkie) na formularz. Po uporzdkowaniu komponentw formularz powinien wyglda mniej wicej tak, jak na rysunku 7.9.

341

Rysunek 7.9. Rezultat przecignicia pl z edytora pl na formularz

Przeanalizujmy, co si waciwie stao. Po pierwsze, dla kadego przecignitego pola Delphi wybrao najlepiej pasujc do jego typu kontrolk prezentacyjn: dla pl acuchowych jest to TDBEdit, dla grafiki TDBImage itp.); kada z kontrolek opatrzona zostaa ponadto etykiet zawierajc nazw pola. Po drugie aby wspomniane kontrolki mogy uzyska czno ze zbiorem danych, potrzebny jest powizany z nim komponent TDataSource; Delphi sprawdzio, i takowy istnieje ju na formularzu (w przeciwnym razie utworzyoby ad hoc nowy). Po otwarciu zbioru kontrolki prezentacyjne wypeniy si zawartoci odpowiadajcych im pl w biecym (pierwszym) rekordzie zbioru.

Pola typu BLOB


Pola tego typu (Binary Large OBject duy obiekt binarny) przeznaczone s do przechowywania danych binarnych bez okrelonej struktury czy wielkoci to samo pole BLOB moe w jednym rekordzie mie wielko 3 bajtw, w innym natomiast 3 kilobajtw. Pola BLOB s szczeglnie przydatne do przechowywania duych porcji tekstu, grafiki oraz surowych strumieni danych, ktrych doskonaym przykadem s obiekty OLE.

TBlobField a inne typy pl


Zgodnie z tabel 7.3, komponent TBlobField wywodzi si z klasy TField. Zbir typw pl, ktre mog by przez niego reprezentowane jest podzbiorem typu TFieldType i ma nastpujc definicj:
TBlobType = ftBlob .. ftOraClob;

za typ aktualnie reprezentowanego pola okrelony jest przez waciwo BlobType. Znaczenie poszczeglnych jej wartoci zostao wyjanione w tabeli 7.4. Tabela 7.4. Typy pl BLOB

342

Typ pola
ftBlob ftMemo ftGraphic ftFmtMemo ftParadoxOLE ftDBaseOLE ftTypedBinary ftCursor..ftDataSet ftOraBlob ftOraClob

Rodzaj danych zawartych w polu Dane nieskategoryzowane lub w formacie definiowanym przez uytkownika Informacja tekstowa Bitmapa Windows Sformatowane pole Memo systemu Paradox Obiekt OLE Paradoxa Obiekt OLE dBasea Binarna reprezentacja zdefiniowanego typu danych Niedopuszczalne dla pola BLOB Pola BLOB tabeli Oracle 8 Pola CLOB tabeli Oracle 8

W wikszoci przypadkw obrbka pl typu BLOB sprowadza si do ich przechowywania w strumieniu; ich zapisywanie i odczytywanie uatwia specjalny rodzaj strumienia TBlobStream, fizycznie stanowicy strumie zlokalizowany wewntrz tabeli. Poniszy przykad z pewnoci pomoe zrozumie jego funkcjonowanie.

Przykadowy projekt wykorzystujcy pola typu BLOB


Projekt Wavez.dpr znajduje si na zaczonym krku CD-ROM; wykorzystano w nim niektre mechanizmy Windows, nie jest wic zgodny ze standardami CLX. Jego formularz gwny przedstawia rysunek 7.10. Komponent TTable powizany jest z tabel o nastpujcej strukturze:

Nazwa pola
WaveTitle FileName Wave

Typ pola
Character Character BLOB

Rozmiar pola
25 25

Rysunek 7.10. Formularz gwny projektu Wavez.dpr Przycisk oznaczony plusem suy do wczytania materiau dwikowego z pliku dyskowego i dodania go do tabeli wykonuje to nieskomplikowana procedura obsugujca zdarzenie kliknicia przycisku:

procedure TMainForm.sbAddClick(Sender: TObject); begin if OpenDialog.Execute then begin

343

tblSounds.Append; tblSounds['FileName'] := ExtractFileName(OpenDialog.FileName); tblSoundsWave.LoadFromFile(OpenDialog.FileName); edTitle.SetFocus; end; end;

Po wybraniu konkretnego pliku (w ramach dialogu OpenDialog) nastpuje przeczenie zbioru danych w tryb doczania rekordu (Append). Nastpnie funkcja ExtractFileName() oczyszcza specyfikacj pliku z ewentualnej cieki, po czym nazwa pliku wpisywana jest do pola FileName zbioru danych tblSounds. Kolejna instrukcja dokonuje wczytania materiau dwikowego do pola BLOB reprezentowanego przez komponent tblSoundsWave zwr uwag, i caa ta operacja sprowadza si do wywoania jednej metody (LoadFromFile). Wczytany materia nie posiada jeszcze tytuu (pole WaveTitle nowego rekordu pozostaje niewypenione), dlatego te po zakoczeniu wczytywania aktywnym komponentem staje si edTitle, do ktrego uytkownik moe wpisa wybrany przez siebie tytu. Rwnie nieskomplikowany jest zapis materiau dwikowego do zewntrznego pliku, nastpujcy w wyniku kliknicia przycisku oznaczonego ikon dyskietki:

procedure TMainForm.sbSaveClick(Sender: TObject); begin with SaveDialog do begin FileName := tblSounds['FileName']; // nadaj nazw pliku

if Execute then

// dialog

tblSoundsWave.SaveToFile(FileName); // zapisz BLOB w pliku end; end;

W oknie dialogowym (SaveDialog) sucym do wyboru pliku dyskowego, domylna nazwa pliku pobierana jest z pola FileName biecego rekordu zbioru danych tblSounds; zapis zawartoci pola BLOB w pliku realizowany jest za pomoc pojedynczej metody (SaveToFile) komponentu reprezentujcego to pole (tblSoundsWave). Odtworzenie materiau dwikowego zawartego w polu BLOB jest ju jednak bardziej zoone. Samo odtwarzanie realizowane jest przez funkcj API o nazwie PlaySound(), konieczne jest jednak wykonanie kilku dodatkowych czynnoci pomocniczych:
procedure TMainForm.sbPlayClick(Sender: TObject); var B: TBlobStream; M: TMemoryStream; begin B := TBlobStream.Create(tblSoundsWave, bmRead); // utwrz strumie BLOB Screen.Cursor := crHourGlass; // kursor sygnalizujcy // oczekiwanie try M := TMemoryStream.Create; try M.CopyFrom(B, B.Size); // kopiuj ze strumienia BLOB // utwrz strumie pamiciowy

344

// do pamiciowego

// Sprbuj odtworzy plik dwikowy; wygeneruj wyjtek, // gdy co si nie powiedzie

Win32Check(PlaySound(M.Memory, 0, SND_SYNC or SND_MEMORY)); finally M.Free; end; finally Screen.Cursor := crDefault; B.Free; end; end; // zwolnij strumie

Powysza procedura rozpoczyna sw prac od utworzenia strumienia TBlobStream na bazie pola BLOB przechowujcego materia dwikowy. Pierwszy argument wywoania konstruktora jest nazw odnonego pola, drugi natomiast okrela zamierzony sposb jego uywania bmRead oznacza wycznie odczyt, bmReadWrite take zapis.
Wskazwka

Uycie trybu bmReadWrite w konstruktorze strumienia TBlobStream wymaga, aby zbir danych zawierajcy odnone pole BLOB znajdowa si w stanie edycji, wstawiania lub doczania rekordu.

Sam strumie TBlobStream nie daje si jednak odtworzy w sposb bezporedni materia dwikowy dla funkcji PlaySound() musi znajdowa si bd to w pliku dyskowym (podaje si wwczas nazw tego pliku), bd w pamici (podaje si wwczas wskanik odpowiedniego obszaru). Zawarto strumienia TBlobStream jest wic kopiowana do pomocniczego strumienia pamiciowego TMemoryStream; moe on by traktowany tak, jakby jego waciwo Memory stanowia wskanik do obszaru zawierajcego zawarto i ten wanie wskanik przekazywany jest jako argument wywoania funkcji PlaySound(). W momencie rozpoczcia kopiowania pomidzy strumieniami zmieniony zostaje domylny wygld kursora (cfDefault) na posta sygnalizujc zajto programu (crHourGlass); przywrcenie standardowej postaci kursora nastpuje dopiero po zakoczeniu odtwarzania, zwalniane s wwczas take strumienie TBlobStream i TMemoryStream. Kompletny kod moduu gwnego projektu przedstawiony jest na wydruku 7.4.

Wydruk 7.4. Modu gwnego formularza projektu Wavez.dpr


unit Main;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, DBCtrls, DB, DBTables, StdCtrls, Mask, Buttons, ComCtrls;

type TMainForm = class(TForm)

345

tblSounds: TTable; dsSounds: TDataSource; tblSoundsWaveTitle: TStringField; tblSoundsWave: TBlobField; edTitle: TDBEdit; edFileName: TDBEdit; Label1: TLabel; Label2: TLabel; OpenDialog: TOpenDialog; tblSoundsFileName: TStringField; SaveDialog: TSaveDialog; pnlToobar: TPanel; sbPlay: TSpeedButton; sbAdd: TSpeedButton; sbSave: TSpeedButton; sbExit: TSpeedButton; Bevel1: TBevel; dbnNavigator: TDBNavigator; stbStatus: TStatusBar; procedure sbPlayClick(Sender: TObject); procedure sbAddClick(Sender: TObject); procedure sbSaveClick(Sender: TObject); procedure sbExitClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); private procedure OnAppHint(Sender: TObject); end;

var MainForm: TMainForm;

implementation

{$R *.DFM}

uses MMSystem;

procedure TMainForm.sbPlayClick(Sender: TObject); var B: TBlobStream; M: TMemoryStream; begin B := TBlobStream.Create(tblSoundsWave, bmRead); // utwrz strumie BLOB Screen.Cursor := crHourGlass; // kursor sygnalizujcy // oczekiwanie

346

try M := TMemoryStream.Create; try M.CopyFrom(B, B.Size); // kopiuj ze strumienia BLOB // do strumienia pamiciowego // utwrz strumie pamiciowy

// Sprbuj odtworzy plik dwikowy; wygeneruj wyjtek, // gdy co si nie powiedzie

Win32Check(PlaySound(M.Memory, 0, SND_SYNC or SND_MEMORY)); finally M.Free; end; finally Screen.Cursor := crDefault; B.Free; end; end; // zwolnij strumie

procedure TMainForm.sbAddClick(Sender: TObject); begin if OpenDialog.Execute then begin tblSounds.Append; tblSounds['FileName'] := ExtractFileName(OpenDialog.FileName); tblSoundsWave.LoadFromFile(OpenDialog.FileName); edTitle.SetFocus; end; end;

procedure TMainForm.sbSaveClick(Sender: TObject); begin with SaveDialog do begin FileName := tblSounds['FileName']; // nadaj nazw pliku

if Execute then

// dialog

tblSoundsWave.SaveToFile(FileName); // zapisz BLOB w pliku end; end;

procedure TMainForm.sbExitClick(Sender: TObject); begin Close; end;

347

procedure TMainForm.FormCreate(Sender: TObject); begin Application.OnHint := OnAppHint; tblSounds.Open; end;

procedure TMainForm.OnAppHint(Sender: TObject); begin stbStatus.SimpleText := Application.Hint; end;

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction); begin tblSounds.Close; end;

end.

Filtrowanie danych
Filtrowanie danych polega na ograniczeniu widocznoci rekordw zbioru danych wycznie do tych rekordw, ktre speniaj okrelone kryterium; umoliwia ono take prost realizacj wyszukiwania rekordw. Wszystko to odbywa si za spraw (wycznie) kodu w Object Pascalu. Wyszukiwanie za pomoc filtrowania ma t zalet, i nie wymaga obecnoci indeksw; chocia na og przebiega ono mniej efektywnie ni to za pomoc indeksw (czym zajmiemy si w dalszej czci niniejszego rozdziau), jest jednak bardzo przydatne w niemal wszystkich aplikacjach bazodanowych. Zawenie zbioru widocznych rekordw na podstawie okrelonego kryterium nastpuje poprzez wykonanie nastpujcych czynnoci: 1. Oprogramowania zdarzenia OnFilterRecord zbioru danych zdarzenie to generowane jest dla kadego rekordu w zbiorze, natomiast procedura jego obsugi decyduje o widocznoci (lub niewidocznoci) badanego rekordu. Ustawienia na True waciwoci Filtered zbioru danych. bazy DBDEMOS fragment jej

2.

W charakterze przykadu przeanalizujmy tabel customer.db nieprzefiltrowanej zawartoci przedstawia rysunek 7.11.

348

Rysunek 7.11. Zbir danych w postaci nieprzefiltrowanej

Ograniczymy teraz zestaw widocznych rekordw tylko do tych, w ktrych nazwa firmy (w polu Company) rozpoczyna si od litery S; w tym celu musimy przypisa zdarzeniu OnFilterRecord nastpujc procedur zdarzeniow:

procedure TForm1.Table1FilterRecord(DataSet: TDataSet; var FieldVal: String; begin // pobierz warto pola Company FieldVal := DataSet['Company'];

var Accept: Boolean);

// zaakceptuj rekord, gdy zawarto pola zaczyna si od "S" Accept := (FieldVal<>'') and (FieldVal[1] = 'S');

end;

Po ustawieniu na True waciwoci Filtered i uruchomieniu projektu, zobaczymy tylko wybrane rekordy (rys. 7.12):

349

Rysunek 7.12. Rekordy stanowice wynik filtrowania

Wskazwka

Alternatywnym sposobem filtrowania rekordw jest wykorzystanie waciwoci Filter, zawierajcej formu okrelajc widoczno rekordu. Jest ona mniej uniwersalna ni zdarzenie OnFilterRecord wszak nie kade kryterium daje si zapisa za pomoc skondensowanej metody ma jednak pewne nastpstwa dla efektywnoci aplikacji. Na przykad w bazach danych SQL komponent TTable przekazuje (podczas otwarcia) do serwera zawarto swej waciwoci Filtered w klauzuli WHERE zapytania SQL, co zazwyczaj powoduje szybsze ustalenie waciwego podzbioru rekordw ni w przypadku przeszukiwania zbioru rekord po rekordzie.

Przeszukiwanie zbiorw danych


Istnieje wiele sposobw poszukiwania rekordw w zbiorach danych. Techniki opisywane w niniejszym podrozdziale s charakterystyczne raczej dla lokalnych zbiorw danych, nie korzystajcych z mechanizmw SQL. Metody wyszukiwania oparte na jzyku SQL opisane zostay w rozdziale 28. ksiki Delphi 4. Vademecum profesjonalisty.

FindFirst() i FindNext()
Klasa TDataSet definiuje metody FindFirst(), FindNext(), FindPrior() i FindLast() ustawiajce zbir danych na (odpowiednio) pierwszym, nastpnym, poprzednim i ostatnim rekordzie speniajcym kryterium okrelone przez procedur obsugi zdarzenia OnFilterRecord. Metody te s funkcjami bezparametrowymi, zwracajcymi warto typu Boolean, informujc, czy dany rekord zosta znaleziony.

350

Lokalizowanie rekordu za pomoc metody Locate()


Filtrowanie mona take wykorzysta do znalezienia konkretnego rekordu na podstawie okrelonej zawartoci jego wybranych pl. Temu celowi suy metoda Locate() klasy TDataSet poniewa opiera ona swe dziaanie na mechanizmie filtrowania, nie jest zalena od istnienia konkretnych indeksw, chocia jest w stanie wykorzystywa indeksy zgodne z kryterium poszukiwania. Metoda TDataSet.Locate() zadeklarowana jest nastpujco:

function Locate( const KeyFields: string; const KeyValues: Variant; Options: TLocateOptions): Boolean;

Pierwszy parametr (KeyFields) zawiera oddzielone rednikami nazwy pl (w szczeglnoci pojedyncz nazw) biorcych udzia w wyszukiwaniu. Drugi parametr (KeyValues) okrela dane wartoci tych pl, natomiast treci trzeciego parametru (Options) s dodatkowe opcje precyzujce scenariusz poszukiwania:

Type TLocateOption = (loCaseInsensitive, loPartialKey); TLocateoptions = set of TLocateOption;

Opcja loCaseInsensitive powoduje niewraliwo na wielko liter w porwnaniach; uycie opcji loPartialKey spowoduje uznanie pola za zgodne z porwnywanym wzorcem nawet wtedy, gdy wzorzec ten stanowi cz (podacuch) jego zawartoci. Ponisza instrukcja jest poleceniem znalezienia rekordu, ktry w polu o nazwie CustNo posiada warto 1356:

Table1.Locate('CustNo', 1356, []);

Metoda Locate zwraca warto True, gdy dany rekord zostanie znaleziony i False w przeciwnym razie.
Wskazwka

Metoda Locate ma t przewag nad innymi metodami wyszukiwania, i zawsze usiuje wybra najefektywniejsz metod wyszukiwania, na przykad dokonujc chwilowego przeczenia aktywnych indeksw. Nie jest jednak od tych indeksw zalena w aden sposb i nie narzuca aplikacji adnych ogranicze w kwestii gospodarowania indeksami dla innych celw.

Przeszukiwanie tabeli za pomoc indeksw


W niniejszym punkcie opiszemy podstawowe waciwoci i metody komponentu TTable, reprezentujcego tabel bazy danych. Szczegln uwag zwrcimy przy tym na zastosowanie indeksw do celw wyszukiwania rekordw, jak rwnie do definiowania zakresw (ranges), stanowicych jedn z odmian filtrowania.

Wyszukiwanie rekordw w tabelach


VCL oferuje kilka metod przeszukiwania tabel. W stosunku do tabel systemw dBase i Paradox Delphi zakada istnienie indeksw zdefiniowanych na polach okrelajcych kryterium poszukiwania. Nie ma takiego wymagania w stosunku do baz SQL, chocia brak indeksu zgodnego z kryterium wyszukiwania moe niekiedy pogorszy znaczco jego efektywno.

351

Na przykad zamy, i zamierzamy znale rekord posiadajcy okrelone wartoci w dwch polach numerycznym i alfanumerycznym. Moemy to zrobi wykorzystujc metod FindKey() albo par metod SetKey()GotoKey(). FindKey() Zadaniem metody FindKey()jest znalezienie rekordu zawierajcego cile okrelone wartoci w okrelonych polach. Jedynym parametrem wywoania typu array of const jest tablica wartoci wymaganych w tych wanie polach; sam zestaw pl nie jest specyfikowany w wywoaniu metody, gdy jest on ju zdeterminowany przez aktywny indeks. Na przykad, w celu znalezienia rekordu, zawierajcego w polach indeksujcych wartoci (kolejno) 123 i Hello, mona uy nastpujcego wywoania:
RecordFound := Table1.FindKey([123, 'CustNo']);

Wynik funkcji True albo False informuje o tym, czy dany rekord zosta znaleziony. W przypadku nieznalezienia rekordu bieca pozycja tabeli pozostaje niezmieniona. Jeeli liczba podanych wartoci pl jest mniejsza ni liczba pl indeksowych, brakujce wartoci przyjmuje si jako puste (NULL), tak wic np. wywoanie
Table1.FindKey([123]);

rwnowane jest
Table1.FindKey([123, '']);

Nietrudno spostrzec, i uywanie metody FindKey() wymaga, by pamita kolejno pl w aktywnym indeksie. Nie ma tej wady drugie ze wspomnianych rozwiza. SetKey()GotoKey() Wyszukiwanie za pomoc pary wywoa Setkey()...GotoKey()ma charakter nieco inny proces wyszukiwania przebiega dwuetapowo. Wywoanie metody SetKey()wprowadza tabel w specyficzny stan (dsSetKey); w tym stanie poszczeglnym polom indeksujcym zostaj przyporzdkowane kolejne wartoci te same, ktre w wywoaniu FindKey()podawane byy w skondensowanej, tablicowej postaci. Waciwe poszukiwanie rozpoczyna si w momencie wywoania metody GotoKey(). Oto przykad rwnowany poprzedniemu:
with Table1 do begin // pierwszy etap SetKey; Fields[0].AsInteger := 123; Fields[1].AsString := 'Hello'; // drugi etap RecordFound := GotoKey; end;

Numery pl odnosz si tutaj do ich kolejnoci w indeksie, nie w rekordzie numer 0 oznacza pierwsze pole indeksujce, itp. Uwany czytelnik mgby zapyta w tym miejscu, dlaczego instrukcja
Fields[0].AsInteger := 123;

nie powoduje przypisania wartoci 123 pierwszemu polu indeksujcemu? Skd komponent TTable wie, i jest to definiowanie kryterium wyszukiwania? Ot wywoanie metody SetKey() powoduje specyficzne przeczenie buforw mamy wic faktycznie do czynienia z zapisem wartoci do pl, nie s to jednak pola biecego rekordu, lecz tzw. rekordu kluczowego. Wywoanie metody GotoKey() przywraca pierwotny stan buforw.

352

Opisane rozwizanie nie uwalnia nas jeszcze od koniecznoci pamitania kolejnoci pl w indeksie; konieczno ta zniknie jednak, jeeli w miejsce numerw pl bdziemy si posugiwa ich nazwami:

with Table1 do begin // pierwszy etap SetKey; Fields['Factory'].AsInteger := 123; Fields['Passwd'].AsString := 'Hello'; // drugi etap RecordFound := GotoKey; end;

Dozwolone jest jednak uywanie wycznie pl indeksujcych; mimo wszystko musimy pamita ich zestaw. I jeszcze ciekawostka poniewa obydwie opisane metody wykonuj w gruncie rzeczy to samo zadanie, mona by podejrzewa midzy nimi jaki zwizek w bibliotece VCL. Istotnie:

function TTable.FindKey(const KeyValues: array of const): Boolean; begin CheckBrowseMode; SetKeyFields(kiLookup, KeyValues); Result := GotoKey; end;

Wyszukiwanie przyblione kryterium najlepszego dopasowania


Metody FindKey()oraz SetKey()...GotoKey() poszukuj rekordu, ktrego zawartoci pl s dokadnie rwne wartociom poszukiwanym. Delphi oferuje analogiczne mechanizmy dla wyszukiwania przyblionego, prowadzcego do znalezienia rekordu o zawartoci pl najlepiej pasujcej3 do poszukiwanego kryterium. Ich metodyka jest dokadnie taka sama, inne s tylko nazwy metod, bdcych notabene procedurami, nie funkcjami:

// pierwszy sposb: Table1.FindNearest([123, 'CustNo']);

// drugi sposb with Table1 do begin // pierwszy etap SetKey; Fields['Factory'].AsInteger := 123; Fields['Passwd'].AsString := 'Hello'; // drugi etap GotoNearest; end;

Tak naprawd jest to warto bezporednio wiksza (w kolejnoci aktywnego indeksu) od szukanej podobnie jak przy poszukiwaniu SOFTSEEK ON w dBase (przyp. tum.).

353

Jeli poszukiwanie zostanie uwieczone sukcesem i waciwo KeyExclusive zbioru danych rwna bdzie False, biecym rekordem bazy stanie si rekord znaleziony; jeeli KeyExclusive rwna bdzie True, rekordem biecym stanie si rekord nastpny (w kolejnoci indeksu) po znalezionym.
Wskazwka

Wszdzie, gdzie to jest moliwe, naley uywa metod FindKey()/FindNearest() zamiast Setkey()GotoKey()/GotoNearest(), poniewa te ostatnie wymagaj wicej kodowania i jako takie s bardziej podatne na bdy programisty.

Ktry indeks?
Dotychczas zakadalimy milczco, e aktywnym indeksem jest tzw. indeks gwny tabeli (primary index). Uycie w tej roli innego indeksu wymaga odpowiedniego ustawienia waciwoci IndexName, zawierajcej nazw indeksu. Oto przykad uycia, jako aktywnego, indeksu po polu Company, nazwanego byCompany w celu poszukiwania rekordu, w ktrym pole to ma warto 'Unisco':
with Table1 do begin IndexName := 'ByCompany'; SetKey; FieldValues['Company'] := 'Unisco'; GotoKey; end;

Wskazwka

Przeczeniu aktywnego indeksu towarzysz pewne czynnoci dodatkowe, ktre mog powodowa kilkusekundow zwok w dziaaniu aplikacji.

Poczeniem filtrowania z indeksowaniem jest w Delphi mechanizm zakresw (ranges). Do zakresu rekordw naley kady rekord zbioru danych, ktrego wyraenie kluczowe (rozumiane jako konkatenacja pl indeksujcych) nie wykracza poza zadane wartoci graniczne. Definiowanie zakresu moe odbywa si jednoetapowo, za pomoc metody SetRange(), bd na raty, za pomoc metod SetRangeStart(), SetRangeEnd() i ApplyRange().

Ostrzeenie

Dla tabel dBasea i Paradoxa pola definiujce zakres musz by polami indeksujcymi. Dla baz SQL-a warunek ten nie musi by speniony, lecz wwczas moe pogorszy si efektywno filtrowania.

SetRange() Podobnie jak metody FindKey() i FindNearest(), metoda SetRange() umoliwia kompleksowe wykonanie zoonego zadania. Parametrami jej wywoania s dwie tablice array of const, z ktrych pierwsza okrela dolne ograniczenia pl indeksujcych, natomiast druga grne ich ograniczenia. Ponisza instrukcja ustanawia zakres rekordw, w ktrych warto pierwszego pola indeksujcego jest nie mniejsza od 10 i nie wiksza od 15: 354

Table1.SetRange([10],[15]);

ApplyRange() Definiowanie zakresu mona take rozoy na raty poszczeglne etapy scenariusza wygldaj wwczas nastpujco: 1. 2. 3. 4. 5. Wywoanie metody SetRangeStart(). Przypisanie polom indeksujcym dolnych ogranicze zakresu. Wywoanie metody SetRangeEnd(). Przypisanie polom indeksujcym grnych ogranicze zakresu. Wywoanie metody ApplyRange().

Poprzedni przykad wykorzystujcy metod SetRange() mona zapisa w nastpujcej, rwnowanej postaci:
with Table1 do begin SetRangeStart; Fields[0].AsInteger := 10; // dolna granica zakresu SetRangeEnd; Fields[0].AsInteger := 15; // grna granica zakresu ApplyRange; end; // zatwierdzenie zakresu

Do usunicia zdefiniowanego zakresu i przywrcenia zbiorowi danych stanu sprzed wywoania SetRange() lub ApplyRange() suy metoda CancelRange():
Table1.CancelRange;

Moduy danych
Moduy danych (datamodules) umoliwiaj skoncentrowanie wszelkich regu i zalenoci zwizanych z bazami danych w pojedynczych obiektach, nadajcych si do wspdzielenia pomidzy formularzami, projektami, grupami czy nawet przedsibiorstwami. Modu danych reprezentowany jest w Delphi przez klas TDataModule i jej klasy pochodne. Jest rodzajem niewidocznego formularza, grupujcego wszelkie komponenty projektu zwizane z dostpem do danych. Fizyczne utworzenie jego obiektu odbywa si przez wybranie pozycji Data Module ze strony New repozytorium (dostpnej za porednictwem opcji File|New|Other). Podstawow korzyci wynikajc z uywania moduw danych jest moliwo wspdzielenia pomidzy wieloma formularzami tego samego ukadu komponentw np. TTable, TQuery i TStoredProc, ustanawiajcego okrelone powizania pomidzy tymi komponentami, czy te specyficzne ograniczenia na poziomie pojedynczych pl, jak np. warto minimalna i maksymalna czy format wywietlania. W ramach pojedynczego moduu danych mona nawet zawrze kompletne odzwierciedlenie regu biznesowych aplikacji (business rules), specyficznych dla danego przedsibiorstwa. W przypadku uywania wycznie zwykych formularzy hermetyzacja taka byaby mocno utrudniona. Moduy danych przejawiaj zreszt znacznie wiksz uniwersalno podobnie jak zwyke formularze, mog by umieszczane w repozytorium i wielokrotnie wykorzystywane w przyszoci na potrzeby tworzonych projektw. Nabiera to szczeglnego znaczenia w warunkach pracy zespoowej repozytorium powinno znajdowa si wwczas w katalogu dostpnym dla wszystkich zainteresowanych projektantw. Aby zademonstrowa wykorzystanie moduu danych, stworzylimy przykadowy projekt, w ktrym kilka formularzy odwouje si do tych samych danych. Nasz modu danych ma posta wrcz elementarn w profesjonalnych aplikacjach bazodanowych moduy danych s znacznie bardziej skomplikowane.

355

Wyszukiwanie, zakresy, filtrowanie


Aby zilustrowa omawiane w tym rozdziale zagadnienia, stworzylimy przykadowy projekt SRF.dpr znajduje si on na zaczonym krku CD-ROM. Celem tego projektu jest pokazanie prawidowego uycia filtrw, wyszukiwania na podstawie indeksw oraz definiowania zakresw. Projekt zawiera kilka formularzy; formularz gwny skada si w zasadzie tylko z przegldarki wywietlajcej dane, kady z pozostaych formularzy zostanie omwiony oddzielnie

Modu danych
Rozpoczniemy od moduu danych. Modu ten, nazwany DM, zawiera jedynie komponenty TTable i TDataSource. Komponent TTable o nazwie Table1 skojarzony jest z tabel customers.db w bazie DBDEMOS, komponent TDataSource (o nazwie DataSource1) stanowi rdo dla kadego komponentu operujcego danymi tabeli. Kod rdowy moduu danych znajduje si w module DataMod.pas.

Formularz gwny
Formularz gwny MainForm zosta przedstawiony na rysunku 7.13; jego plik rdowy nosi nazw Main.pas. Zawiera przegldark DBGrid1 skojarzon z komponentem DataSource1 moduu danych oraz przyciski umoliwiajce wybr pola kluczowego.

Rysunek 7.13. Formularz gwny projektu


Wskazwka

Aby komponent DataSource1 moduu danych dostpny by (na etapie projektowania) dla przegldarki umieszczonej w formularzu gwnym, naley umieci nazw DataMod na licie uses moduu Main.pas. Najprociej moemy to zrobi otwierajc okno moduu Main.Pas w edytorze kodu i wybierajc z menu gwnego IDE opcj File|Use Unit; wywietlona zostanie lista wszystkich moduw projektu, z ktrej naley wybra pozycj DataMod. Operacj t naley powtrzy dla kadego moduu, ktrego formularz pobiera informacj z moduu danych.

356

Pole wyboru o nazwie RGKeyField suy do wyboru aktywnego indeksu spord dwch istniejcych indeksw. Kod rdowy zwizany z t operacj wyglda nastpujco:
procedure TMainForm.RGKeyFieldClick(Sender: TObject); begin case RGKeyField.ItemIndex of 0: DM.Table1.IndexName := ''; // indeks gwny 1: DM.Table1.IndexName := 'ByCompany'; // indeks pomocniczy, po polu Company end; end;

Formularz gwny zawiera take komponent TMainMenu, umoliwiajcy otwieranie i zamykanie pozostaych formularzy. Kompletny tekst moduu Main.pas znajduje si na wydruku 7.5.

Wydruk 7.5. Main.Pas ilustracja funkcjonowania zakresw

unit Main;

interface

uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, Grids, DBGrids, DB, DBTables, Buttons, Mask, DBCtrls, Menus, KeySrch, Rng, Fltr;

type TMainForm = class(TForm) DBGrid1: TDBGrid; RGKeyField: TRadioGroup; MainMenu1: TMainMenu; Forms1: TMenuItem; KeySearch1: TMenuItem; Range1: TMenuItem; Filter1: TMenuItem; N1: TMenuItem; Exit1: TMenuItem; procedure RGKeyFieldClick(Sender: TObject); procedure KeySearch1Click(Sender: TObject); procedure Range1Click(Sender: TObject); procedure Filter1Click(Sender: TObject); procedure Exit1Click(Sender: TObject); private { Private declarations } public { Public declarations } end;

357

var MainForm: TMainForm;

implementation

uses DataMod;

{$R *.DFM}

procedure TMainForm.RGKeyFieldClick(Sender: TObject); begin case RGKeyField.ItemIndex of 0: DM.Table1.IndexName := ''; // indeks gwny 1: DM.Table1.IndexName := 'ByCompany'; // indeks pomocniczy, po polu Company end; end;

procedure TMainForm.KeySearch1Click(Sender: TObject); begin KeySearch1.Checked := not KeySearch1.Checked; KeySearchForm.Visible := KeySearch1.Checked; end;

procedure TMainForm.Range1Click(Sender: TObject); begin Range1.Checked := not Range1.Checked; RangeForm.Visible := Range1.Checked; end;

procedure TMainForm.Filter1Click(Sender: TObject); begin Filter1.Checked := not Filter1.Checked; FilterForm.Visible := Filter1.Checked; end;

procedure TMainForm.Exit1Click(Sender: TObject); begin Close; end;

end.

Notatka

358

Zwr uwag na nastpujc instrukcj w module Rng.Pas:

DM.Table1.SetRange([StartEdit.Text], [EndEdit.Text]);

Wartoci graniczne zadane s tutaj w postaci tekstowej, tymczasem jeden z indeksw opiera si na polu CustNo zawierajcym liczb cakowit. Nie ma w tym jednak adnej niekonsekwencji po prostu metody SetRange(), FindKey() i FindNearest()samoczynnie dokonuj niezbdnej konwersji pomidzy wartociami znakowymi i numerycznymi, zwalniajc tym samym programist od selektywnego stosowania funkcji w rodzaju IntToStr() i StrToInt().

Formularz szukania na podstawie indeksw


Formularz ten nosi nazw KeySearchForm i realizuje funkcje poszukiwania rekordu na podstawie wyraenia indeksowego zwizanego z aktywnym indeksem. Jego kod rdowy znajduje si w pliku KeySrch.Pas. Oprcz szukanej wartoci moliwe jest take okrelenie dwch aspektw poszukiwania: po pierwsze, moliwe jest wyszukiwanie w sposb przyrostowy (incremental) poszukiwanie zostaje wwczas przeprowadzone po kadej zmianie pola Szukaj:. Podczas wyszukiwania normalnego poszukiwanie rozpoczyna si dopiero po klikniciu jednego z przyciskw Dokadne i Przyblione, regulujcych (jak atwo wywnioskowa) drugi aspekt wyszukiwania. Kompletny kod moduu KeySrch.Pas przedstawia wydruk 7.6. Wydruk 7.6. Kod rdowy moduu KeySrch.Pas
unit KeySrch;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls;

type TKeySearchForm = class(TForm) Panel1: TPanel; Label3: TLabel; SearchEdit: TEdit; RBNormal: TRadioButton; Incremental: TRadioButton; Label6: TLabel; ExactButton: TButton; NearestButton: TButton; procedure ExactButtonClick(Sender: TObject); procedure NearestButtonClick(Sender: TObject); procedure RBNormalClick(Sender: TObject); procedure IncrementalClick(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); private procedure NewSearch(Sender: TObject); end;

359

var KeySearchForm: TKeySearchForm;

implementation

uses DataMod, Main;

{$R *.DFM}

procedure TKeySearchForm.ExactButtonClick(Sender: TObject); begin

{ Sprbuj znale rekord, dla ktrego wyraenie indeksowe rwne jest dokadnie poszukiwanej wartoci. Zauwa, i Delphi w razie potrzeby przeprowadza automatyczn konwersj pomidzy znakow, a numeryczn postaci pola. }

if not DM.Table1.FindKey([SearchEdit.Text]) then MessageDlg(Format('Brak rekordu dla "%s" .', [SearchEdit.Text]), mtInformation, [mbOk], 0); end;

procedure TKeySearchForm.NearestButtonClick(Sender: TObject); begin { Znajd rekord najlepiej pasujcy do poszukiwanego. Ponownie pamitaj o automatycznej konwersji pomidzy numeryczn a znakow postaci pola }

DM.Table1.FindNearest([SearchEdit.Text]); end;

procedure TKeySearchForm.NewSearch(Sender: TObject); { Ta metoda uruchamiana jest porednio przy zmianie pola edycyjnego, jeeli wczone jest szukanie przyrostowe }

begin DM.Table1.FindNearest([SearchEdit.Text]); // szukaj tekstu end;

360

procedure TKeySearchForm.RBNormalClick(Sender: TObject); begin

ExactButton.Enabled := True; NearestButton.Enabled := True; SearchEdit.OnChange := Nil; end;

// odblokuj przyciski Dokadny/Przybliony

// zablokuj zdarzenie OnChange

procedure TKeySearchForm.IncrementalClick(Sender: TObject); begin ExactButton.Enabled := False; NearestButton.Enabled := False; SearchEdit.OnChange := NewSearch; NewSearch(Sender); // odblokuj zdarzenie OnChange // szukaj wg biecej zawartoci pola // end; edycyjnego // zablokuj przyciski Dokadny/Przybliony

procedure TKeySearchForm.FormClose(Sender: TObject; var Action: TCloseAction); begin Action := caHide; MainForm.KeySearch1.Checked := False; end;

end.

Jak ju wspominalimy, Delphi automatycznie dokonuje ewentualnych niezbdnych konwersji pomidzy znakow a numeryczn postaci argumentw metod FindKey() i FindNearest(). Argumenty te pobiera wic mona bezporednio z pola edycyjnego. Na krtki komentarz zasuguje te realizacja szukania przyrostowego. W tym trybie szukania przyciski Dokadne i Przyblione s zablokowane, po kadej zmianie zawartoci pola edycyjnego przeprowadzane jest szukanie przyblione generowane zdarzenie OnChange wywouje procedur obsugi NewSearch() wywoujc metod FindNearest(). W trybie szukania normalnego przyciski Dokadne i Przyblione s dostpne, zalepione jest natomiast zdarzenie OnChange pola edycyjnego.

Formularz filtrowania
Formularz filtrowania skada si z dwch czci. Grna cz demonstruje poszukiwanie pierwszego, ostatniego, kolejnego lub poprzedniego rekordu w warunkach aktywnoci lub nieaktywnoci filtru, ktrym jest okrelona zawarto pola Ogranicz do stanu zwizana z polem State. Dolna cz zwizana jest z metod Locate() dwa pola edycyjne umoliwiaj wybr pola stanowicego kryterium poszukiwania oraz podanie danej wartoci tego pola. Grupa Dopasowanie umoliwia wybr i okrelenie opcji wyszukiwania: wybranie poszukiwania przyblionego powoduje wczenie opcji loPartialKey, natomiast brak zaznaczenia pola Rozrniaj mae/due litery wcza opcj loCaseInsensitive. Po kadorazowej zmianie opcji Wykonaj filtrowanie (komponent cbFiltered), jej stan kopiowany jest do waciwoci Filtered tabeli Table1 w module danych:

DM.Table1.Filtered := cbFiltered.Checked;

361

Jeeli kopiowana warto rwna jest True (opcja jest zaznaczona), tabela Table1 wykonuje filtrowanie posugujc si nastpujc procedur zdarzeniow (z moduu DataMod.pas):
procedure TDM.Table1FilterRecord(DataSet: TDataSet; var Accept: Boolean); begin { zaakceptuj rekord, jeeli zawarto pola edycyjnego w formularzu filtrowania rwna jest zawartoci pola State tabeli, reprezentowanego przez komponent Table1State }

Accept := Table1State.Value = FilterForm.DBEdit1.Text; end;

W wyniku kliknicia przycisku Lokalizuj wywoywana jest metoda Locate() wspomnianej tabeli, po uprzednim ustaleniu opcji loPartialKey i loCaseInsensitive:
procedure TFilterForm.LocateBtnClick(Sender: TObject); var LO: TLocateOptions; begin LO := []; if not CBCaseSens.Checked then Include(LO, loCaseInsensitive);

if RBClosest.Checked then Include(LO, loPartialKey);

if not DM.Table1.Locate(CBField.Text, EValue.Text, LO) then MessageDlg('Brak dopasowania', mtInformation, [mbOk], 0); end;

Nazwa pola, wzgldem ktrego odbywa si filtrowanie, pobierana jest z listy combo o nazwie CBField; lista ta kompletowana jest w czasie tworzenia formularza:

procedure TFilterForm.FormCreate(Sender: TObject); var i: integer; begin with DM.Table1 do begin for i := 0 to FieldCount - 1 do CBField.Items.Add(Fields[i].FieldName);

362

end; end;

Wskazwka:

Powysza procedura ma szans na poprawne wykonanie tylko wtedy, gdy w momencie tworzenia formularza TFilterForm modu danych DM jest ju kompletny, w przeciwnym razie otrzymamy komunikat o naruszeniu kontroli dostpu (access violation). Spenienie tego warunku moemy bardzo atwo zapewni, ustalajc waciw kolejno pozycji na licie formularzy tworzonych automatycznie (Autocreate forms), na karcie Forms opcji projektu: modu danych DM musi nastpowa zaraz po formularzu gwnym MainForm (ktry musi by tworzony w pierwszej kolejnoci).

Kompletny kod moduu formularza filtrowania przedstawia wydruk 7.7.

Wydruk 7.7. Fltr.Pas modu formularza filtrowania


unit Fltr;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons, Mask, DBCtrls, ExtCtrls;

type TFilterForm = class(TForm) Panel1: TPanel; Label4: TLabel; DBEdit1: TDBEdit; cbFiltered: TCheckBox; Label5: TLabel; SpeedButton1: TSpeedButton; SpeedButton2: TSpeedButton; SpeedButton3: TSpeedButton; SpeedButton4: TSpeedButton; Panel2: TPanel; EValue: TEdit; LocateBtn: TButton; Label1: TLabel; Label2: TLabel; CBField: TComboBox; MatchGB: TGroupBox; RBExact: TRadioButton; RBClosest: TRadioButton; CBCaseSens: TCheckBox; procedure cbFilteredClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure LocateBtnClick(Sender: TObject);

363

procedure SpeedButton1Click(Sender: TObject); procedure SpeedButton2Click(Sender: TObject); procedure SpeedButton3Click(Sender: TObject); procedure SpeedButton4Click(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); end;

var FilterForm: TFilterForm;

implementation

uses DB, DataMod, Main;

{$R *.DFM}

procedure TFilterForm.cbFilteredClick(Sender: TObject); begin { wcz filtrowanie gdy zaznaczona jest opcja "wykonaj filtrowanie" }

DM.Table1.Filtered := cbFiltered.Checked; end;

procedure TFilterForm.FormCreate(Sender: TObject); var i: integer; begin with DM.Table1 do begin for i := 0 to FieldCount - 1 do CBField.Items.Add(Fields[i].FieldName); end; end;

procedure TFilterForm.LocateBtnClick(Sender: TObject); var LO: TLocateOptions; begin LO := []; if not CBCaseSens.Checked then Include(LO, loCaseInsensitive);

if RBClosest.Checked

364

then Include(LO, loPartialKey);

if not DM.Table1.Locate(CBField.Text, EValue.Text, LO) then MessageDlg('Brak dopasowania', mtInformation, [mbOk], 0); end;

procedure TFilterForm.SpeedButton1Click(Sender: TObject); begin DM.Table1.FindFirst; end;

procedure TFilterForm.SpeedButton2Click(Sender: TObject); begin DM.Table1.FindNext; end;

procedure TFilterForm.SpeedButton3Click(Sender: TObject); begin DM.Table1.FindPrior; end;

procedure TFilterForm.SpeedButton4Click(Sender: TObject); begin DM.Table1.FindLast; end;

procedure TFilterForm.FormClose(Sender: TObject; var Action: TCloseAction); begin Action := caHide; MainForm.Filter1.Checked := False; end;

end.

Zakadki
Zakadki (bookmarks), zgodnie ze sw nazw, umoliwiaj zapamitanie biecej pozycji zbioru danych i pniejszy szybki powrt do niej na danie. atwo ich wykorzystywania wynika z faktu, i wymagaj operowania tylko jedn waciwoci. Zakadka reprezentowana jest w Delphi przez typ TBookmarkStr, a realizujc t koncepcj waciwoci zbioru danych jest Bookmark. Zawiera ona informacj o biecej pozycji w zbiorze; aby pozycj t zapamita, naley odczyta warto waciwoci i przechowa j w zmiennej typu TBookmarkStr:

365

var BM: TBookMarkStr; begin BM := Table1.BookMark;

Zmiana waciwoci Bookmark, przez przypisanie jej wartoci wczeniej zapamitanej, spowoduje ustawienie zbioru danych na pozycji odpowiadajcej tej wartoci:

Table1.Bookmark := BM;

Poniewa TBookmarkStr jest synonimem typu String


type TBookmarkStr = string;

zakadki nale do zmiennych o kontrolowanym czasie ycia (nie jest konieczne ich zwalnianie). Gdy jednak zakadka okazuje si niepotrzebna, moemy zwolni zajt przez ni pami, przypisujc jej acuch pusty:

BM := '';

Wybr acucha AnsiString jako reprezentacji dla zakadki powoduje jej niezaleno od konkretnej implementacji bazy danych, poniewa dane zakadki obsugiwane s cakowicie przez BDE.

Wskazwka

Mimo i nadal w Delphi dostpne s funkcje GetBookmark(), GotoBookmark() i FreeBookmark() pochodzce jeszcze z wersji Delphi 1, zaleca si uywanie waciwoci Bookmark i zmiennych TBookmarkStr jako konstrukcji wygodniejszych i mniej podatnych na bdy.

Funkcjonowanie zakadek mona przeledzi na przykadowym projekcie BookmarkDemo.dpr, ktry umiecilimy na zaczonym krku CD-ROM.

Podsumowanie
Celem niniejszego rozdziau byo wprowadzenie Czytelnika w problematyk programowania obsugi baz danych. Przedstawilimy komponenty wywodzce si z klasy TDataSet i reprezentujce zrnicowane technologicznie zbiory danych. Omwilimy sposoby poruszania si po zbiorze danych i manipulowania jego zawartoci. Zilustrowalimy take kilkoma przykadami moliwoci, jakie niesie ze sob filtrowanie i indeksowanie, w szczeglnoci rnorodne sposoby wyszukiwania rekordw. W kolejnych rozdziaach zajmiemy si now w Delphi 6 technologi dbExpress oraz mechanizmem dbGo, umoliwiajcym dostp do danych typu ADO.

366

Rozdzia 8.

Tworzenie aplikacji bazodanowych za pomoc technologii dbExpress


dbExpress jest now technologi Borlanda pozwalajc tworzy efektywne aplikacje bazodanowe w Delphi 6.

Technologia dbExpress wyrnia si pod wzgldem funkcjonalnoci z co najmniej z trzech powodw. Po pierwsze, jest wygodniejsz i zgrabniejsz (z punktu widzenia projektanta) ni jej poprzedniczka BDE. Po drugie, jest technologi midzyplatformow, umoliwiajc tworzenie aplikacji zgodnych z Kyliksem. Po trzecie jest technologi rozszerzaln (extensible). Podstaw architektury dbExpress s sterowniki dla rnych typw baz danych; kady z tych sterownikw implementuje zestaw interfejsw umoliwiajcych dostp do danych specyficznych dla serwera. Wspprac aplikacji z tymi sterownikami organizuj komponenty grupy DataCLX, funkcjonujce podobnie do komponentw BDE, tyle e bez niektrych zbdnych balastw.

Specyfika technologii dbExpress


Jedn z najwaniejszych cech architektury dbExpress, zapewniajcych jej wysok efektywno, jest jednokierunkowy charakter jej zbiorw danych.

Jednokierunkowe zbiory danych tylko do odczytu


Jednokierunkowy charakter zbiorw danych przesdza o braku buforowania rekordw, ktre jest pomocne jedynie przy nawigacji dwukierunkowej i modyfikacji danych. Rodzi to jednak pewne ograniczenia: Niedopuszczalne jest uywanie metod Last() i Prior() klasy TDataSet, spord metod nawigacyjnych mona uywa jedynie First() i Next(). Niemoliwa jest modyfikacja danych (z powodu braku buforw edycyjnych). Moliwe jest jednak edytowanie rekordw za pomoc innych komponentw (TClientDataSet, TSQLClientDataSet) zajmiemy si tym w dalszej czci rozdziau.

363

Jednokierunkowe zbiory danych nie realizuj filtrowania jako mechanizm odnoszcy si do wielu rekordw, wymagaoby ono buforowania wielorekordowego. Jednokierunkowe zbiory danych nie obsuguj pl przegldowych.

dbExpress kontra BDE


W przeciwiestwie do BDE, dbExpress nie zuywa zasobw serwera na potrzeby zapyta zwizanych z metadanymi lub innych dodatkowych polece podczas realizacji da uytkownika. dbExpress nie konsumuje take tyle zasobw klienckich, co BDE, gdy jednokierunkowe zbiory danych nie maj potrzeby cacheowania danych, za definicje metadanych przetwarzane s przez interfejsy implementowane w bibliotekach DLL.
dbExpress nie generuje take dodatkowych zapyta zwizanych np. z nawigowaniem po zbiorze danych, czy

te odczytem pl BLOB; do serwera trafiaj tylko zapytania wygenerowane przez aplikacj uytkownika. Skutkuje to nie tylko wiksz efektywnoci wykonywania aplikacji, lecz take czyni prostszym sam proces jej tworzenia.

dbExpress a aplikacje midzyplatformowe


Istotn zalet technologii dbExpress jest jej przydatno zarwno dla Delphi 6, jak i dla Kyliksa wykorzystuje ona komponenty CLX, a wic po skompilowaniu przez kompilator Kyliksa korzystajce z niej aplikacje mog by uruchamiane pod Linuksem. Moliwy staje si w ten sposb dostp do midzyplatformowych systemw baz danych, jak MySQL czy InterBase.

Komponenty dbExpress
Wszystkie komponenty zwizane z technologi dbExpress znajduj si na stronie dbExpress palety komponentw (widocznej zarwno w aplikacji windowsowej, jak i w aplikacji CLX).

TSQLConnection
Jest to odpowiednik komponentu TDatabase i tak naprawd jest do niego bardzo podobny (co z pewnoci potwierdz autorzy istniejcych aplikacji wykorzystujcych BDE). Nic w tym dziwnego, wszak obydwa te komponenty peni podobn funkcj: zadaniem komponentu TSQLConnection jest zapewnienie poczenia pomidzy danymi serwera, a innymi komponentami dbExpress. komponentu TSQLConnection regulowane jest przez dwa pliki konfiguracyjne: oraz dbxconnections.ini. S one instalowane jako elementy wspdzielonych skadnikw Borlanda, zazwyczaj w katalogu \Program Files\Common Files\Borland Shared\ \DbExpress. Plik dbxdrivers.ini zawiera list wszystkich obsugiwanych przez dbExpress sterownikw wraz z ustawieniami dotyczcymi tyche sterownikw. Plik dbxconnections.ini zawiera natomiast list tzw. pocze nazwanych(named connections), bdcych odpowiednikami aliasw BDE i zwizane z tymi poczeniami ustawienia. Moliwe jest zignorowanie zawartoci pliku dbxconnections.ini i samodzielne dostarczenie parametrw poczenia; naley w tym celu ustawi na True waciwo LoadParamsOnConnect komponentu. Przykad takiego postpowania zaprezentujemy za chwil.
dbxdrivers.ini

Funkcjonowanie

Komponent TSQLConnection musi uywa sterownika przeznaczonego dla konkretnego typu bazy danych i znajdujcego si na licie zawartej w pliku dbxdrivers.ini. Kompletny opis metod i waciwoci komponentu TSQLConnection znajduje si w systemie pomocy. W niniejszym rozdziale ograniczymy si do praktycznych przykadw zwizanych z nawizywaniem poczenia i tworzeniem nowych pocze.

364

Nawizywanie poczenia
W celu ustanowienia poczenia ze zbiorem danych, umie na formularzu komponent TSQLConnection i ustaw jego waciwo ConnectionName, wybierajc odpowiedni nazw z listy rozwijalnej (w inspektorze obiektw) powinny si tam znajdowa co najmniej cztery pozycje: IBLocal, DB2Connection, MSConnection i Oracle. Na uytek niniejszego rozdziau wykorzystamy poczenie IBLocal (jeeli nie zainstalowae serwera InterBase podczas instalacji Delphi 6, musisz zainstalowa go teraz). Zwr uwag, i po wybraniu poczenia automatycznie zostan zainicjowane niektre inne waciwoci, jak
DriverName, GetDriverFunc, LibraryName i VendorLib; przypisane im wartoci domylne pochodz

oczywicie z pliku dbxdrivers.ini. Do ustawienia pozostaych waciwoci i parametrw wykorzysta mona waciwo Params, skojarzon z edytorem prezentowanym na rysunku 8.1.

Rysunek 8.1. Edytor waciwoci TSQLConnection.Params

Wskazwka

Domyln wartoci parametru Database jest database.gdb, na og bezsensowna. Naley j zmieni na rzeczywist specyfikacj bazy danych w naszym przykadzie bdzie to baza Employee.gdb zlokalizowana w jednym z podkatalogw serwera InterBase (u nas C:\Borland\InterBase\examples\Database\).

Kiedy ju waciwoci komponentu zostan prawidowo ustawione, zmiana jego waciwoci Connected na True spowoduje rozpoczcie nawizywania poczenia. Wywietlone zostanie wwczas okno logowania, w ktre naley wpisa nazw uytkownika i haso w naszym przykadzie uytkownik to sysdba, za haso to masterkey.

Tworzenie nowego poczenia


Moliwe jest take tworzenie nowych nazwanych pocze odnoszcych si do wybranych baz danych. Okazuje si to pomocne np. w przypadku testowania nowej aplikacji na istniejcej bazie danych. By nie naraa na szwank zawartoci bazy danych, podane jest utworzenie jej kopii i skojarzenie z ni nowego poczenia; przeczanie pomidzy oryginaem a testow kopi bazy sprowadza si wwczas do zmiany waciwoci ConnectionName.

365

Do tworzenia nowych pocze suy edytor pocze, uruchamiany w wyniku dwukrotnego kliknicia komponentu TSQLConnection (lub za pomoc polecenia Edit Connection Properties z jego menu kontekstowego). Okno tego edytora jest przedstawione na rysunku 8.2.

Rysunek 8.2. Edytor pocze komponentu TSQLConnection Na pasku narzdziowym okna znajduje si pi przyciskw, sucych do (kolejno) dodawania, usuwania, zmiany nazwy i testowania poczenia oraz wywietlania ustawie dostpnych sterownikw. Po klikniciu pierwszego przycisku ukae si okno zawierajce pytania o wykorzystywany sterownik oraz nazw dla nowego poczenia; sterownik mona wybra z rozwijalnej listy, za jako nazw poczenia mona wpisa dowoln nazw odzwierciedlajc jego charakter, na przykad TestIBConnection. Po klikniciu przycisku OK ujrzymy na licie parametrw to, co zawarte jest pod waciwoci Params komponentu ponownie trzeba bdzie odpowiednio ustawi parametr Database. Po wykonaniu opisanych czynnoci mona zamkn edytor pocze, ustawi na True waciwo Connected komponentu i zalogowa si do bazy.

Pomijanie lub modyfikacja logowania


Ustawienie na False waciwoci LoginPrompt spowoduje, i okno logowania do bazy nie bdzie wywietlane, za nazwa uytkownika i haso pobrane zostan z parametrw (odpowiednio) User_Name i Password waciwoci Params.

Zastpienie standardowego okna logowania dialogiem wasnego pomysu jest moliwe dziki zdarzeniu OnLogin (waciwo LoginPrompt musi by ustawiona na True). Oto przykadowe rozwizanie:
procedure TMainForm.SQLConnection1Login(Database: TSQLConnection; LoginParams: TStrings); var UserName: String; Password: String; begin if InputQuery('Uytkownik', 'Podaj nazw uytkownika', UserName) then begin if InputQuery('Haso', 'Podaj haso', Password) then begin LoginParams.Values['User_Name'] := UserName;

366

LoginParams.Values['Password'] := Password; end; end; end;

W powyszym przykadzie dwukrotnie korzystamy z funkcji InputQuery() w celu pobrania acucha, za porednictwem okna opatrzonego tytuem i komentarzem. Przykad pochodzi z projektu LoginDemo.dpr, znajdujcego si na zaczonym krku CD-ROM; projekt ten zawiera rwnie ilustracj wykorzystania zdarze AfterConnect i AfterDisconnect.

Dorane ustalanie parametrw poczenia


Ustawienia dokonane przez edytor pocze, jak rwnie warto waciwoci Params, to ustawienia domylne, przechowywane w pliku dbxconnections.ini i pobierane z niego w czasie ustanawiania poczenia na etapie projektowania aplikacji. Ustawienia te wykorzystywane s w czasie wykonania programu aplikacja nie korzysta z pliku dbxconnections.ini. Moliwe jest jednak podpicie do aplikacji pliku zawierajcego charakterystyczne dla niej ustawienia poczeniowe. Naley wwczas ustawi na True waciwo LoadParamsOnConnect komponentu TSQLConnection, w rezultacie czego w momencie uruchomienia aplikacji wczyta on swe ustawienia poczeniowe z pliku okrelonego jako warto o nazwie Connection Registry File klucza HKEY_CURRENT_USER\Software\Borland\DBExpress rejestru systemowego. Waciwe ustawienie tej wartoci jest wwczas jednym z niezbdnych elementw procesu instalacyjnego aplikacji.

TSQLDataSet
Komponent TSQLDataSet reprezentuje jednokierunkowy zbir danych zawierajcy dane pochodzce z serwera; pod pojciem owego zbioru danych naley rozumie tabel, zapytanie SQL lub wynik procedury skadowanej. Charakter zbioru danych okrelaj dwie waciwoci komponentu CommandType i CommandText, ktrych znaczenie wyjanione jest w tabeli 8.1.

Tabela 8.1. Znaczenie waciwoci CommandType i CommandText komponentu TSQLDataSet


CommandType ctQuery ctStoredProc ctTable CommandText okrela

zapytanie SQL. nazw procedury skadowanej. nazw tabeli na serwerze bazy danych. Serwer oparty na technologii SQL automatycznie generuje instrukcje SELECT, powodujce pobranie wszystkich pl ze wszystkich rekordw tabeli.

Tak wic, gdy waciwo CommandType ma warto ctQuery, waciwo CommandText zawiera tre zapytania SQL okrelajcego struktur i zawarto zbioru danych na przykad SELECT * FROM CUSTOMER. Jeeli zapytanie SQL nie okrela zbioru wynikowego, lecz zawiera polecenie, naley w celu jego wykonania wywoa metod ExecSQL(). Gdy CommandType przybiera warto ctTable, CommandText zmienia si (w inspektorze obiektw) w list rozwijaln, umoliwiajc wybr jednej z tabel bazy danych; ewentualne zapytania SQL niezbdne do pobrania zawartoci wybranej tabeli generowane s automatycznie. Wreszcie dla waciwoci CommandType rwnej ctStoredProc waciwo CommandText zawiera nazw procedury skadowanej. Wykonanie tej procedury nastpuje w wyniku wywoania metody ExecSQL() komponentu, nie za przez ustawienie waciwoci Active na True.

367

Pobieranie zawartoci tabeli


Aby pobra zawarto tabeli, naley ustawi waciwo CommandType na ctTable i (po ewentualnym zalogowaniu si) wybra nazw danej tabeli z listy rozwijalnej towarzyszcej waciwoci CommandText. Ilustruje to przykadowy projekt TableData znajdujcy si na zaczonym krku CD-ROM.

Wywietlanie wyniku zapytania SQL


Aby pobra dane okrelone przez zapytanie SQL, naley ustawi waciwo CommandType na ctQuery i wpisa tre zapytania pod waciwo CommandText. Ilustrujcy to projekt ma nazw QueryData.dpr.

Wywietlanie zbioru stanowicego wynik procedury skadowanej


Aby otrzyma zawarto zbioru okrelonego jako wynik procedury skadowanej, naley okreli typ zbioru (CommandType) jako ctQuery, za zawartoci waciwoci CommandText uczyni zapytanie SQL odnoszce si do procedury skadowanej jako podmiotu. Jeeli w charakterze przykadu wemiemy nastpujc procedur
InterBase
CREATE PROCEDURE SELECT_COUNTRIES RETURNS ( RCOUNTRY VARCHAR(15), RCURRENCY VARCHAR(10) ) AS BEGIN FOR SELECT COUNTRY, CURRENCY FROM COUNTRY INTO :rCOUNTRY, :rCURRENCY DO SUSPEND; END

to treci wspomnianego zapytania mogoby by


Select * from SELECT_COUNTRIES

Zwr uwag, i uywamy tu nazwy procedury skadowanej w taki sam sposb, jak nazwy tabeli. Ilustracj opisanego wykorzystania procedury skadowanej jest projekt SProcData na zaczonym krku CD-ROM.

Wykonywanie procedury skadowanej


Aby wykona procedur skadowan, nie zwracajc zbioru wynikowego, naley ustawi ctStoredProc jako typ zbioru (CommandType), za jako CommandText poda nazw procedury (ktr mona wybra z listy rozwijalnej). W charakterze przykadu rozpatrzmy nastpujc procedur:
CREATE PROCEDURE ADD_COUNTRY ( ICOUNTRY VARCHAR(15), ICURRENCY VARCHAR(10) ) AS BEGIN INSERT INTO COUNTRY(COUNTRY, CURRENCY) VALUES (:iCOUNTRY, :iCURRENCY); SUSPEND; END

Powoduje ona wstawienie do tabeli nowej pozycji, okrelajcej nazw kraju i jego walut. Aby t procedur zrealizowa, naley wywoa metod ExecSQL() komponentu, na przykad jako reakcj na kliknicie odpowiedniego przycisku:
procedure TForm1.btnAddCurrencyClick(Sender: TObject); begin sqlDSAddCountry.ParamByName('ICountry').AsString := edtCountry.Text; sqlDSAddCountry.ParamByName('ICURRENCY').AsString := edtCurrency.Text; sqlDSAddCountry.ExecSQL(False); edtCountry.Text := ''; edtCurrency.Text := ''; end;

Dwie pierwsze instrukcje dokonuj waciwego ustawienia parametrw, przypisujc im wartoci pobrane z kontrolek edycyjnych. Po wykonaniu procedury zawarto kontrolek jest usuwana. Pojedynczy, boolowski

368

parametr metody ExecSQL() okrela, czy parametry zapytania SQL maj by poddane preparacji (True), czy te nie (False). Przykadowy projekt, ilustrujcy wykonanie procedury skadowanej za pomoc komponentu TSQLDataSet nosi nazw ExecSProc.

Reprezentacja metadanych
Za porednictwem komponentu TSQLDataSet mona pobiera nie tylko zawarto bazy danych, lecz take rnorodne informacje pomocnicze, okrelajce struktur tych danych, zwane metadanymi (metadata). Do pobierania rnych kategorii metadanych suy metoda SetSchemaInfo() komponentu:
procedure SetSchemaInfo(SchemaType: TSChemaType; SchemaObjectName, SchemaPattern: String);

Pierwszy z parametrw okrela kategori metadanych do pobrania, drugi natomiast nazw konkretnego obiektu w danej kategorii: tabeli, procedury skadowanej, jej parametru, indeksu itp. Ostatni, trzeci parametr zawiera tzw. wzorzec-mask SQL, uywany do filtrowania danych wynikowych. Dopuszczalne kategorie metadanych przedstawia tabela 8.2. Tabela 8.2. Kategorie metadanych rozrniane przez metod TSQLDataSet.SetSchemaInfo() Kategoria
stNoSchema

Znaczenie Brak informacji o kategorii; obiekt zaadowany zostaje danymi okrelonymi przez waciwoci CommandType i CommandText, a nie metadanymi serwera. Informacja o tabelach bazy danych, w zakresie okrelonym
TableScope stowarzyszonego komponentu TSQLConnection.

stTables

przez waciwo

stSysTables stProcedures stColumns stProcedureParams stIndexes

Informacja o tabelach systemowych serwera. Jeeli serwer nie wykorzystuje tabel systemowych do przechowywania metadanych, otrzymamy pusty zbir danych. Informacja o wszystkich procedurach skadowanych na serwerze. Informacja o wszystkich kolumnach (polach) okrelonej tabeli. Informacja o wszystkich parametrach okrelonej procedury skadowanej. Informacja o wszystkich indeksach zdefiniowanych dla okrelonej tabeli.

Dane wynikowe maj posta tabelaryczn i mog zosta wywietlone np. za pomoc komponentu TDBGrid. Ilustruje to przykadowy projekt SchemaInfo.dpr, znajdujcy si na zaczonym krku CD-ROM; jego formularz gwny zosta przedstawiony na rysunku 8.3.

Rysunek 8.3. Formularz projektu SchemaInfo.dpr

369

Gdy wybierzemy w polu opcji odpowiedni kategori i klikniemy przycisk Pobierz metadane, uzyskamy zestawienie metadanych tej kategorii, ujte w form tabelaryczn. Tre procedury zdarzeniowej przycisku przedstawiamy na wydruku 8.1. Wydruk 8.1. Pobieranie metadanych rnych kategorii w projekcie SchemaInfo.dpr
procedure TMainForm.Button1Click(Sender: TObject); begin sqldsSchemaInfo.Close; cdsSchemaInfo.Close; case RadioGroup1.ItemIndex of 0: sqldsSchemaInfo.SetSchemaInfo(stSysTables, '', ''); 1: sqldsSchemaInfo.SetSchemaInfo(stTables, '', ''); 2: sqldsSchemaInfo.SetSchemaInfo(stProcedures, '', ''); 3: sqldsSchemaInfo.SetSchemaInfo(stColumns, 'COUNTRY', ''); 4: sqldsSchemaInfo.SetSchemaInfo(stProcedureParams, 'SHOW_LANGS', ''); 5: sqldsSchemaInfo.SetSchemaInfo(stIndexes, 'COUNTRY', ''); end; sqldsSchemaInfo.Open; cdsSchemaInfo.Open; end;

Wywoanie metody SetSchemaInfo() powoduje odpowiednie ustawienie ukrytego pola FSchemaInfo:


procedure TCustomSQLDataSet.SetSchemaInfo(SchemaType: TSchemaType; SchemaObjectName, SchemaPattern: string); begin FSchemaInfo.FType := SchemaType; FSchemaInfo.ObjectName := SchemaObjectName; FSchemaInfo.Pattern := SchemaPattern; end;

Jak wynika z wydruku 8.1, ustawienie to dokonywane jest przy zamknitym zbiorze danych pobranie metadanych nastpuje przy jego otwarciu. Konstruktor komponentu ustawia typ tego pola na stNoSchema, co oznacza normalne pobieranie danych okrelone przez waciwoci CommandType i CommandText; kada modyfikacja wspomnianych waciwoci rwnie resetuje typ pola do wartoci stNoSchema.

Komponenty kompatybilne
Najbardziej wyranym przejawem wstecznej kompatybilnoci technologii dbExpress z BDE jest obecno w palecie takich komponentw, jak TSQLTable, TSQLQuery i TSQLStoredProc; funkcjonuj one podobnie jak ich odpowiedniki z BDE, z ograniczeniami wynikajcymi z jednokierunkowoci zbioru danych. Nie oferuj nic nowego ponad to, co udostpnia komponent TSQLDataSet.

TSQLMonitor
Komponent TSQLMonitor okazuje si bardzo przydatny w procesie ledzenia aplikacji SQL; rejestruje on polecenia SQL uywane w komunikacji z komponentem poczeniowym TSQLConnection, wskazywanym przez sw waciwo SQLConnection. Rejestrowane polecenia przechowywane s na licie acuchw wskazywanej przez waciwo TraceList; zawarto tej listy mona skopiowa np. do komponentu TMemo w celu czytelnego jej wywietlenia tak te uczynilimy w naszym przykadowym projekcie SQLMonitor.dpr, czego wiadectwem jest rysunek 8.4.

370

Rysunek 8.4. Wynik przykadowego ledzenia aplikacji za pomoc komponentu TSQLMonitor


Wskazwka

Ustawienie na True waciwoci AutoSave komponentu TSQLMonitor powoduje samoczynne zapisywanie listy komunikatw w pliku o nazwie okrelonej przez waciwo FileName.

Edytowalne, dwukierunkowe zbiory danych dbExpress


Jak dotd komponenty dbExpress jawiy si nam jako realizacja idei jednokierunkowego, niemodyfikowalnego zbioru danych no, moe z wyjtkiem procedury skadowanej ADD_COUNTRY. Nie oznacza to jednak, i baza danych, w poczeniu z ktr funkcjonuj komponenty dbExpress, nie da si za ich pomoc w ogle edytowa; za chwil przedstawimy komponent, ktry umoliwia nie tylko dwukierunkow nawigacj po zbiorach danych dbExpress, lecz take modyfikacj danych na serwerze.

TSQLClientDataSet
Komponent TSQLClientDataSet skrywa w sobie dwa komponenty TSQLDataSet i TProvider; pierwszy z nich zapewnia mu efektywny dostp do danych, drugi natomiast dwukierunkow nawigowalno i moliwo edycji danych. W architekturze komponentw danej aplikacji komponent TSQLClientDataSet reprezentuje zbir danych, podobnie jak TSQLDataSet: z jednej strony zwizany jest z komponentem poczeniowym za pomoc swej waciwoci DBConnection, z drugiej natomiast skojarzony jest z komponentem TDataSource poprzez waciwo DataSet tego ostatniego. Moesz si o tym przekona, analizujc przykadowy projekt o nazwie Editable.dpr, znajdujcy si na zaczonym krku CD-ROM. Uruchomiwszy wspomniany projekt, moesz porusza si po zbiorze danych w obydwu kierunkach, jak rwnie dodawa, modyfikowa i usuwa jego rekordy. Gdyby jednak docelowy zbir danych zamkn, okazaoby si, i po tych zmianach nie ma ladu, gdy zostay przeprowadzone jedynie w pamici aplikacji-klienta. Aby nada im trway charakter, naley wywoa metod ApplyUpdates() komponentu TSQLClientDataSet. W naszym przykadowym projekcie wywoujemy j w procedurze SQLClientDataSet1AfterPost(), obsugujcej zdarzenia AfterDelete i AfterPost, co powoduje aktualizacj danych na serwerze rekord po rekordzie. Notabene postpowanie takie nie jest bynajmniej charakterystyczne dla technologii dbExpress; na gruncie technologii MIDAS na identycznej zasadzie funkcjonowa komponent TClientDataSet, czego obszerny przykad zamiecilimy w rozdziale 33. ksiki Delphi 4. Vademecum profesjonalisty.

371

Wskazwka

Komponent TSQLClientDataSet nie udostpnia programicie swych pomocniczych komponentw TSQLDataSet i TProvider s one wskazywane przez jego prywatne pola FDataSet i FProvider, za waciwo Provider jest waciwoci chronion (protected). Chcc dowolnie manipulowa ich waciwociami, musimy wic uy ich niezalenych egzemplarzy, zamiast komponentu TSQLClientDataSet.

Realizacja aplikacji dbExpress


Bazodanow aplikacj opart na technologii dbExpress mona zrealizowa na dwa sposoby: jako monolit zawierajcy statycznie doczone sterowniki do obsugi baz danych, bd te ze sterownikami wydzielonymi w postaci bibliotek DLL. W pierwszym przypadku naley doczy do projektu moduy wyszczeglnione w tabeli 8.3. Tabela 8.3. Moduy wymagane w monolitycznej wersji aplikacji dbExpress Modu
dbExpInt dbExpOra dbExpDb2 dbExpMy CrtL, MidasLib

Przeznaczenie Poczenie z bazami danych InterBase Poczenie z bazami danych Oracle Poczenie z bazami danych DB2 Poczenie z bazami danych MySQL Wymagane przez aplikacje wykorzystujce komponenty klienckich zbiorw danych, jak
TSQLClientDataSet.

Realizujc aplikacj w wersji z wydzielonymi sterownikami, powinnimy doczy do niej wybrane (lub wszystkie zalenie od potrzeb) biblioteki spord wymienionych w tabeli 8.4.

Tabela 8.4. Biblioteki DLL wymagane przez aplikacj dbExpress w wersji rozdzielonej Biblioteka
dbexpint.dll dbexpora.dll dbexpdb2.dll dbexpmy.dll Midas.dll

Przeznaczenie Poczenie z bazami danych InterBase Poczenie z bazami danych Oracle Poczenie z bazami danych DB2 Poczenie z bazami danych MySQL Wymagane przez aplikacje wykorzystujce komponenty klienckich zbiorw danych, jak
TSQLClientDataSet.

Podsumowanie
Technologia dbExpress umoliwia efektywny dostp do danych, niemoliwy do zrealizowania za pomoc BDE. Owa efektywno wie si jednak z ograniczeniami w postaci jednokierunkowego charakteru zbiorw danych i niemonoci ich (bezporedniej) modyfikacji. Ograniczenia te mona jednak przeama za pomoc komponentw TSQLClientDataset i TClientDataSet, zapewniajcych wewntrzne buforowanie oraz transakcyjne uaktualnianie danych na serwerze, za pomoc metody ApplyUpdates(). Spord dostpnych w Delphi technologii bazodanowych, w chwili obecnej jedynie dbExpress umoliwia tworzenie aplikacji midzyplatformowych.

372

Cz IV Projektowanie i wykorzystywanie komponentw

387

Rozdzia 10.

Architektura komponentw: VCL i CLX


Niektrzy programici pamitaj zapewne bibliotek OWL (Object Windows Library) stanowic cz Turbo Pascala. Jej pojawienie si uprocio znacznie tradycyjny proces programowania w Windows, czynic prac programistw bardziej efektywn i produktywn przynajmniej w stosunku do tego, z czym przyszo im si zmaga do tej pory. Obiekty OWL uwolniy programist od wielu nucych szczegw zwizanych z klasyfikowaniem komunikatw w rozbudowanych instrukcjach case, zarzdzaniem oknami itp. Innymi sowy, otwarta zostaa droga do nowej metodologii tworzenia aplikacji programowania obiektowego. Biblioteka VCL wprowadzona w Delphi 1 okazaa si godnym nastpc OWL, zbudowanym jednake na odmiennych zasadach. W kolejnych wersjach Delphi biblioteka ta ulegaa cigemu rozwojowi, chociaby w zwizku z rozwojem samego Object Pascala, czy te przejciem (w Delphi 2) na platform 32-bitow; jej natura pozostaa jednak jak dotd niezmieniona. W Delphi 6 pojawi si natomiast element dla niej konkurencyjny biblioteka CLX, anonsowana przez Borland jako biblioteka nowej generacji, stanowica podstaw tworzenia rodzimych aplikacji dla Windows i Linuksa oraz projektowania komponentw do wielokrotnego uytku. Zarwno VCL, jak i CLX zaprojektowane zostay do wsppracy ze rodowiskiem Delphi 6; umoliwia to wizualne tworzenie aplikacji, zamiast mudnego wpisywania w kod rdowy fragmentw odpowiadajcych poszczeglnym aspektom funkcjonalnoci okien.

Biblioteki VCL i CLX s do skomplikowane, a to, w jakim stopniu bdziemy musieli zapozna sie z ich szczegami, zaley od celu naszej pracy moemy budowa kompletne aplikacje lub projektowa nowe komponenty. W pierwszym przypadku wystarczajca jest zazwyczaj znajomo interfejsu uywanych komponentw ich waciwoci, metod i zdarze oraz oczywicie zrozumienie zasad programowania obiektowego: enkapsulacji, dziedziczenia i polimorfizmu. Projektowanie nowych komponentw wymaga dogbnej znajomoci wewntrznych szczegw bibliotek VCL i CLX, wszak jedn z pierwszych decyzji projektowych jest wybr waciwej klasy bazowej dla nowego komponentu; niezbdna jest te w wikszoci przypadkw umiejtno operowania niskopoziomowymi mechanizmami systemw operacyjnych (np. komunikatami Windows), jak rwnie znajomo zasad integracji komponentw z elementami rodowiska IDE. Niniejszy rozdzia jest wprowadzeniem do bibliotek VCL i CLX. Przedstawiamy tu ogln hierarchi ich komponentw oraz ich przeznaczenie, a take najwaniejsze ich waciwoci, metody i zdarzenia. Na zakoczenie zajmiemy si niektrymi szczegami mechanizmu RTTI.

389

CLX odsona pierwsza


Midzyplatformowa biblioteka CLX, bdca jedn z nowoci Delphi 6, skada si z czterech elementw wymienionych w tabeli 10.1.

Tabela 10.1. Funkcjonalny podzia biblioteki CLX Grupa


VisualCLX DataCLX

Zawarto Midzyplatformowe komponenty interfejsu uytkownika i funkcje graficzne. Komponenty dla Windows i Linuksa mog rni si midzy sob. Komponenty aplikacji-klientw bazodanowych, realizujce stron klienta w bazodanowych aplikacjach lokalnych, klient-serwer i wielowarstwowych. Kady komponent tej grupy jest wsplny dla Windows i Linuksa. Komponenty internetowe realizujce m.in., technologie Apache DSO i CGI Web Broker. Wsplne dla Windows i Linuksa. Biblioteka czasu wykonania (runtime library). Jej kod rdowy jest identyczny dla Windows i Linuksa, z tym, e pod Linuksem nosi ona nazw BaseRTL.

NetCLX RTL

W niniejszym rozdziale zajmiemy si gwnie komponentami z grupy VisualCLX. Zrealizowane zostay na bazie wyprodukowanej przez firm Troll Tech biblioteki Qt (cute lub, jak chc autorzy, kyu-tee) i w chwili obecnej obsuguj dwie platformy systemowe Windows i Linuksa.

Co to jest komponent?
Komponenty to podstawowe cegieki, z ktrych autor aplikacji tworzy elementy interfejsu programisty oraz te elementy aplikacji, ktre co prawda nie s bezporednio widoczne, lecz stanowi nie mniej wan jej cz. Komponenty zmagazynowane s w palecie komponentw, skd moemy je pobiera na formularz, modyfikowa ich waciwoci, a take wypenia treci procedury zdarzeniowe, okrelajc w ten sposb reakcje komponentu na rnorodne zachowania kocowego uytkownika aplikacji i rnorodne zdarzenia zachodzce w systemie. Przez twrcw nowych komponentw postrzegane s przede wszystkim jako klasy w jzyku programowania w Delphi jest to Object Pascal, cho niewykluczone jest uycie w tym celu rwnie C++ i jzyka asemblera. Funkcjonalna zoono komponentu ograniczona jest przy tym jedynie inwencj (i zamierzeniami) projektanta, tak wic obok np. prostych etykiet tekstowych (TLabel), mog w aplikacji istnie komponenty realizujce kompletne arkusze kalkulacyjne! Kluczem do zrozumienia bibliotek VCL i CLX jest znajomo rnorodnych typw komponentw zawartych w tych bibliotekach. Podane jest zrozumienie wsplnych elementw komponentw, jak rwnie zapoznanie si z hierarchi samych komponentw i przeznaczeniem poszczeglnych szczebli tej hierarchii.

Hierarchia komponentw
Rysunki 10.1 i 10.2 przedstawiaj hierarchi komponentw w bibliotekach (odpowiednio) VCL i CLX; ju na pierwszy rzut oka mona zauway due podobiestwo pomidzy obydwiema bibliotekami.

390

Rysunek 10.1. Hierarchia klas VCL

391

Rysunek 10.2. Hierarchia klas CLX

Komponenty niewizualne
Komponenty niewizualne (nonvisual components) nie s widoczne dla kocowego uytkownika aplikacji, s jednak dostpne w czasie projektowania aplikacji, kiedy to za pomoc inspektora obiektw mona modyfikowa ich waciwoci determinujce okrelone zachowanie, jak rwnie programowa obsug wybranych zdarze. Przykadem komponentw niewizualnych s TTimer, TTable, a take standardowe okienka dialogowe (np. TOpenDialog). Jak wida na rysunkach 10.1 i 10.2, komponenty niewizualne wywodz si bezporednio z klasy TComponent.

Komponenty wizualne
Zgodnie ze sw nazw, komponenty wizualne s widocznymi dla uytkownika elementami interfejsu graficznego, przy czym niekoniecznie musz wykazywa przejawy interakcji z uytkownikiem. Komponenty te wywodz si bezporednio z klasy TControl, ktra definiuje waciwoci zwizane m.in. z geometri i wygldem komponentu, jak Top, Left, Color itp.
Notatka

Naley zdawa sobie spraw z rnicy pomidzy czsto uywanymi okreleniami komponent i kontrolka (control); kontrolka jest widzialnym elementem interfejsu uytkownika, natomiast komponentem jest kady obiekt, ktry moe pojawi si w palecie komponentw, i ktrego waciwociami mona operowa na etapie projektowania. Kada kontrolka wywodzi si w Delphi z klasy TControl, bdcej z kolei pochodn klasy

392

TComponent tak wic kada kontrolka jest jednoczenie komponentem, nie kady komponent jest jednak kontrolk.

Wszystkie kontrolki podzieli mona na dwie rozczne grupy: pierwsz grup stanowi te, ktre mog przyjmowa skupienie (focus), drug wszystkie pozostae.

Kontrolki zdolne przyjmowa skupienie


Ta grupa kontrolek wywodzi si z klasy TWinControl (w bibliotece VCL) lub TWidgetControl (w CLX). Kontrolki bazujce na klasie TWinControl stanowi otoczki standardowych kontrolek Windows (std nazywane s czsto kontrolkami okienkowymi windowed controls), natomiast kontrolki wywodzce si z klasy TWidgetControl obudowuj obiekty ekranowe biblioteki Qt. Wrd najwaniejszych cech charakteryzujcych opisywane kontrolki wymieni naley nastpujce:

mog przyjmowa skupienie i tym samym stanowi obiekt docelowy dla zdarze generowanych przez klawiatur; posiadaj elementy interakcji z uytkownikiem; mog peni rol kontrolek rodzicielskich (parents) dla innych kontrolek, zwanych kontrolkami potomnymi (childrens); posiadaj waciwo Handle, stanowic cznik z odpowiednim mechanizmem systemu operacyjnego.

Notatka

Waciwo Handle wystpuje zarwno w klasie TWinControl, jak i TWidgetControl. W klasie TWinControl reprezentuje ona okno zwizane z kontrolk i zawiera fizycznie uchwyt tego okna, natomiast w klasie TWidgetControl stanowi wskanik do odpowiedniego obiektu ekranowego; wskanik ten nazywany bywa czsto gadetem. Fakt, i rzeczona waciwo posiada t sam nazw w obydwu bibliotekach (VCL i CLX) przyczynia si do atwiejszego tworzenia aplikacji midzyplatformowych, upraszcza te proces przenoszenia aplikacji z VCL do CLX.

Dalszymi szczegami kontrolek TWinControl i TWidgetControl zajmiemy si w nastpnych rozdziaach.

Uchwyty
Uchwytami (handles) nazywamy 32-bitowe liczby cakowite, stanowice wskaniki do okrelonych obiektw Win32. Tych ostatnich nie naley myli z obiektami Delphi w Win32 wystpuj trzy rodzaje obiektw:

obiekty jdra (kernel) na przykad zdarzenia, pamiciowe odwzorowania plikw (memorymapped files) i procesy;

obiekty uytkownika (user) kontrolki edycyjne, listy, przyciski itp.

obiekty graficzne (GDI) bitmapy, pdzle, pira, czcionki itd.

Kade okno w Win32 posiada unikatowy uchwyt, ktry reprezentuje je w odwoaniach do funkcji API. Dziki Delphi uytkownik uwolniony jest w wikszoci przypadkw od operowania tymi funkcjami (na

393

rzecz operowania rwnowanymi metodami obiektw), jeeli jednak uchwyt odnonego okna okazuje si niezbdny, jest dostpny pod waciwoci Handle.

Kontrolki niezdolne do przyjmowania skupienia


Niektre kontrolki, mimo i widoczne na ekranie, nie s w stanie przyjmowa skupienia, nie posiadaj wic zdolnoci interakcji z uytkownikiem. S one (przewanie) uywane jedynie do wywietlania elementw interfejsu i z tego wzgldu nazywane s kontrolkami graficznymi (graphical controls). Ich klas bazow jest TGraphicControl (por. rysunki 10.1 i 10.2). Poniewa kontrolki graficzne nie mog przyjmowa skupienia, nie posiadaj reprezentujcego je uchwytu (lub w CLX gadetu). Skutkuje to mniejszym (w stosunku do kontrolek okienkowych) obcieniem zasobw, lecz niesie ze sob pewne ograniczenia: kontrolki graficzne nie mog posiada kontrolek potomnych. Przykadami kontrolek graficznych s m.in. TImage, TBevel i TPaintBox.

Struktura komponentu
Kady komponent jest obiektem jzyka Object Pascal, mieszczcym w sobie wybrane aspekty i mechanizmy, ktrych uywa projektant tworzcy aplikacj. Bdc obiektem, kady komponent posiada naturalne cechy obiektowe (pola, metody i waciwoci); naley jednake do domeny okrelonej klasy (TComponent) i posiada rwnie cechy charakterystyczne tylko dla niej.
Notatka

Naley odrni od siebie pojcia klasy i komponentu: klasa jest struktur jzyka Object Pascal, natomiast komponent jest klas posiadajc moliwoci wsppracy ze rodowiskiem IDE.

Waciwoci komponentu
Idea waciwoci (properties) zostaa opisana w rozdziale 2. S one mechanizmem umoliwiajcym odczytywanie oraz przypisywanie wartoci polom komponentu, w sposb okrelony przez projektanta. Za same przedmiotowe pola s najczciej polami ukrytymi przed uytkownikiem (posiadaj kategori private lub protected), co zabezpiecza je przed niekontrolowan modyfikacj.

Dostp do pl za porednictwem waciwoci


Dziki mechanizmowi waciwoci, odczytywanie i zmiana wartoci pl obiektu nie dokonuj si ju za pomoc zwykego kopiowania wartoci; definicja waciwoci moe spowodowa, e przypisanie (odczytanie) wartoci bdzie si wiza z wykonaniem pewnej metody. Spjrzmy na poniszy przykad:
TCustomEdit = class(TWinControl) Private FMaxLength: integer; Protected procedure SetMaxLength(Value: integer); published Property MaxLength: Integer read FMaxLength write SetMaxlength default 0; end;

Deklaracja waciwoci MaxLength stanowi, i reprezentowana przez ni warto bdzie typu integer. Klauzule read i write okrelaj sposb przypisywania wartoci i jej odczytywania. Po sowie read wystpuje nazwa pola FMaxLength, co oznacza, e odczytanie wartoci waciwoci MaxLength jest rwnowane odczytaniu tego wanie pola. Po sowie write wystpuje jednake nazwa metody a to oznacza, e przypisanie waciwoci MaxLength nowej wartoci odbywa si przez wykonanie tej metody. Jeeli wic w tekcie programu wystpi nastpujce instrukcje:

394

L := X.MaxLength; ...... X.MaxLength := K

to fizycznie bd one rwnowane nastpujcym instrukcjom:


L := X.FMaxLength; ..... X.SetMaxLength(K);

Gdyby w klauzuli read rwnie okreli nazw metody


Property MaxLength: Integer read GetMaxLength write SetMaxlength default 0;

instrukcje
L := X.MaxLength; ...... X.MaxLength := K;

byyby rwnowane nastpujcym:


L := X.GetMaxlength(); ...... X.SetMaxLength(K);

Pomijajc jedn z klauzul, read lub write, moemy uczyni waciwo tylko odczytywaln lub tylko zapisywaln. Klauzula default okrela warto domyln waciwoci jako 0 ma ona znaczenie przy zapisywaniu komponentu do strumienia; zajmiemy si tym zagadnieniem w nastpnym rozdziale.

Metody dostpowe waciwoci


W poprzednim przykadzie przypisanie nowej wartoci waciwoci MaxLength realizowane byo jako wywoanie metody SetMaxLength. Metody obiektu, ktrych identyfikatory pojawiaj si w klauzulach read lub write deklaracji waciwoci nosz nazw metod dostpowych (access methods); nazwa ta do trafnie okrela ich przeznaczenie zapewniaj dostp do wartoci waciwoci oraz do samej waciwoci (w celu jej modyfikacji). Metoda dostpowa przypisujca waciwoci now warto musi by jednoparametrow procedur, a typ jej parametru tosamy z typem waciwoci (wyjtkiem s waciwoci tablicowe i waciwoci indeksowane, opisywane w nastpnych rozdziaach przyp. tum.). W naszym przykadzie metoda ta zdefiniowana jest nastpujco:
Procedure TCustomEdit.SetMaxLength(Value: Integer); begin if FMaxLength <> Value then begin FMaxLength := Value; if HandleAllocated then SendMessage(Handle, EM_LIMITTEXT, Value, 0) end; end;

Przyjrzyjmy si bliej jej treci. Na samym pocztku sprawdza, czy prba nadania waciwoci nowej wartoci nie jest przypadkiem zbdn fatyg. Jeeli nie, to nowa warto przypisywana jest polu FMaxLength, jednoczenie do okna kontrolki wysyany jest komunikat EM_LIMITTEXT, okrelajcy maksymaln dugo wprowadzanego tekstu. Ta ostatnia czynno wykonywana jest przy okazji, poza tym niejako w ukryciu, zaliczana jest wic do kategorii tzw. efektw ubocznych (side effects). Zwrmy uwag, e centraln postaci przedstawionego scenariusza jest tak naprawd pole FMaxLength to ono przechowuje biec warto maksymalnej dugoci. Pole to nie jest jednak dostpne dla uytkownika (vide klauzula protected), a do jego obrbki suy waciwo MaxLength tylko ona jest bezporednio widoczna dla uytkownika, o polu FMaxLength moe on nie mie nawet pojcia. Pozwala to w dowolny sposb przeorganizowa szczegy implementacyjne obiektu jeeli tylko nie zmieni si definicja samej waciwoci, uytkownik niczego nie zauway.

Waciwociami zajmiemy si dokadniej w rozdziaach 11. i 13. niniejszego tomu ksiki.

395

Typy waciwoci
Poniewa kada waciwo obiektu reprezentuje warto konkretnego typu, wic stosuj si do niej te wszystkie zasady Object Pascala, ktre odnosz si do typw danych w oglnoci. Poniewa kada waciwo komponentu edytowalna jest poprzez inspektor obiektw, wic jest oczywiste, e rodzaj waciwoci okrela sposb jej edycji. Poszczeglne rodzaje waciwoci zebrane s w tabeli 10.2., a obszerne ich omwienie znajduje si w systemie pomocy Delphi pod hasem properties. Tabela 10.2. Typy waciwoci komponentu Typ waciwoci Sposb obsugi waciwoci przez inspektor obiektw Prosta Wyliczeniowa Warto waciwoci pojawia si w okienku i moe by edytowana przez uytkownika. Waciwoci wyliczeniowe (enumerated), wcznie z typem Boolean, pojawiaj si jako rozwijana lista, zawierajca poszczeglne elementy zdefiniowane w kodzie rdowym. Uytkownik moe te cyklicznie wdrowa po zestawie wartoci bez rozwijania listy (za pomoc dwukrotnego klikania). Waciwo zbiorowa (set) jest podzbiorem predefiniowanego zbioru elementw. Dwukrotne kliknicie spowoduje rozwinicie listy wszystkich predefiniowanych elementw, a ich bieca przynaleno (nieprzynaleno) do waciwoci oznaczana jest sowem True (False). Waciwo, ktra sama jest obiektem, z reguy posiada swj wasny edytor waciwoci. Jeeli jednak posiada (jako obiekt) waciwoci publikowane (published), to pojawi si one w edytorze macierzystym. Klasy obiektw bdcych waciwociami innych obiektw musz by typu pochodnego w stosunku do klasy TPersistent. Waciwoci tablicowe (array) musz posiada swj wasny edytor inspektor obiektw nie posiada standardowych narzdzi do edycji waciwoci tablicowych.

Zbiorowa

Obiektowa

Tablicowa

Metody
Jako e komponenty s obiektami, posiadaj swe metody. Obszerny opis metod obiektw znajduje si w rozdziale 2., nie bdziemy go wic tutaj powtarza.

Zdarzenia
Zdarzenia, rozumiane w kategoriach Delphi, reprezentuj rzeczywiste zdarzenia zachodzce w systemie: kliknicie mysz, nacinicie klawisza itp. Okrelenie akcji, ktr obiekt ma wykona w odpowiedzi na konkretne zdarzenie, naley do programisty; miejscem do zaprogramowania tej akcji jest wyrniona procedura obsugi, zwana procedur zdarzeniow (event handler procedure).

Definiowanie obsugi zdarze w czasie projektowania


Gdy spojrzysz na stron zdarze komponentu TEdit (w inspektorze obiektw), zobaczysz tam szereg zdarze, m.in. OnChange, OnClick, OnDblClick itp. Z punktu widzenia projektanta komponentw zdarzenia s wskanikami do metod obiektu. Uytkownik, przyporzdkowujc zdarzeniu wykonywalny kod, definiuje w rzeczywistoci obsug tego zdarzenia. Dwukrotne kliknicie linii zdarzenia (w inspektorze obiektw) spowoduje jak wiadomo wywietlenie szkieletu stosownej procedury zdarzeniowej: tutaj wanie jest miejsce na obsug zdarzenia. Oto przykad tworzonego przez Delphi szkieletu procedury zdarzeniowej:

396

TForm1 = class(TForm) Button1 : TButton; procedure Button1Click(Sender: TObject); End; ... procedure TForm1.Button1Click(Sender: TObject); begin // Tutaj jest miejsce na zdefiniowanie obsugi kliknicia przycisku Button1 end;

Definiowanie obsugi zdarze w czasie wykonania programu


Fakt, e zdarzenia (w kategoriach Delphi) s wskanikami do metod obiektu, staje si widoczny w przypadku definiowania obsugi zdarze w czasie wykonania programu. W charakterze ilustracji zdefiniujemy wasn procedur obsugi zdarzenia OnClick dla komponentu TButton. Procedura ta bdzie metod klasy formularza zawierajcego przycisk:
TForm1 = class(TForm) Button1: TButton; ... private procedure MyOnclickEvent(Sender: TObject); // deklaracja metody ... // definicja metody procedure TForm1.MyOnclickEvent(Sender: TObject); begin // tu jest miejsce na twj kod } end;

Przypisanie metody do konkretnego zdarzenia odbywa si bdzie podczas tworzenia formularza:


procedure TForm1.Formcreate(Sender: TObject); begin Button1.OnClick := MyOnClickEvent; end;

Moliwe jest take zobojtnienie obiektu na dane zdarzenie naley wwczas przypisa odnonemu zdarzeniu pusty wskanik:
Button1.OnClick := NIL;

Fakt, e zdarzenia maj charakter wskanikw, kryje w sobie pewn puapk: ot rne rodzaje zdarze nios ze sob rnoraki zakres informacji, przez co kade zdarzenie pociga za sob specyficzn posta procedury obsugi daje si to wyranie odczu, gdy poeksperymentujemy troch z rnymi zdarzeniami w inspektorze obiektw. Na przykad, metoda przypisywana zdarzeniu zwizanemu z ruchem myszy powinna mie nastpujcy typ:
TMouseEvent= procedure ( Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer) of object;

Poniewa wskaniki do procedur zdarzeniowych przechowywane s w polach obiektu, nic nie stoi na przeszkodzie, by kontrolowa dostp do tych pl za pomoc waciwoci jak w poniszym przykadzie:
TControl = class (TComponent) Private FOnMouseDown : TMouseEvent; Protected Property OnMouseDown: TMouseEvent read FOnMouseDown write FOnMouseDown; Public End;

Zdarzeniami i ich obsug zajmiemy si dokadniej w rozdziale 11.

Strumieniowalno
Jedn z cech komponentw jest moliwo przechowywania ich w tzw. strumieniach, bdcych specjalnym rodzajem plikw dyskowych. Wasno ta nosi nazw strumieniowalnoci lub potokowoci (streamability). Pliki *.DFM, towarzyszce niemale kademu moduowi Delphi, to nic innego, jak wanie strumienie w formacie

397

RCDATA (pomijamy tu moliwo przechowywania formularzy w postaci tekstowej). Dla twrcw aplikacji

kocowych szczegy mechanizmu strumieniowania nie s z pewnoci zbyt interesujce, lecz projektant nowych komponentw musi zajrze za kulisy. Szczegy zwizane z przechowywaniem komponentw w strumieniach opisujemy w rozdziale 12.

Komponenty jako waciciele innych komponentw


Jedn z zalenoci, jakie istniej wrd komponentw aplikacji jest relacja wasnoci (ownership), polegajca na tym, i kady komponent w czasie swej destrukcji (zwalniania) zobowizany jest zwolni wszystkie komponenty zawaszczone (owned), czyli po prostu te, ktrych jest wacicielem. Dla kadego komponentu waciwoci wskazujc na komponent-waciciela jest Owner. Kady formularz jest (w opisanym sensie) wacicielem wszystkich umieszczonych na nim komponentw. Podczas rcznego (tj. na etapie projektowania) umieszczania na formularzu komponentu pobranego z palety, waciwo Owner tego komponentu zostaje automatycznie zainicjowana wskanikiem do formularza; podczas tworzenia obiektu w kodzie programu naley czynno t wykona samodzielnie cho notabene i tak dzieje si to automatycznie, bo wskanik do komponentu-waciciela jest parametrem konstruktora, na przykad:
MyButton := TButton.Create(Self); // Self oznacza tu formularz

Moliwe jest oczywicie tworzenie obiektw osieroconych bez waciciela; taki obiekt musi by jednak bezwarunkowo zwolniony w sposb jawny, zaleca si wic ujcie caoci scenariusza z jego udziaem w konstrukcj tryfinally, jak w poniszym przykadzie:
MyTable := TTable.Create(NIL); // pusty wskanik do waciciela try // operacje z udziaem MyTable finally MyTable.Free; end;

Mechanizm automatycznego zwalniania obiektw zawaszczonych (podczas wasnej destrukcji) wpisany jest organicznie w klas TComponent, co mona stwierdzi na podstawie analizy fragmentw jej destruktora Destroy():
destructor TComponent.Destroy; begin Destroying; end; procedure TComponent.Destroying; var I: Integer; begin if not (csDestroying in FComponentState) then // aby zapobiec zaptlonej rekursji begin Include(FComponentState, csDestroying); if FComponents <> nil then // tablica FComponents[] zawiera wskaniki do komponentw zawaszczonych for I := 0 to FComponents.Count - 1 do TComponent(FComponents[I]).Destroying; end; end;

Jak wida, pierwsz czynnoci wykonywan przez destruktor jest zwalnianie obiektw zawaszczonych, ktrych liczba i wskaniki ukrywaj si w tablicy FComponents. Zasada ta stosowana jest rekurencyjnie, zatem

398

w przypadku cyklicznych powiza wasnociowych pomidzy komponentami (zjawisko patologiczne, aczkolwiek moliwe) istnieje niebezpieczestwo nieskoczonej rekurencji. Aby tego niebezpieczestwa unikn, pole FComponentState zwalnianego komponentu wzbogacone zostaje o element csDestroying, ktrego obecno powoduje natychmiastowe zakoczenie metody Destroying() przy powtrnym wejciu do niej.

Komponenty rodzicielskie
Inn relacj pomidzy komponentami, niekiedy mylon z relacj wasnoci, jest relacja rodzicielstwa (parenthood). Obowizuje ona tylko w stosunku do kontrolek okienkowych wskazanie na okno rodzicielskie zawarte jest we waciwoci Parent klasy TWinControl (w CLX TWidgetControl). Wskazania na wszystkie komponenty potomne w stosunku do danego komponentu rodzicielskiego zawarte s we waciwoci Controls[] (klasy TWinControl lub TWidgetControl), za ich liczba we waciwoci ControlCount. Wskazanie na kontrolk rodzicielsk zawarte jest w jej waciwoci Parent. Jak komponent-waciciel odpowiedzialny by za zwalnianie swych komponentw zawaszczonych, tak komponent rodzicielski odpowiedzialny jest m.in. za przekazywanie komunikatw do swych kontrolek pochodnych (poniszy fragment odnosi si jedynie do VCL):

procedure TWinControl.Broadcast(var Message); var I: Integer; begin for I := 0 to ControlCount - 1 do begin Controls[I].WindowProc(TMessage(Message)); if TMessage(Message).Result <> 0 then Exit; end; end;

Naley zaznaczy, i relacje wasnoci i rodzicielstwa s zupenie rnymi relacjami chociaby w kontekcie rnego zestawu odpowiedzialnych za nie waciwoci. Komponent posiadajcy rodzica niekoniecznie musi mie waciciela (i vice versa), cakiem naturalne s te sytuacje, w ktrych rodzicielskim jest komponent inny ni komponent-waciciel.

Hierarchia komponentw wizualnych


Wspominalimy ju w rozdziale 2. i wida to wyranie na rysunkach 10.1 i 10.2 i klas macierzyst wszystkich klas jest w Object Pascalu TObject. Definiuje ona jedynie kilka podstawowych metod odzwierciedlajcych wsplne wszystkim klasom elementy zachowania si i cznoci z systemem operacyjnym. I tak, metody Create() i Destroy() dokonuj (odpowiednio) przydziau i zwalniania pamici dla egzemplarza obiektu, za kilka metod z kategorii RTTI (patrz rozdz. 2.) udostpnia informacj zwizan z klas konkretnego obiektu jej typem, nazw i genealogi.
Wskazwka

Zalecane jest uywanie metody Free() zamiast bezporedniego wywoywania destruktora Destroy(). Metoda Free() wywouje destruktor Destroy(), ale przed jego wywoaniem sprawdza, czy zawartoci zmiennej obiektowej nie jest przypadkiem NIL: procedure TObject.Free; begin if Self <> nil then Destroy; end;

399

Schodzc na coraz to nisze szczeble hierarchii dziedziczenia, napotykamy klasy coraz to bogatsze, o coraz wikszej specjalizacji. Dla twrcy nowego komponentu jedn z najwaniejszych decyzji projektowych jest wybr odpowiedniej klasy bazowej dla nowo tworzonego komponentu.

Klasa TPersistent
Klasa TPersistent jest klas abstrakcyjn, stanowic podstaw dla wszystkich klas zdolnych do przechowywania w strumieniach zawartoci swych obiektw. Wywodzi si bezporednio z klasy TObject i nie definiuje adnych waciwoci ani zdarze, za to kilka jej metod wartych jest osobnego komentarza; ich opis zawarty jest w tabeli 10.3.

Tabela 10.3. Najwaniejsze metody klasy TPersistent Metoda


Assign() AssignTo()

Znaczenie Dokonuje kopiowania zawartoci pomidzy dwoma obiektami tej samej klasy. Wykonuje czynno odwrotn do metody Assign() przepisuje do innego obiektu kopi swych wasnych danych. Powoduje to, e we wszystkich klasach biblioteki VCL metoda Assign() realizowana jest przez proste odwoanie do metody AssignTo(). Jej domyln treci jest wygenerowanie wyjtku EConvertError, tak wic wszystkie klasy chcce uywa metody Assign() powinny implementowa wasn wersj metody AssignTo() uczyniy to m.in. klasy TControl, TWinControl itp. Umoliwia zdefiniowanie sposobu, w jaki zapisywa si bd do strumienia dodatkowe lub niepublikowane waciwoci komponentu; jest to zazwyczaj wykorzystywane do obsugi danych niestandardowego typu, np. danych binarnych. Jeeli metoda DefineProperties() jest w danej klasie pusta (jak w klasie TPersistent), do strumienia zapisuj si wycznie waciwoci opublikowane (published).

DefineProperties()

Szczegy zwizane z przechowywaniem obiektw w strumieniach opisane s w rozdziale 22. (Zaawansowane techniki tworzenia komponentw) ksiki Delphi 4. Vademecum profesjonalisty.

Klasa TComponent
Klasa TComponent jest bezporedni pochodn klasy TPersistent. Wywodzi si z niej kady komponent Delphi, tote definiuje ona szereg waciwoci i metod, umoliwiajcych m.in. obsug komponentw przez inspektor obiektw oraz penienie roli waciciela w stosunku do innych komponentw (waciwo Owner).

Waciwoci klasy TComponent


Najwaniejsze z waciwoci klasy TComponent zebrane zostay w tabeli 10.4.

400

Tabela 10.4. Najwaniejsze waciwoci klasy TComponent Waciwo


Owner ComponentCount ComponentIndex Components ComponentState ComponentStyle

Znaczenie Zawiera wskazanie na komponent-waciciela. Zawiera liczb zawaszczonych komponentw. Okrela pozycj (liczc od zera) identyfikujc komponent w tablicy Components[] komponentu-waciciela. Stanowi tablic (indeksowan od zera) wskaza na zawaszczone komponenty. Zawiera informacj o biecym stanie komponentu szczegy w rozdziale 11. Reguluje pewne aspekty zachowania si obiektu. Przykadami dozwolonych wartoci s csInheritable i csCheckPropAvail, obydwie omwione w systemie pomocy Delphi. Zawiera unikatow komponentu. (w ramach formularza) nazw

Name Tag

Warto typu integer, nie majca adnego znaczenia dla Delphi, sposb jej wykorzystania ley cakowicie w gestii programisty. Poczwszy od Delphi 2, zarwno typ integer, jak i dowolny typ wskanikowy, s 32-bitowe, a wic pole to moe by wykorzystane w charakterze wskanika do innych obiektw. Waciwo zarezerwowana dla projektanta formularzy.

DesignInfo

Metody klasy TComponent


Niektre spord metod klasy TComponent su do wsppracy z projektantem formularzy, inne za wykorzystywane s na wewntrzne potrzeby komponentu. Konstruktor Create() odzwierciedla zdolno komponentu do posiadania waciciela wskazanie na niego jest jedynym parametrem wywoania:
constructor Create(AOwner: TComponent); virtual;

W przeciwiestwie do klasy TObject, konstruktor klasy TComponent jest konstruktorem wirtualnym, wic wszystkie klasy pochodne w stosunku do niej musz deklarowa swe konstruktory z klauzul override. Cho moliwe jest definiowanie innych konstruktorw, jedynie TComponent.Create() uywany jest do tworzenia obiektu w czasie projektowania, jak rwnie podczas odczytu obiektu ze strumienia (take w czasie wykonywania aplikacji). Destruktor Destroy() odpowiedzialny jest za zwolnienie pamici przydzielonej obiektowi, po uprzednim zwolnieniu ewentualnych obiektw zawaszczonych. Metoda FindComponent() suy do poszukiwania komponentu wrd komponentw zawaszczonych; kryterium poszukiwania stanowi nazwa (waciwo Name) poszukiwanego komponentu. Wielko liter w specyfikowanej nazwie nie ma znaczenia, za sama metoda nie jest rekursywna wyszukiwanie ogranicza si jedynie do komponentw bezporednio zawaszczonych (czyli do zawartoci tablicy FComponents). Wynikiem poszukiwania jest wskazanie na dany komponent:
function FindComponent(const AName: string): TComponent;

Aby np. wpisa tekst do okienka TEdit o nazwie Edit1, naley uy nastpujcej konstrukcji:
TEdit(FindComponent('Edit1')).Text := 'Cze!';

Powysza konstrukcja ma jednak dwie istotne wady. Po pierwsze, wrd zawaszczonych komponentw moe nie by komponentu o nazwie Edit1 funkcja FindComponent() zwrci wtedy warto NIL. Po drugie,

401

znaleziony komponent moe mie typ niezgodny z typem TEdit, co spowodowaoby trudne do przewidzenia rezultaty. Ponisza konstrukcja uwzgldnia obydwie te moliwoci:
MyEditor := FindComponent('Edit1'); If MyEditor <> NIL Then begin if MyEditor is TEdit Then TEdit(MyEditor).Text := 'Cze!'; end;

Metody HasParent() i GetParentComponent() dotycz relacji rodzicielstwa wrd komponentw. Pierwsza z nich informuje, czy dany komponent posiada komponent rodzicielski, druga natomiast zwraca jego wskazanie. Poniewa relacja rodzicielstwa ma sens dopiero dla domeny TWinControl, wic obydwie z wymienionych metod zwracaj (w klasie TComponent) informacj, z ktrej wynika jednoznacznie brak komponentu rodzicielskiego:
function TComponent.HasParent: Boolean; begin Result := False; end; function TComponent.GetParentComponent: TComponent; begin Result := nil; end;

Metoda InsertComponent() powoduje doczenie kolejnej pozycji do tablicy FComponents (bdcej list komponentw zawaszczonych); nowy komponent doczony zostaje na koniec tablicy. Konstruktor Create() automatycznie wywouje t metod w stosunku do wskazanego komponentu-waciciela:
constructor TComponent.Create(AOwner: TComponent); begin if AOwner <> nil then AOwner.InsertComponent(Self); end;

Metoda RemoveComponent() powoduje usunicie wskazanego komponentu z listy komponentw zawaszczonych. Destruktor Destroy() wywouje t metod automatycznie, usuwajc wskazanie na zwalniany obiekt z listy FComponents komponentu macierzystego:
destructor TComponent.Destroy; begin if FOwner <> nil then FOwner.RemoveComponent(Self); end;

Klasa TControl
Klasa ta reprezentuje komponenty wizualne, bdce czci interfejsu uytkownika i nazywane potocznie kontrolkami (controls). Kady obiekt klasy TControl, jako komponent wizualny, posiada szereg waciwoci i komponentw natury geometrycznej Top, Left, Width, Height oraz ClientRect, ClientWidth, ClientHeight. Kilka innych waciwoci odpowiedzialnych jest za wygld i dostpno kontrolki: waciwo Visible okrela, czy kontrolka jest aktualnie widoczna na ekranie, natomiast waciwo Enabled okrela jej zdolno do reagowania na zdarzenia, pochodzce m.in. od myszy i klawiatury. Szczegy zewntrznego wygldu kontrolki okrelaj m.in. waciwoci Color, Font, Caption oraz Text. Kontrolki zdolne s do reagowania na wiele standardowych zdarze, midzy innymi OnClick, OnDblClick, OnMouseDown, OnMouseMove i OnMouseUp, jak rwnie na zdarzenia zwizane z przeciganiem OnDragOver, OnDragDrop i OnEndDrag. Klasa TControl przedefiniowuje ponadto metody HasParent() i GetParentComponent():
function TControl.HasParent: Boolean; begin Result := FParent <> nil; end; function TControl.GetParentComponent: TComponent;

402

begin Result := Parent; end;

i cho posiada ona waciwo Parent co oznacza, i moe by komponentem potomnym jednak nie definiuje jeszcze waciwoci Controls[], nie moe wic sama peni funkcji komponentu rodzicielskiego mog j peni dopiero kontrolki okienkowe (wywodzce si z klasy TWinControl lub TWidgetControl). Klasa TControl nie jest jednak zbytnio uyteczna sama w sobie, stanowi raczej punkt wyjcia do definiowania klas pochodnych, ktrymi na pierwszym poziomie s klasy TWinControl (TWidgetControl) i TGraphicControl.

Klasy TWinControl i TWidgetControl


TWinControl i TWidgetControl s klasami bazowymi dla kontrolek okienkowych, ktre s otoczkami standardowych kontrolek Windows (VCL) lub obiektw ekranowych Qt (CLX), stanowicych aktywne

elementy interfejsu uytkownika kontrolki edycyjne, listy combo, przyciski itp. W miejsce niskopoziomowych funkcji API, bdcych podstawowymi rodkami operowania kontrolkami w tradycyjnym programowaniu, Delphi udostpnia rwnowane waciwoci, metody i zdarzenia. Kada kontrolka okienkowa charakteryzuje si trzema nastpujcymi cechami: moe stawa si aktywna, posiada uchwyt (w CLX gadet), czyli cznik z mechanizmami niskopoziomowymi, jak rwnie moe peni funkcj kontrolki rodzicielskiej w stosunku do kontrolek potomnych. Jako e TWinControl i TWidgetControl stanowi klasy bazowe dla definiowania bardzo wielu komponentw Delphi, konieczne jest zapoznanie si z ich waciwociami, metodami i zdarzeniami.

Waciwoci klas TWinControl i TWidgetControl


Klasy TWinControl i TWidgetControl definiuj kilka waciwoci zwizanych z otrzymywaniem i utrat skupienia oraz z wygldem zewntrznym kontrolki. Dla uproszczenia bdziemy w dalszym cigu opisu przywoywa jedynie klas TWinControl; naley jednak pamita, i opis ten w rwnym stopniu odnosi si do jej odpowiednika w CLX TWidgetControl. Waciwo Brush okrela wzr i kolor ta wypeniajcego wntrze rysowanych ksztatw; przykady jej wykorzystania znajduj si w rozdziale 8. ksiki Delphi 4. Vademecum profesjonalisty (Wykorzystanie narzdzi graficznych Delphi i operowanie czcionkami).

Waciwo ControlCount okrela liczb kontrolek potomnych (child), za ich zestaw reprezentowany jest przez tablicow waciwo Controls. Ustawienie na True waciwoci Ctl3D nadaje kontrolce wygld symulujcy trjwymiarowo. Waciwo Handle stanowi uchwyt (lub wskanik) do odnonego obiektu ekranowego, dla ktrego kontrolka jest otoczk. Aktualny kontekst sytuacyjny dla systemu pomocy Delphi okrelany jest przez waciwo HelpContext. O widocznoci kontrolki na ekranie informuje waciwo Showing. Ma ona sens jedynie w czasie wykonania i nie wolno jej zmienia. Wreszcie, waciwo TabStop okrela, czy kontrolka naley do ptli cyklicznego przeczania aktywnoci za pomoc klawisza Tab (tzw. cyklu Tab). Numer kontrolki w tym cyklu jest okrelony przez jej waciwo TabOrder.

Metody klasy TWinControl


Metody kontrolek okienkowych zwizane s z tworzeniem okna, stanem jego aktywnoci, obsug zdarze i pozycjonowaniem. Obfito tych metod nie pozwala na wyczerpujce opisanie ich w tym miejscu, lecz na szczcie s one wystarczajco dokadnie opisane w systemie pomocy Delphi. Metody zwizane z tworzeniem, odtwarzaniem oraz usuwaniem okna przeznaczone s gwnie dla konstruktora komponentw pochodnych i opisane s wyczerpujco w rozdziale 11.; mowa tu m.in. o metodach

403

CreateParams(), CreateWnd(), CreateWindowHandle(), DestroyWnd(), DestroyWindowHandle() i ReCreateWnd().

Metody zwizane z aktywnoci okna, jego pozycjonowaniem i wyrwnywaniem (alignment) to m.in. CanFocus(), Focused(), AlignControls(), EnableAlign(), DisableAlign() i Realign().

Zdarzenia klasy TWinControl


Zdarzenia dotyczce kontrolek okienkowych zwizane s z klawiatur, mysz oraz przenoszeniem aktywnoci (focus) midzy oknami. Chodzi tutaj m.in. o zdarzenia OnKeyDown, OnKeyPress i OnKeyUp oraz OnEnter i OnExit. Wszystkie one opisane s obszernie w systemie pomocy Delphi.

Klasa TGraphicControl
Jest to klasa nieaktywnych kontrolek graficznych. S one widoczne na ekranie, lecz, w przeciwiestwie do komponentw okienkowych, nie mog przyjmowa skupienia aktywne, nie mog peni roli rodzicielskiej i nie posiadaj przyporzdkowanego uchwytu. Stanowi w zwizku z tym mniejsze obcienie dla zasobw systemowych, szybciej te wywietlaj si na ekranie. Komponenty graficzne mog co prawda reagowa na zdarzenia pochodzce od myszy (np. kliknicie), lecz jest to reakcja porednia: zdarzenia te odbierane s przez rodzicielskie kontrolki okienkowe i dopiero w dalszej kolejnoci przekazywane do potomnych kontrolek graficznych. Poniewa kontrolki graficzne zaliczaj si do komponentw wizualnych, posiadaj ptno reprezentowane przez waciwo Canvas. Za odwieenie (na danie) wygldu kontrolki na ekranie odpowiedzialna jest jej metoda Paint().

Klasy kategorii TCustom


Gdy studiuje si zawarto biblioteki VCL, nietrudno zauway pewn szczegln kategori klas, ktrych nazwy rozpoczynaj si od TCustom. Klasy te peni wycznie funkcj klas bazowych na potrzeby tworzenia nowych komponentw, a ich istnienie wynika z pewnej charakterystycznej cechy dziedziczenia waciwoci, o ktrej pisalimy ju w rozdziale 2.: ot dziedziczenie waciwoci nie moe osabi stopnia jej widocznoci, w szczeglnoci nie da si anulowa opublikowania waciwoci. Wic aby twrca nowego komponentu mia pen swobod w wyborze zestawu waciwoci publikowanych (published), klasy bazowe typu TCustom nie publikuj adnej ze swych waciwoci. W bibliotece VCL istnieje kilka klas opisywanej kategorii, wywodzcych si bezporednio z TWinControl TCustomControl, TCustomTabControl, TCustomListControl, TCustomHeaderControl, TCustomStatusBar, TCustomEdit, TCustomTreeView, TCustomUpDown, TCustomHotKey i TCustomStaticText. S one klasami bazowymi dla rnego rodzaju kontrolek o rnorodnym wygldzie i funkcjonalnoci. Wspominalimy ju wielokrotnie, i kontrolki stanowi otoczki odpowiednich obiektw ekranowych. Dla niektrych kontrolek cao zwizanych z nimi operacji graficznych wykonywana jest przez te wanie obiekty, niektre kontrolki wprowadzaj jednak specyficzne dla siebie elementy wygldu i o utrzymanie tego wygldu musz zatroszczy si same; temu wanie celowi suy ich ptno, bdce obiektem klasy TCanvas i stanowice waciwo o nazwie Canvas. Do takiej grupy kontrolek nale wszystkie kontrolki graficzne (klasa TGraphicControl i klasy pochodne) oraz cz kontrolek okienkowych spord klas wymienionych w poprzednim akapicie do grupy tej nale TCustomControl, TCustomTabControl, TCustomHeaderControl, TCustomStatusBar i TCustomTreeView. W bibliotece CLX klasami pochodnymi TWidgetControl, posiadajcymi ptno, s: TCustomControl, TCustomHeaderControl, TCustomComboBox; nie posiadaj natomiast ptna klasy TFrameControl, TSubGridForm, TCustomEdit, TButtonControl, TScrollBar, TTrackBar i TCustomSpinEdit.

404

Pozostae klasy
Istniej w bibliotece VCL klasy, ktre same nie s komponentami, lecz wykorzystywane s w charakterze waciwoci komponentw. Jak ju wspominalimy, obiektowe waciwoci komponentu s zawsze pochodnymi klasy TPersistent, cho niekoniecznie komponentami. Przykadami klas tego rodzaju s TStrings, TCanvas i TCollection.

Klasy TStrings i TStringList


Abstrakcyjna klasa TStrings posiada metody i waciwoci do manipulowania listami acuchw. Listy takie stanowi skadowe wielu klas na przykad TListBox. Klasa TStrings sama w sobie nie implementuje adnych mechanizmw zarzdzania pamici (jest to zadanie klasy macierzystej, posiadajcej waciwo typu TStrings), udostpnia natomiast wiele uytecznych metod uatwiajcych zarzdzanie acuchami znakowymi i stanowicych wygodne zamienniki rwnowanych funkcji API. Wiele z tych metod to jednak metody abstrakcyjne, przeznaczone do implementacji w klasach pochodnych. Ich znaczenie opisane jest w tabeli 10.5.

Tabela 10.5. Metody klasy TStrings Deklaracja metody


Add(const S:String): Integer; AddObject(const S:String; AObject:TObject): Integer; AddStrings(Strings: TStrings); Clear; Delete(Index: Integer); Exchange(Index1, Index2:Integer); IndexOf(const S:String):integer; Insert(Index:Integer; const S: String); Move(CurIndex, NewIndex: Integer); LoadFromFile(const FileName:string); SaveToFile(const FileName:string);

Opis Dodaje acuch S do listy i zwraca jego pozycj na licie. Docza do listy par acuch obiekt i zwraca jej pozycj na licie. Docza zawarto podanej listy na koniec listy biecej. Usuwa z listy wszystkie pozycje. Usuwa wskazan pozycj z listy. Zamienia miejscami wskazane pozycje listy. Zwraca pozycj podanego acucha. Wstawia podany acuch na wskazan pozycj. Przemieszcza acuch znajdujcy si na pozycji CurIndex na pozycj NewIndex. Umieszcza na licie kolejne linie pliku tekstowego. Zapisuje kolejne pozycje listy w pliku tekstowym jako jego kolejne linie.

Powstaje zatem pytanie, w jaki sposb funkcjonuj klasy posiadajce waciwoci i pola typu TStrings, skoro jak powiedziano przed chwil klasa ta nie implementuje niemal adnej funkcjonalnoci? Odpowied jest bardzo prosta i wynika z istoty polimorfizmu. Ot, deklarowanym typem waciwoci jest co prawda TStrings, lecz przypisany do niej obiekt jest ju egzemplarzem klasy pochodnej. Najlepiej wyjani to na przykadzie oto fragment definicji jednej z klas wykorzystujcej list acuchw:
TCustomListBox = class() private FItems : TStrings

Klasa ta jak kada klasa Object Pascala posiada konstruktor Create(). Przyjrzyjmy si uwanie jednemu z jego fragmentw:
constructor TCustomListBox.Create(AOwner: TComponent);

405

begin FItems := TListBoxStrings.Create; end;

Po wykonaniu konstruktora waciwo zadeklarowana jako TStrings zawiera wic wskazanie na obiekt klasy TListBoxStrings, zadeklarowanej nastpujco (w module StdCtrls.pas): Wydruk 10.1. Deklaracja klasy TListBoxStrings
TListBoxStrings = class(TStrings) private ListBox: TCustomListBox; protected procedure Put(Index: Integer; const S: string); override; function Get(Index: Integer): string; override; function GetCount: Integer; override; function GetObject(Index: Integer): TObject; override; procedure PutObject(Index: Integer; AObject: TObject); override; procedure SetUpdateState(Updating: Boolean); override; public function Add(const S: string): Integer; override; procedure Clear; override; procedure Delete(Index: Integer); override; procedure Exchange(Index1, Index2: Integer); override; function IndexOf(const S: string): Integer; override; procedure Insert(Index: Integer; const S: string); override; procedure Move(CurIndex, NewIndex: Integer); override; end;

Jak wida, klasa TListBoxStrings przedefiniowuje wszystkie abstrakcyjne metody swej klasy bazowej TStrings. Autorzy nowych komponentw mog oczywicie definiowa wasne klasy wywodzce si z TStrings, wane jednak, by implementowali je waciwie. W przypadku wtpliwoci mog si wzorowa na gotowych rozwizaniach zawartych w kodzie rdowym bibliotek VCL i CLX.
Notatka

Zaprezentowane zjawisko jest przykadem polimorfizmu dziaanie okrelonej metody zalene jest od klasy konkretnego obiektu i nie da si okreli jedynie na podstawie deklaracji zmiennej, bdcej wskazaniem na ten egzemplarz.

Uyteczn klas, uatwiajc operowanie samodzielnymi listami acuchw, jest TStringList; wywodzi si ona oczywicie z klasy TStrings, lecz implementuje wszystkie swe metody. Oto przykad utworzenia obiektu tej klasy, zawierajcego cztery acuchy:
Var MyStringList: TStringList; Begin MyStringList := TStringList.Create; ..... MyStringList.Add('Charles M. Widor'); MyStringList.Add('Louis Vierne'); MyStringList.Add('Maurice Durufle'); MyStringList.Add('Jehan Alain');

Nic oczywicie nie stoi na przeszkodzie, by tak utworzon list potraktowa jako waciwo jakiego komponentu, oryginalnie zadeklarowan jako TStrings na przykad waciwo Lines klasy TMemo:
Var Memo1: TMemo; ...... Memo1.Lines := MyStringList;

Powyszy przykad daje okazj do poruszenia jeszcze jednego problemu, wynikajcego z nieistnienia (w Object Pascalu) statycznych obiektw i moliwoci reprezentowania obiektw jedynie przez wskaniki. Ot powysza

406

instrukcja przypisuje waciwoci Lines wskazanie na konkretny obiekt, bez tworzenia jego kopii. Komponent Memo1 jest jednak wacicielem komponentu MyStringList i usunie go automatycznie podczas swej destrukcji. Aby unikn tej niespodzianki, naley do waciwoci Lines przypisa wskazanie nie na oryginalny obiekt, lecz jego kopi, ktr sporzdza si za pomoc metody Assign():
Memo1.Lines.Assign(MyStringList);

Rozwizanie takie jest konieczne take wwczas, gdy ta sama lista acuchw przypisywana jest do kilku waciwoci, w tym samym komponencie lub w rnych komponentach, jak poniej:
Memo1.Lines := MyStringList; ListBox1.Items := MyStringList;

Po takim przypisaniu zwolnienie obiektu Memo1 spowoduje automatyczne zniszczenie obiektu MyStringList, wykorzystywanego wci przez ListBox1. Zamiast wic posugiwa si oryginalnym obiektem, naley raczej utworzy jego oddzielne kopie:
Memo1.Lines.Assign(MyStringList); ListBox1.Items.Assign(MyStringList);

Klasa TCanvas
Jak wczeniej wspomnielimy, wiele kontrolek posiada waciwo Canvas, wskazujc na obiekt klasy TCanvas zwany ptnem i realizujcy wszelkie operacje graficzne odnoszce si do tzw. kontekstu urzdzenia (w bibliotece CLX do obiektu paintera odpowiedzialnego za wywietlanie grafiki). Piszemy o tym dokadniej w rozdziale 8. (Wykorzystanie narzdzi graficznych Delphi i operowanie czcionkami) ksiki Delphi 4. Vademecum profesjonalisty.

Wykorzystanie informacji RTTI


Zgodnie z informacjami podanymi w rozdziale 2., w czasie wykonania programu stworzonego w Delphi, dostpna jest spora dawka informacji o wykorzystywanych przez niego klasach. Mechanizm ten, zwany w oryginale RTTI (Runtime Type Information) wprowadzono w Delphi 1, w Delphi 2 jego posta ulega znaczcym zmianom, w zwizku z przejciem na platform 32-bitow. Natomiast we wszystkich wersjach Turbo Pascala informacja o wykorzystywanych obiektach dostpna bya jedynie w czasie kompilacji (jeli nie liczy funkcji TypeOf(), zwracajcej adres tablicy VMT podanego obiektu lub klasy). Informacja RTTI manifestuje sw obecno na co najmniej dwa sposoby. Przede wszystkim jest ona niezbdnym elementem umoliwiajcym obsug (skompilowanych!) komponentw przez inspektor obiektw elementem co prawda nie jedynym, lecz niezbdnym. Po drugie, znajduje swe odzwierciedlenie nie tylko w funkcjach Win32 API, ale nawet w elementach syntaktycznych jzyka Object Pascal (operatory is i as); przedmiotem rozwaa niniejszego podrozdziau bdzie wanie w drugi jej aspekt. W charakterze przykadu rozpatrzmy nastpujce zagadnienie: naley uniemoliwi modyfikacj wszystkich pl edycyjnych formularza poprzez udostpnienie ich jedynie do odczytu; przez okrelenie pola edycyjne rozumiemy tutaj komponenty klasy TEdit lub pochodnej. W Turbo Pascalu stwierdzenie zgodnoci dwch typw obiektowych na podstawie wskanika do obiektu jest niewykonalne instrukcja w rodzaju
if TypeOf(MyObj) = TypeOf(TEdit)

nie wykryje przypadku, gdy obiekt MyObj jest obiektem klasy pochodnej w stosunku do TEdit. W Delphi 1 caa sprawa uprocia si radykalnie za spraw magicznego operatora is:

for i := 0 to ComponentCount 1 do begin if Components[i] is TEdit Then TEdit(Components[i]).ReadOnly := TRUE; end;

407

Drugim operatorem Object Pascala, wykorzystujcym informacj RTTI, jest operator as, realizujcy inteligentne rzutowanie typw. Ponisza procedura zdarzeniowa
procedure TForm1.ControlOnClickEvent(Sender: TObject); begin TControl(Sender).Enabled := FALSE; end;

bdzie funkcjonowa poprawnie jedynie wtedy, gdy obiekt wskazywany przez Sender jest obiektem klasy TControl lub pochodnej, poniewa rzutowanie typw odbywa si na si i wymusza interpretacj obiektu Sender na mod klasy TControl, bez sprawdzenia jego faktycznego typu. Moe to prowadzi do nieprzewidywalnych zachowa programu. Nie ma tej wady ponisza wersja procedury:
procedure TForm1.ControlOnClickEvent(Sender: TObject); var SenderControl: TControl; begin SenderControl := Sender as TControl; if SenderControl <> NIL then SenderControl.Enabled := False; end;

A oto inny ciekawy przykad wykorzystania RTTI. Wrd rozmaitych komponentw zwizanych z bazami danych, wyrnia si tzw. komponenty wraliwe na dane (data-aware components) zawieraj one waciwo o nazwie (przewanie) DataSource typu TDataSource, stanowic wskazanie na obiekt reprezentujcy (mwic skrtowo) zawarto bazy danych. Chcielibymy odnale wszystkie tego rodzaju komponenty na formularzu i wykona dla kadego z nich jak okrelon akcj. Przedstawiona przed chwil metoda testowania przynalenoci obiektu do okrelonej klasy nie na wiele si tu przyda, z prostej przyczyny nie istnieje klasa reprezentujca wszystkie komponenty wraliwe na dane i nic ponadto. Teoretycznie, mona by uwzgldni cay zbir znanych klas tego rodzaju, rozwizanie takie jest jednak skrajnie niepraktyczne. RTTI oferuje rozwizanie tego problemu w jego naturalnej postaci, mianowicie dla danego komponentu mona sprawdzi, czy posiada on waciwo klasy TDataSource lub pochodnej. Wymaga to jednak operowania strukturami RTTI w sposb bezporedni zademonstrujemy to w dalszym cigu niniejszego rozdziau.

Informacja o genealogii i zestawie waciwoci klasy


Informacja RTTI dostpna jest dla kadej klasy uytej w programie. Zawarta jest w pamici procesu i zorganizowana w struktury zdefiniowane w module TypInfo.Pas; ze struktur tych korzystaj zarwno mechanizmy IDE, jak i podprogramy biblioteki RTL. Dua cz informacji RTTI dostpna jest take za porednictwem metod klasy TObject, z ktrych najwaniejsze przedstawia tabela 10.6.

Tabela 10.6. Niektre metody klasy TObject udostpniajce informacj z kategorii RTTI Metoda
ClassName() ClassType() InheritsFrom() ClassParent() InstanceSize() ClassInfo()

Typ wyniku
String TClass Boolean TClass Word Pointer

Znaczenie wyniku Nazwa klasy obiektu Klasa obiektu (jako warto metaklasy) Test przynalenoci obiektu do okrelonej klasy (lub jej klas pochodnych) Klasa macierzysta w stosunku do klasy obiektu (jako warto metaklasy) Rozmiar (w bajtach) egzemplarza obiektu Wskanik do struktury RTTI w pamici procesu

Jak mona si domyla, szczeglnie interesujca jest ostatnia z wymienionych funkcji, gdy zawiera wskanik do tej porcji RTTI, ktra dotyczy przedmiotowej klasy:
class function ClassInfo: Pointer;

408

Struktura, ktrej wskanik jest zwracany przez omawian funkcj, zdefiniowana jest w module TypInfo.Pas i ma nastpujc posta:
PTypeInfo = ^TTypeInfo; TTypeInfo = record Kind: TTypeKind; Name: ShortString; {TypeData: TTypeData} end;

Wykomentowane pole TypeData odzwierciedla reprezentacj informacji o typie, ktrej rodzaj zawiera pole Kind:
type TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat, tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString, tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray);

Wrd niektrych z wymienionych typw tkxxx rozrnia si dodatkowo poszczeglne podtypy, na przykad:
type TTypeKinds = set of TTypeKind; TOrdType = (otSByte, otUByte, otSWord, otUWord, otSLong, otULong); TFloatType = (ftSingle, ftDouble, ftExtended, ftComp, ftCurr); TMethodKind = (mkProcedure, mkFunction, mkConstructor, mkDestructor, mkClassProcedure, mkClassFunction, { Obsolete } mkSafeProcedure, mkSafeFunction);

Kompletna definicja struktury TTypeData, sucej tak naprawd do mapowania udostpnianego obszaru, zostaa przedstawiona na wydruku 10.2.

Wydruk 10.2. Struktura TTypeData


PTypeData = ^TTypeData; TTypeData = packed record case TTypeKind of tkUnknown, tkLString, tkWString, tkVariant: (); tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: ( OrdType: TOrdType; case TTypeKind of tkInteger, tkChar, tkEnumeration, tkWChar: ( MinValue: Longint; MaxValue: Longint; case TTypeKind of tkInteger, tkChar, tkWChar: (); tkEnumeration: ( BaseType: PPTypeInfo; NameList: ShortStringBase; EnumUnitName: ShortStringBase)); tkSet: ( CompType: PPTypeInfo)); tkFloat: ( FloatType: TFloatType); tkString: ( MaxLength: Byte); tkClass: ( ClassType: TClass; ParentInfo: PPTypeInfo; PropCount: SmallInt; UnitName: ShortStringBase; {PropData: TPropData}); tkMethod: ( MethodKind: TMethodKind; ParamCount: Byte; ParamList: array[0..1023] of Char {ParamList: array[1..ParamCount] of record Flags: TParamFlags; ParamName: ShortString; TypeName: ShortString;

409

end; ResultType: ShortString}); tkInterface: ( IntfParent : PPTypeInfo; { ancestor } IntfFlags : TIntfFlagsBase; Guid : TGUID; IntfUnit : ShortStringBase; {PropData: TPropData}); tkInt64: ( MinInt64Value, MaxInt64Value: Int64); tkDynArray: ( elSize: Longint; elType: PPTypeInfo; // nil jeli elemnt tablicy nie jest obiektem // o kontrolowanym czasie ycia varType: Integer; // rwnowanik varType w automatyzacji OLE elType2: PPTypeInfo; // niezalene od kontroli czasu ycia DynUnitName: ShortStringBase); end;

Wywoujc metod ClassInfo(), otrzymujemy wskanik do struktury TTypeInfo zwizanej z dan klas. Jej pierwszy bajt (Kind) identyfikuje rodzaj informacji, nastpne 256 bajtw (Name) to acuch zawierajcy nazw typu, a kolejne bajty to nic innego, jak obszar mapowany przez struktur TTypeData wybr jej waciwego wariantu uzaleniony jest od zawartoci pola Kind.

Ostrzeenie

Definicja struktury TTypeData nie zostaa oficjalnie udokumentowana moliwe jest wic, i w przyszych wersjach Delphi bdzie miaa inn posta.

Przykadowe zastosowanie RTTI do uzyskania informacji o klasie ilustruje projekt znajdujcy si na zaczonym krku CD-ROM pod nazw ClassInfo.dpr; poniej przedstawiamy kod jego formularza gwnego. Wydruk 10.3. Kod formularza gwnego projektu ClassInfo.Dpr
unit MainFrm; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, DBClient, MidasCon, MConnect; type TMainForm = class(TForm) pnlTop: TPanel; pnlLeft: TPanel; lbBaseClassInfo: TListBox; spSplit: TSplitter; lblBaseClassInfo: TLabel; pnlRight: TPanel; lblClassProperties: TLabel; lbPropList: TListBox; lbSampClasses: TListBox; procedure FormCreate(Sender: TObject); procedure lbSampClassesClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var MainForm: TMainForm; implementation uses TypInfo; {$R *.DFM}

410

function CreateAClass(const AClassName: string): TObject; { Niniejsza metoda ilustruje sposb tworzenia obiektu danej klasy na podstawie NAZWY tej klasy. Konieczne jest, by klasa ta bya zarejestrowana za pomoc funkcji RegisterClasses() - jak dzieje si to w czci initialization niniejszego moduu } var C : TFormClass; SomeObject: TObject; begin C := TFormClass(FindClass(AClassName)); SomeObject := C.Create(nil); Result := SomeObject; end;

procedure GetBaseClassInfo(AClass: TObject; AStrings: TStrings); { Niniejsza metoda odczytuje niektre podstawowe informacje na temat danego obiektu i dodaje je do wskazanej listy acuchw } var ClassTypeInfo: PTypeInfo; ClassTypeData: PTypeData; EnumName: String; begin ClassTypeInfo := AClass.ClassInfo; ClassTypeData := GetTypeData(ClassTypeInfo); with AStrings do begin Add(Format('Nazwa klasy: %s', [ClassTypeInfo.Name])); EnumName := GetEnumName(TypeInfo(TTypeKind), Integer(ClassTypeInfo.Kind)); Add(Format('Typ: %s', [EnumName])); Add(Format('Rozmiar: %d', [AClass.InstanceSize])); Add(Format('Zdefiniowana w: %s.pas', [ClassTypeData.UnitName])); Add(Format('Liczba waciwoci: %d',[ClassTypeData.PropCount])); end; end; procedure GetClassAncestry(AClass: TObject; AStrings: TStrings); { Niniejsza metoda pobiera nazwy klas-przodkw klasy danego obiektu i dodaje je do wskazanej listy acuchw } var AncestorClass: TClass; begin AncestorClass := AClass.ClassParent; { Iteracja wzwy acucha genealogicznego a do osignicia najwyszej klasy bazowej } AStrings.Add('Genealogia klasy'); while AncestorClass <> nil do begin AStrings.Add(Format(' %s',[AncestorClass.ClassName])); AncestorClass := AncestorClass.ClassParent; end; end;

procedure GetClassProperties(AClass: TObject; AStrings: TStrings); { Niniejsza metoda odczytuje nazw i typ kadej waciwoci wskazanego obiektu i dodaje te informacje do wskazanej listy acuchw } var PropList: PPropList; ClassTypeInfo: PTypeInfo; ClassTypeData: PTypeData; i: integer;

411

NumProps: Integer; begin ClassTypeInfo := AClass.ClassInfo; ClassTypeData := GetTypeData(ClassTypeInfo); if ClassTypeData.PropCount <> 0 then begin // przydziel pami niezbdn do przechowania wskanikw do wszystkich // rekordw TpropInfo reprezentujcych waciwoci obiektu GetMem(PropList, SizeOf(PPropInfo) * ClassTypeData.PropCount); try // wypenij tablic PropList wskanikami do struktur TPropInfo GetPropInfos(AClass.ClassInfo, PropList); for i := 0 to ClassTypeData.PropCount - 1 do // odrzu wszystkie waciwoci zdarzeniowe (czyli bdce wskanikami // do metod) if not (PropList[i]^.PropType^.Kind = tkMethod) then AStrings.Add(Format('%s: %s', [PropList[i]^.Name, PropList[i]^.PropType^.Name])); // Teraz uwzgldnij TYLKO waciwoci zdarzeniowe NumProps := GetPropList(AClass.ClassInfo, [tkMethod], PropList); if NumProps <> 0 then begin AStrings.Add(''); AStrings.Add('ZDARZENIA ================ '); AStrings.Add(''); end; // Wpisz dane o waciwociach zdarzeniowych do listy acuchw for i := 0 to NumProps - 1 do AStrings.Add(Format('%s: %s', [PropList[i]^.Name, PropList[i]^.PropType^.Name])); finally FreeMem(PropList, SizeOf(PPropInfo) * ClassTypeData.PropCount); end; end; end; procedure TMainForm.FormCreate(Sender: TObject); begin // Dla ilustracji wpisz nazwy kilku przykadowych klas lbSampClasses.Items.Add('TApplication'); lbSampClasses.Items.Add('TButton'); lbSampClasses.Items.Add('TForm'); lbSampClasses.Items.Add('TListBox'); lbSampClasses.Items.Add('TPaintBox'); lbSampClasses.Items.Add('TMidasConnection'); lbSampClasses.Items.Add('TFindDialog'); lbSampClasses.Items.Add('TOpenDialog'); lbSampClasses.Items.Add('TTimer'); lbSampClasses.Items.Add('TComponent'); lbSampClasses.Items.Add('TGraphicControl'); end; procedure TMainForm.lbSampClassesClick(Sender: TObject); var SomeComp: TObject; begin lbBaseClassInfo.Items.Clear; lbPropList.Items.Clear; // utwrz egzemplarz danej klasy SomeComp := CreateAClass(lbSampClasses.Items[lbSampClasses.ItemIndex]); try GetBaseClassInfo(SomeComp, lbBaseClassInfo.Items); GetClassAncestry(SomeComp, lbBaseClassInfo.Items); GetClassProperties(SomeComp, lbPropList.Items); finally SomeComp.Free; end; end;

412

initialization begin RegisterClasses([TApplication, TButton, TForm, TListBox, TPaintBox, TMidasConnection, TFindDialog, TOpenDialog, TTimer, TComponent, TGraphicControl]); end; end.

Formularz gwny projektu (rysunek 10.3) skada si z trzech list. Dolna lista (lbSampClasses) zawiera nazwy klas skadajcych si na projekt; po wybraniu okrelonej pozycji na lewej, grnej licie (lbBaseClassInfo), wywietlone zostaj podstawowe cechy wybranej klasy jej nazwa, rozmiar egzemplarza, genealogia, liczba waciwoci itp.; jak atwo zauway, pole Kind rekordu TTypeInfo zawiera warto tkClass. Same za nazwy i typy waciwoci wywietlane s w prawej licie (lbPropList).

Rysunek 10.3. Oglna informacja RTTI o klasie

Zaprezentowany kod formularza gwnego zawiera m.in. trzy funkcje pomocnicze, speniajce nastpujce zadania:
GetBaseClassInfo() wpisuje na list lbBaseClassInfo wybrane informacje o obiekcie: jego typ, rozmiar, nazw moduu rdowego i liczb waciwoci. GetClassAncestry() dodaje do listy lbBaseClassInfo genealogi klasy, poczwszy od jej klasy macierzystej, na klasie TObject koczc. GetClassProperties() wypenia list lpPropList nazwami i typami waciwoci.

W chwili, gdy uytkownik wybierze jedn z klas na licie lbSampClasses, funkcja CreateAClass() tworzy obiekt tej klasy:
function CreateAClass(const AClassName: string): TObject;

413

var C : TFormClass; SomeObject: TObject; begin C := TFormClass(FindClass(AClassName)); SomeObject := C.Create(nil); Result := SomeObject; end;

Co najmniej dwa elementy tej procedury warte s osobnego komentarza. Ot, wykorzystuje ona jedn z nowoci Delphi, nieobecn w Turbo Pascalu mianowicie tzw. typ referencyjny, zwany rwnie metaklas (por. jeden z przypisw do rozdziau 2.). Pod tym pojciem kryje si zbir wszystkich klas pochodnych w stosunku do danej klasy (wraz z t klas) w naszym przykadzie jest to zbir wszystkich klas zgodnych (w sensie operatora is) z klas TForm:
TFormClass = class of TForm;

Kada z klas jest (jako typ) wartoci typu referencyjnego; warto t (fizycznie stanowic wskazanie na tzw. punkt zerowy tablicy VMT) udostpnia funkcja FindClass() na podstawie nazwy odnonej klasy jednake pod warunkiem, i klasa ta zostaa zarejestrowana za pomoc funkcji RegisterClasses(). Rejestracja taka dokonywana jest w czci inicjacyjnej moduu gwnego.
initialization begin RegisterClasses([TApplication, TButton, TForm, TListBox, TPaintBox, TMidasConnection, TFindDialog, TOpenDialog, TTimer, TComponent, TGraphicControl]); end; end.

Zwracana przez funkcj FindClass() warto, reprezentujca typ klasy, przypisywana jest do zmiennej C; rzutowanie tej wartoci na typ TFormClass jest konieczne z tego wzgldu, e funkcja FindClass() zwraca wynik bdcy znacznie szersz metaklas:
function FindClass(const ClassName: string): TPersistentClass;

gdzie
TPersistentClass = class of TPersistent;

Dysponujc typem danej klasy jako wartoci typu referencyjnego, moemy wywoa na rzecz teje klasy konstruktor Create() w celu utworzenia jej obiektu:
SomeObject := C.Create(nil);

Klasa, stanowica podmiot wywoania konstruktora, nie jest tu dana w sposb bezporedni, lecz jako wyraenie typu referencyjnego. Utworzony egzemplarz obiektu jest nastpnie wykorzystywany przez trzy wymienione wyej funkcje, a po wykorzystaniu zwalniany:
var SomeComp: TObject; SomeComp := CreateAClass( lbSampClasses.Items[lbSampClasses.ItemIndex]); try GetBaseClassInfo(SomeComp, lbBaseClassInfo.Items); GetClassAncestry(SomeComp, lbBaseClassInfo.Items); GetClassProperties(SomeComp, lbPropList.Items); finally SomeComp.Free; end;

Uzyskiwanie oglnej informacji o obiekcie


Gdy dysponujemy ju egzemplarzem obiektu, naley dosta si do odpowiadajcej mu struktury TTypeData jest to zadaniem funkcji GetTypeData(), zdefiniowanej w module TypInfo.Pas:
function GetTypeData(TypeInfo: PTypeInfo): PTypeData;

414

Funkcja ta wykorzystywana jest w treci moduu dwukrotnie: w treci funkcji GetBaseClassInfo() i GetClassProperties(). Wymagany argument wywoania dostpny jest bezporednio jako wynik metody ClassInfo():
ClassTypeInfo := AClass.ClassInfo; ClassTypeData := GetTypeData(ClassTypeInfo);

Ostatecznie, wykorzystujc trzy rda informacji sam obiekt oraz struktury ClassTypeInfo i ClassTypeData wpisujemy na podan list acuchw wybrane informacje o obiekcie:
with AStrings do begin Add(Format('Nazwa klasy: %s', [ClassTypeInfo.Name])); EnumName := GetEnumName(TypeInfo(TTypeKind), Integer(ClassTypeInfo.Kind)); Add(Format('Typ: %s', [EnumName])); Add(Format('Rozmiar: %d', [AClass.InstanceSize])); Add(Format('Zdefiniowana w: %s.pas', [ClassTypeData.UnitName])); Add(Format('Liczba waciwoci: %d',[ClassTypeData.PropCount])); end;

Zadaniem funkcji GetEnumName() jest zwrcenie identyfikatora odpowiadajcego okrelonej wartoci typu wyliczeniowego; funkcja ta zdefiniowana jest w module TypInfo.Pas.

Odtwarzanie genealogii obiektu


To zadanie jest w gruncie rzeczy nieskomplikowane i polega na posuwaniu si w gr listy coraz to starszych przodkw obiektu typ klasy macierzystej jest wynikiem metody ClassParent(). Caa zabawa koczy si na prbie pobrania klasy macierzystej dla klasy TObject czego wynikiem jest warto NIL. Nazwy kolejnych klas macierzystych dodawane s jako kolejne pozycje listy:
AncestorClass := AClass.ClassParent; { Iteracja wzwy acucha genealogicznego a do osigniecia najwyszej klasy bazowej } AStrings.Add('Genealogia klasy'); while AncestorClass <> nil do begin AStrings.Add(Format(' %s',[AncestorClass.ClassName])); AncestorClass := AncestorClass.ClassParent; end;

Uzyskiwanie informacji o waciwociach obiektu


Liczb waciwoci obiektu zawiera pole PropCount struktury TTypeData. Co do samych waciwoci: istnieje kilka metod pobrania ich charakterystyki my opiszemy tutaj dwie z nich. Uzyskanie charakterystyki waciwoci obiektu naley do procedury GetClassProperties(). Po zidentyfikowaniu liczby waciwoci nastpuje przydzielenie pamici dla tablicy, ktrej poszczeglne elementy wskazywa bd struktury identyfikujce poszczeglne waciwoci:
GetMem(PropList, SizeOf(PPropInfo) * ClassTypeData.PropCount);

Zmienna Proplist jest wskanikiem do nastpujcej tablicy:


type TPropList = array [ 0 .. 16739] of PPropInfo;

Kady element tej tablicy wskazuje natomiast na nastpujc struktur:


TPropInfo = packed record PropType: PPTypeInfo; GetProc: Pointer; SetProc: Pointer; StoredProc: Pointer; Index: Integer; Default: Longint; NameIndex: SmallInt; Name: ShortString; end;

415

Wpisania informacji do tablicy dokonuje procedura GetPropInfos():


GetPropInfos(AClass.ClassInfo, PropList);

Analizujc kolejne elementy tablicy, doczamy charakterystyk poszczeglnych waciwoci do wywietlanej listy, z pominiciem jednake waciwoci zdarzeniowych, bdcych de facto wskanikami do metod:
for i := 0 to ClassTypeData.PropCount - 1 do // odrzu wszystkie waciwoci zdarzeniowe (czyli bdce wskanikami // do metod Kind=tkMethod) if not (PropList[i]^.PropType^.Kind = tkMethod) then AStrings.Add(Format('%s: %s', [PropList[i]^.Name, PropList[i]^.PropType^.Name]));

Innym sposobem wypenienia tablicy charakterystyk waciwoci wybranych kategorii jest wywoanie funkcji
GetPropList(); wykorzystamy j do tego, aby uzupeni list waciwociami zdarzeniowymi, ktre

pominlimy w pierwszym kroku:


NumProps := GetPropList(AClass.ClassInfo, [tkMethod], PropList); if NumProps <> 0 then begin AStrings.Add(''); AStrings.Add('ZDARZENIA ================ '); AStrings.Add(''); end; // Wpisz dane o waciwociach zdarzeniowych na list acuchw for i := 0 to NumProps - 1 do AStrings.Add(Format('%s: %s', [PropList[i]^.Name, PropList[i]^.PropType^.Name]));

Wynikiem funkcji GetPropList() jest liczba waciwoci nalecych do kategorii okrelonej przez drugi parametr wywoania; jeeli trzeci parametr, okrelajcy list docelow ma warto NIL, zwrcenie liczby waciwoci jest jedynym efektem dziaania procedury umoliwia to przydzielenie wystarczajcej pamici dla tablicy wynikowej, ktrej wskanik przekazany bdzie przy powtrnym wywoaniu. W opisywanym tu przykadzie nie ma takiego problemu, gdy rozmiar tablicy zawierajcej wskaniki do struktur TPropInfo ustalany jest na podstawie oglnej liczby waciwoci klasy. Reasumujc procedura GetPropInfos() jest uyteczna w sytuacji, gdy pobiera si informacj o wszystkich waciwociach; jeeli jednak interesuj nas tylko waciwoci okrelonych kategorii, wygodniejsza jest funkcja GetPropList().

Sprawdzanie istnienia okrelonej waciwoci w obiekcie


Nieco wczeniej zwrcilimy uwag na problem rozrnienia pomidzy komponentami wraliwymi na dane a pozostaymi komponentami formularza, informujc jednoczenie, i rozwizanie tego problemu sprowadza si do sprawdzania obecnoci waciwoci DataSource w klasie komponentu. Sprawdzenie istnienia danej waciwoci jest szczeglnym przypadkiem odczytania jej charakterystyki, ktr to czynno wykonuje funkcja GetPropInfo():
function GetPropInfo(TypeInfo: PTypeInfo; const PropName: string): PPropInfo;

Jeli waciwo o nazwie okrelonej przez PropName nie istnieje w danej klasie, funkcja zwraca warto NIL. To natychmiast sugeruje rozwizanie problemu:
Function IsDataAware(Acomponent: TComponent): Boolean; // funkcja sprawdza, czy komponent posiada waciwo // o nazwie DataSource zgodn z typem TDataSource var PropInfo: PPropInfo; begin PropInfo := GetPropInfo(AComponent.ClassInfo, 'DataSource'); Result := (PropInfo <> NIL); // dodatkowe testy zwikszajce wiarygodno if Result Then begin if not ( (PropInfo^.PropType^.Kind = tkClass) and (GetTypeData(PropInfo^.PropType^).ClassType.InheritsFrom(TDataSource)) ) Then Resut := FALSE; end;

416

end;

Oczywicie, sama nazwa waciwoci (DataSource) niczego jeszcze nie przesdza wszak kady moe zdefiniowa klas opatrujc tak nazw waciwo powiedzmy cakowitoliczbow. Aby wic zwikszy wiarygodno funkcji IsDataAware(), dodatkowo sprawdza si, czy waciwo o nazwie DataSource jest waciwoci obiektow o typie zgodnym z typem TDataSource. Zaprezentowan funkcj mona by uoglni na dowoln waciwo dowolnej klasy:
Function HasProperty(AComponent: TComponent; APropertyName: String):Boolean; var PropInfo: PPropInfo; begin PropInfo := GetPropInfo(AComponent.ClassInfo, APropertyName); Result := PropInfo <> NIL; end;

Naley jednak wyranie zaznaczy, i informacja RTTI obejmuje wycznie publikowane (published) waciwoci obiektw i tylko do nich odnosi si to wszystko, co zostao przed chwil zaprezentowane.

Informacja o metodach obiektu


RTTI zawiera rwnie informacj o metodach klas ich rodzajach (procedura, funkcja, konstruktor, destruktor itp.) oraz parametrach (ich nazwach, typach i sposobach przekazywania). Ilustracj wykorzystania tej informacji jest projekt MethodInfo.dpr (w wersjach dla VCL i CLX), znajdujcy si na doczonym krku CD-ROM. Kod jego formularza gwnego prezentujemy na poniszym wydruku.

Wydruk 10.4. Kod formularza gwnego projektu MethodInfo.Dpr


unit MainFrm; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, DBClient, MidasCon, MConnect; type TMainForm = class(TForm) lbSampMethods: TListBox; lbMethodInfo: TMemo; lblBasicMethodInfo: TLabel; procedure FormCreate(Sender: TObject); procedure lbSampMethodsClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var MainForm: TMainForm; implementation uses TypInfo, DBTables, Provider; {$R *.DFM} type // konieczna jest redefinicja poniszej struktury - jest ona // wykomentowana w typinfo.pas PParamRecord TParamRecord Flags: ParamName: TypeName: end; = ^TParamRecord; = record TParamFlags; ShortString; ShortString;

procedure GetBaseMethodInfo(ATypeInfo: PTypeInfo; AStrings: TStrings); {

417

Niniejsza metoda pobiera kilka informacji o typie i wpisuje je na podan list acuchw } var MethodTypeData: PTypeData; EnumName: String; begin MethodTypeData := GetTypeData(ATypeInfo); with AStrings do begin Add(Format('Nazwa metody: %s', [ATypeInfo^.Name])); EnumName := GetEnumName(TypeInfo(TTypeKind), Integer(ATypeInfo^.Kind)); Add(Format('Typ: %s', [EnumName])); Add(Format('Liczba parametrw: %d',[MethodTypeData.ParamCount])); end; end; procedure GetMethodDefinition(ATypeInfo: PTypeInfo; AStrings: TStrings); { Niniejsza metoda pobiera informacj o definicji metody i wpisuje j na podan list acuchw } var MethodTypeData: PTypeData; MethodDefine: String; ParamRecord: PParamRecord; TypeStr: ^ShortString; ReturnStr: ^ShortString; i: integer; begin MethodTypeData := GetTypeData(ATypeInfo); // Okrel typ metody case MethodTypeData.MethodKind of mkProcedure: MethodDefine := mkFunction: MethodDefine := mkConstructor: MethodDefine := mkDestructor: MethodDefine := mkClassProcedure: MethodDefine := mkClassFunction: MethodDefine := end;

'procedure '; 'function '; 'constructor '; 'destructor '; 'class procedure '; 'class function ';

// wskanik do pierwszego parametru ParamRecord := @MethodTypeData.ParamList; i := 1; // pierwszy parametr // pobieraj kolejno informacj o kolejnych parametrach, // tworzc deklaracj metody

while i <= MethodTypeData.ParamCount do begin if i = 1 then MethodDefine := MethodDefine+'('; if pfVar in ParamRecord.Flags then MethodDefine := MethodDefine+('var '); if pfconst in ParamRecord.Flags then MethodDefine := MethodDefine+('const '); if pfArray in ParamRecord.Flags then MethodDefine := MethodDefine+('array of '); // ustawiona flaga pfAddress oznacza NIEJAWNY parametr Self, ktrego // nie wykazujemy w informacji o metodzie { if pfAddress in ParamRecord.Flags then MethodDefine := MethodDefine+('*address* '); } if pfout in ParamRecord.Flags then MethodDefine := MethodDefine+('out ');

// uyj "arytmetyki na wskanikach" do otrzymania nazwy typu parametru: TypeStr := Pointer(Integer(@ParamRecord^.ParamName) + Length(ParamRecord^.ParamName)+1);

418

MethodDefine := Format('%s%s: %s', [MethodDefine, ParamRecord^.ParamName, TypeStr^]); inc(i); // zwiksz numer parametru // przejd do nastpnego parametru; zwr uwag na "arytmetyk // na wskanikach" ParamRecord := PParamRecord(Integer(ParamRecord) + SizeOf(TParamFlags) + (Length(ParamRecord^.ParamName) + 1) + (Length(TypeStr^)+1)); // jeeli wyczerpano zestaw parametrw, zamknij nawias if i <= MethodTypeData.ParamCount then begin MethodDefine := MethodDefine + '; '; end else MethodDefine := MethodDefine + ')'; end; // jeeli metoda jest FUNKCJ, RTTI zawiera informacj o typie // jej wyniku; znajduje si ona bezporednio po informacji // o ostatnim parametrze if MethodTypeData.MethodKind = mkFunction then begin ReturnStr := Pointer(ParamRecord); MethodDefine := Format('%s: %s;', [MethodDefine, ReturnStr^]) end else MethodDefine := MethodDefine+';'; // dodaj kompletny acuch do listy with AStrings do begin Add(MethodDefine) end; end; procedure TMainForm.FormCreate(Sender: TObject); begin // wypenij list kilkoma przykadowymi nazwami metod with lbSampMethods.Items do begin AddObject('TNotifyEvent', TypeInfo(TNotifyEvent)); AddObject('TMouseEvent', TypeInfo(TMouseEvent)); AddObject('TBDECallBackEvent', TypeInfo(TBDECallBackEvent)); AddObject('TDataRequestEvent', TypeInfo(TDataRequestEvent)); AddObject('TGetModuleProc', TypeInfo(TGetModuleProc)); AddObject('TReaderError', TypeInfo(TReaderError)); end; end; procedure TMainForm.lbSampMethodsClick(Sender: TObject); begin lbMethodInfo.Lines.Clear; with lbSampMethods do begin GetBaseMethodInfo(PTypeInfo(Items.Objects[ItemIndex]), lbMethodInfo.Lines); GetMethodDefinition(PTypeInfo(Items.Objects[ItemIndex]), lbMethodInfo.Lines); end; end; end.

Dziaanie projektu rozpoczyna si od wpisania na list lbSampMethods (w oknie dolnym) nazw kilku powszechnie uywanych typw metod wraz ze wskanikami zwracanymi przez funkcj systemow TypeInfo(). Podwietlenie pozycji na tej licie powoduje powstanie zdarzenia OnClick, ktrego obsuga odczytuje najpierw podstawowe informacje o typie procedury (za pomoc procedury GetBaseMethodInfo()), po czym odtwarza jej definicj (czynno t wykonuje procedura GetMethodDefinition()). Formularz projektu z przykadow zawartoci przedstawia rysunek 10.4.

419

Wskazwka

Funkcja TypeInfo() naley do magicznych funkcji kompilatora, podobnie jak Concat(), Readln() itp.; nie mona jej zadeklarowa w kategoriach Object Pascala.

Rysunek 10.4. Przykadowa informacja o metodzie klasy

Informacja o typach porzdkowych


Informacja RTTI nie ogranicza si bynajmniej do typw obiektowych, lecz obejmuje wszystkie typy wykorzystywane w danej aplikacji. Wskanik do zawierajcego t informacj kawaka pamici otrzyma mona za pomoc opisywanej przed chwil funkcji TypeInfo() poprzez podanie identyfikatora typu jako parametru wywoania. Zaprezentujemy t moliwo na przykadzie dwch typw porzdkowych: integer oraz wyliczeniowego, a take dla typu zbiorowego (set). Uzyskiwanie informacji o typach cakowitoliczbowych ilustruje projekt IntegerRTTI.dpr znajdujcy si na zaczonym krku CD-ROM, w wersjach dla VCL i CLX; kod jego formularza gwnego prezentujemy na poniszym wydruku.

Wydruk 10.5. Uzyskiwanie informacji RTTI o typach cakowitoliczbowych


unit MainFrm; interface

420

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TMainForm = class(TForm) lbSamps: TListBox; memInfo: TMemo; procedure FormCreate(Sender: TObject); procedure lbSampsClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var MainForm: TMainForm; implementation uses TypInfo; {$R *.DFM}

procedure TMainForm.FormCreate(Sender: TObject); begin with lbSamps.Items do begin AddObject('Word', TypeInfo(Word)); AddObject('Byte', TypeInfo(Byte)); AddObject('Integer', TypeInfo(Integer)); AddObject('SmallInt', TypeInfo(SmallInt)); AddObject('ShortInt', TypeInfo(ShortInt)); end; end; procedure TMainForm.lbSampsClick(Sender: TObject); var OrdTypeInfo: PTypeInfo; OrdTypeData: PTypeData; TypeNameStr: String; TypeKindStr: String; MinVal, MaxVal: Int64; begin memInfo.Lines.Clear; with lbSamps do begin // pobierz wskanik do struktury TTypeInfo OrdTypeInfo := PTypeInfo(Items.Objects[ItemIndex]); // pobierz wskanik do struktury TTypeData OrdTypeData := GetTypeData(OrdTypeInfo); // pobierz nazw typu TypeNameStr := OrdTypeInfo.Name; // pobierz oznaczenie typu TypeKindStr := GetEnumName(TypeInfo(TTypeKind), Integer(OrdTypeInfo^.Kind)); // pobierz skrajne wartoci typu MinVal := OrdTypeData^.MinValue; MaxVal := OrdTypeData^.MaxValue;

// dodaj do listy informacj o typie with memInfo.Lines do begin Add('Nazwa: '+TypeNameStr); Add('Typ RTTI: '+TypeKindStr); Add('Warto Min : '+IntToStr(MinVal)); Add('Warto Max : '+IntToStr(MaxVal)); end;

421

end; end; end.

Formularz projektu z przykadow zawartoci jest przedstawiony na rysunku 10.5.

Rysunek 10.5. Przykadowa informacja RTTI o typie cakowitoliczbowym Po wybraniu pozycji na licie nastpuje wywoanie metody zwracajcej wskanik do obszaru, ktry mapowany jest przez pierwszy wariant struktury TTypeData (por. wydruk 10.2); caa uyteczna informacja sprowadza si do okrelenia wartoci granicznych typu. Projekt EnumRTTI.dpr, ilustrujcy uzyskiwanie analogicznej informacji dla typu wyliczeniowego, jest niemale identyczny rnica polega na dodatkowej ptli pobierajcej identyfikatory i wartoci kolejnych elementw typu.

Wydruk 10.6. Uzyskiwanie informacji RTTI o typie wyliczeniowym


unit MainFrm; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TMainForm = class(TForm) lbSamps: TListBox; memInfo: TMemo; procedure FormCreate(Sender: TObject); procedure lbSampsClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var MainForm: TMainForm; implementation uses TypInfo, Buttons;

422

{$R *.DFM}

procedure TMainForm.FormCreate(Sender: TObject); begin // Kilka przykadowych nazw typw with lbSamps.Items do begin AddObject('TButtonState', TypeInfo(TButtonState)); AddObject('TFormStyle', TypeInfo(TFormStyle)); AddObject('Boolean', TypeInfo(Boolean)); //AddObject('WordBool', TypeInfo(WordBool)); //AddObject('ByteBool', TypeInfo(ByteBool)); //AddObject('LongBool', TypeInfo(LongBool)); end; end; procedure TMainForm.lbSampsClick(Sender: TObject); var OrdTypeInfo: PTypeInfo; OrdTypeData: PTypeData; TypeNameStr: String; TypeKindStr: String; MinVal, MaxVal: Integer; i: integer; begin memInfo.Lines.Clear; with lbSamps do begin // pobierz wskanik do struktury TTypeInfo OrdTypeInfo := PTypeInfo(Items.Objects[ItemIndex]); // pobierz wskanik do struktury TTypeData OrdTypeData := GetTypeData(OrdTypeInfo); // pobierz oznaczenie typu TypeNameStr := OrdTypeInfo.Name; // pobierz skrajne wartoci typu TypeKindStr := GetEnumName(TypeInfo(TTypeKind), Integer(OrdTypeInfo^.Kind));

// pobierz skrajne wartoci typu MinVal := OrdTypeData^.MinValue; MaxVal := OrdTypeData^.MaxValue; // dodaj do listy informacj o typie with memInfo.Lines do begin Add('Nazwa: '+TypeNameStr); Add('Typ RTTI: '+TypeKindStr); Add('Warto Min : '+IntToStr(MinVal)); Add('Warto Max : '+IntToStr(MaxVal)); // pobierz wartoci i identyfikatory poszczeglnych elementw // typu wyliczeniowego if OrdTypeInfo^.Kind = tkEnumeration then for i := MinVal to MaxVal do Add(Format(' Warto %d Nazwa: %s', [i, GetEnumName(OrdTypeInfo, i)])); end; end; end; end.

423

Formularz projektu z przykadow zawartoci zosta przedstawiony na rysunku 10.6.

Rysunek 10.6. Przykadowa informacja RTTI o typie wyliczeniowym

Ostatnie z prezentowanych zastosowa RTTI dotyczy bdzie typu zbiorowego, a dokadniej waciwoci
BorderIcons formularza i waciwoci Options przegldarki TCustomGrid. Przykadowy projekt o nazwie SetRTTI.dpr znajduje si na zaczonym krku CD-ROM w wersjach dla VCL i CLX; poniej prezentujemy

kod jego formularza gwnego. Wydruk 10.7. Uzyskiwanie informacji RTTI o typie zbiorowym
unit MainFrm; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Grids; type TMainForm = class(TForm) lbSamps: TListBox; memInfo: TMemo; procedure FormCreate(Sender: TObject); procedure lbSampsClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var MainForm: TMainForm; implementation uses TypInfo, Buttons; {$R *.DFM}

procedure TMainForm.FormCreate(Sender: TObject); begin // Kilka przykadowych typw with lbSamps.Items do begin AddObject('TBorderIcons', TypeInfo(TBorderIcons)); AddObject('TGridOptions', TypeInfo(TGridOptions)); end; end;

424

procedure GetTypeInfoForOrdinal(AOrdTypeInfo: PTypeInfo; AStrings: TStrings); var // OrdTypeInfo: PTypeInfo; OrdTypeData: PTypeData; TypeNameStr: String; TypeKindStr: String; MinVal, MaxVal: Integer; i: integer; begin // pobierz wskanik do struktury TTypeInfo OrdTypeData := GetTypeData(AOrdTypeInfo); // pobierz nazw typu TypeNameStr := AOrdTypeInfo.Name; // pobierz oznaczenie typu TypeKindStr := GetEnumName(TypeInfo(TTypeKind), Integer(AOrdTypeInfo^.Kind)); // pobierz skrajne wartoci typu MinVal := OrdTypeData^.MinValue; MaxVal := OrdTypeData^.MaxValue;

// dodaj do listy informacj o typie with AStrings do begin Add('Nazwa: '+TypeNameStr); Add('Typ RTTI: '+TypeKindStr);

// Wywoaj niniejsz funkcj rekursywnie, by pobra informacj // o typie bazowym typu zbiorowego if AOrdTypeInfo^.Kind = tkSet then begin Add('=========='); Add(''); GetTypeInfoForOrdinal(OrdTypeData^.CompType^, AStrings); end; // Poka wartoci i identyfikatory typu wyliczeniowego stanowicego // typ bazowy typu zbiorowego if AOrdTypeInfo^.Kind = tkEnumeration then begin Add('Warto Min: '+IntToStr(MinVal)); Add('Warto Max: '+IntToStr(MaxVal)); for i := MinVal to MaxVal do Add(Format(' Warto: %d end; end; end; procedure TMainForm.lbSampsClick(Sender: TObject); begin memInfo.Lines.Clear; with lbSamps do GetTypeInfoForOrdinal(PTypeInfo(Items.Objects[ItemIndex]), memInfo.Lines); end; end.

Nazwa: %s', [i, GetEnumName(AOrdTypeInfo, i)]));

W powyszym przykadzie definiowane s dwie listy. Pierwsza z nich zawiera informacj o typach podlegajcych badaniu, do drugiej natomiast wpisywane s informacje o elementach typu, uzyskane przez procedur GetTypeInfoForOrdinal(). Procedura GetTypeInfoForOrdinal() rozpoczyna sw prac od pobrania informacji zwizanej z typem zbiorowym jako caoci; pole Kind udostpnionego obszaru zawiera warto tkSet, za pole CompType (por. wydruk 10.2) zawiera wskanik do wskanika struktury TTypeInfo, zwizanej z typem bazowym typu

425

zbiorowego. Wykorzystujc zawarto pola CompType, procedura wywouje sam siebie udostpniona w wyniku tego wywoania struktura TTypeData zawiera w polu Kind warto tkEnumeration dalszy cig scenariusza jest ju znany z projektu dotyczcego typu wyliczeniowego. Formularz dziaajcego projektu jest przedstawiony na rysunku 10.7.

Rysunek 10.7. Przykadowa informacja RTTI o typie zbiorowym

Przypisywanie waciwociom wartoci za porednictwem RTTI


Opisane rodki dostpu do opublikowanych waciwoci komponentu umoliwiaj przypisywanie tym waciwociom konkretnych wartoci. Wykorzystuje t moliwo inspektor obiektw, mog j take wykorzystywa aplikacje uytkowe w module TypInfo.Pas znajduje si pewna liczba uatwiajcych to procedur, notabene wartych przestudiowania, w celu lepszego zapoznania si z mechanizmami RTTI. Rozpatrzmy w charakterze przykadu nastpujce zadanie: majc dany komponent i nazw jego waciwoci, przypisz tej ostatniej okrelon warto typu integer, pod warunkiem jednake, i waciwo ta sama bdzie typu integer (uprzednio naley oczywicie zbada, czy klasa komponentu w ogle posiada opublikowan waciwo o podanej nazwie). Problem ten rozwizuje nastpujca procedura:
procedure SetIntegerPropertyIfExists(AComp: TComponent; ApropName: String; AValue: Integer); var PropInfo: PPropInfo; begin PropInfo := GetPropInfo(AComp.ClassInfo, ApropName); if PropInfo <> NIL then begin if PropInfo^.PropType^.Kind = tkInteger then

426

SetOrdProp(AComp, PropInfo, Integer(AValue)); end; end;

Procedura ta posiada trzy parametry, okrelajce kolejno obiekt, nazw waciwoci i przypisywan warto. Wykorzystuje funkcj GetPropInfo() w celu odnalezienia struktury TPropInfo zwizanej z konkretn waciwoci; jeeli waciwo o danej nazwie nie istnieje w danej klasie (lub nie jest opublikowana), funkcja ta zwraca warto NIL. Po odnalezieniu danej struktury TPropInfo nastpuje sprawdzenie typu zwizanej z ni waciwoci. Zapisana w polu Kind warto identyfikuje typ waciwoci, bdcy w kategoriach RTTI wartoci nastpujcego typu:
TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat, tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString, tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray);

Po upewnieniu si, i odnona waciwo jest typu integer, nastpuje przypisanie jej wartoci, z wykorzystaniem w tym celu funkcji SetOrdProp() przeznaczonej dla typw porzdkowych. Wywoanie procedury SetIntegerPropertyIfExists() mogoby mie na przykad nastpujc posta:
SetIntegerPropertyIfExists(Button2, 'Width', 50);

Funkcja SetOrdProp() jest jedn z procedur przypisujcych warto (setter methods) waciwoci identyfikowanej za pomoc struktury TPropInfo; modu TypInfo.Pas definiuje ponadto analogiczne funkcje dokonujce odczytu wartoci (getter methods). Najwaniejsze z nich przedstawia tabela 10.7.

Tabela 10.7. Przykadowe funkcje moduu TypInfo.Pas dokonujce odczytu i przypisywania wartoci waciwociom identyfikowanym za pomoc RTTI

Typ waciwoci porzdkowy wyliczeniowy obiektowy


string

Funkcja przypisujca
SetOrdProp() SetEnumProp() SetObjectProp() SetStrProp() SetFloatProp() SetVariantProp() SetMethodProp() SetInt64Prop()

Funkcja odczytujca
GetOrdProp() GetEnumProp() GetObjectProp() GetStrProp() GetFloatProp() GetVariantProp() GetMethodProp() GetInt64Prop()

zmiennoprzecinkowy
variant

zdarzeniowy
Int64

Ponisza procedura ilustruje przypisanie wartoci do waciwoci obiektowej:


procedure SetObjectPropertyIfExists(AComponent: TComponent; APropName: String; AValue: TObject); var PropInfo: PPropInfo; begin PropInfo := GetPropInfo(AComponent.ClassInfo, APropName); if PropInfo <> NIL then begin if PropInfo^.PropType^.Kind = tkClass then SetObjectProp(AComponent, PropInfo, AValue); end; end;

427

Oto przykad wykorzystania tej procedury:


var F: TFont; begin F := TFont.Create; F.Name := 'Arial'; F.Size := 24; F.Color := clRed; SetObjectPropertyIfExists(Panel1, 'Font', F); end;

Na zakoczenie przykad przypisania wartoci waciwoci zdarzeniowej:


procedure SetMethodPropertyIfExists(AComponent: TComponent; APropName: String; AMethod: TMethod); var PropInfo: PPropInfo; begin PropInfo := GetPropInfo(AComponent.ClassInfo, APropName); if PropInfo <> NIL then begin if PropInfo^.PropType^.Kind = tkMethod then SetMethodProp(AComponent, PropInfo, AMethod); end; end;

Wartoci waciwoci zdarzeniowej jest wskanik do metody stanowicej procedur zdarzeniow; kada metoda reprezentowana jest w RTTI przez nastpujcy rekord, zdefiniowany w module System.Pas:
TMethod = record Code, Data: Pointer; end;

Do uzyskania tego rekordu suy funkcja GetMethodProp(), otrzymujca jako parametry wskanik do obiektu i nazw metody, na przykad:
var OfMyMethod: TMethod; OfMyMethod := GetMethodProp(Panel1, 'OnClick'); SetMethodPropertyIfExists(Button5, 'OnClick', OfMyMethod);

Powysza sekwencja przypisuje przyciskowi Button5 t sam procedur obsugi zdarzenia OnClick, z ktrej korzysta panel Panel1. Ilustracj koncepcji opisywanych w niniejszym punkcie jest projekt SetProperties.dpr, znajdujcy si na zaczonym krku CD-ROM, w wersjach dla VCL i CLX.

Podsumowanie
Niniejszy rozdzia jest wprowadzeniem do bibliotek VCL i CLX. Przedstawilimy w nim hierarchi komponentw obydwu bibliotek oraz cechy charakterystyczne komponentw na poszczeglnych szczeblach tej hierarchii. Zaprezentowalimy te przykady wykorzystania mechanizmw RTTI do uzaleniania pewnych czynnoci (w czasie wykonania programu) od konkretnych szczegw definicji wykorzystywanych klas i ich waciwoci.

428

Rozdzia 12.

Zaawansowane techniki tworzenia komponentw


W poprzednim rozdziale przedstawilimy podstawowe zasady tworzenia nowych komponentw i wczania ich do palety. Niniejszy rozdzia opisuje techniki bardziej zaawansowane; przedstawimy m.in. komponenty pseudowizualne, budow i zasady dziaania edytora waciwoci, niestandardowe edytory komponentw oraz kolekcje komponentw.

Komponenty pseudowizualne
Tre niniejszej ksiki obfituje w przykady zastosowa komponentw widocznych, ktrych przykadami s TButton i TEdit oraz komponentw niewidocznych, jak chociaby prezentowany w poprzednim rozdziale TTimer, czy te bazodanowy komponent TTable. Obecnie przedstawimy grup komponentw o cechach porednich komponenty te nie zasuguj na miano wizualnych, gdy nie mona ich umieci w palecie komponentw; z drugiej strony manifestuj one sw obecno podczas wykonywania programu na rwni z innymi widocznymi komponentami. Z tego wzgldu nosz nazw komponentw pseudowizualnych (pseudovisual components).

Niestandardowe podpowiedzi kontekstowe


Podpowiedzi kontekstowe prostokty pojawiajce si po zatrzymaniu kursora myszy na wikszoci obiektw Win32 to wanie jeden z przykadw komponentw pseudowizualnych. Pokaemy teraz, w jaki sposb mona manipulowa ich parametrami; midzy innymi, jak sam za chwil zobaczysz, niekoniecznie musz mie ksztat prostoktny! Konstrukcj przykadowego projektu podzielimy na cztery etapy: 1. 2. 3. 4. Zdefiniowanie klasy reprezentujcej nowy typ podpowiedzi. Zwolnienie egzemplarza domylnej klasy podpowiedzi. Zdefiniowanie nowej klasy podpowiedzi jako klasy domylnej. Stworzenie egzemplarza nowej klasy podpowiedzi.

489

Tworzenie klasy pochodnej do THintWindow


Istot nowej klasy podpowiedzi TddgHintWindow jest zmiana najbardziej (wydawaoby si) przyrodzonej cechy podpowiedzi jej prostoktnego ksztatu. Nowy komponent ma ksztat (skrajnie) zaokrglonego w naronikach prostokta. Stanowi to skdind okazj do omwienia frapujcego aspektu interfejsu Win32 API, a mianowicie tworzenia okienek o ksztacie innym ni prostoktny! Klasa TddgHintWindow zdefiniowana jest w module RndHint.pas, ktrego tre prezentujemy na wydruku 12.1.

Wydruk 12.1. Implementacja nieprostoktnego okienka podpowiedzi kontekstowej


unit RndHint; interface uses Windows, Classes, Controls, Forms, Messages, Graphics; type TddgHintWindow = class(THintWindow) private FRegion: THandle; procedure FreeCurrentRegion; public destructor Destroy; override; procedure ActivateHint(Rect: TRect; const AHint: string); override; procedure Paint; override; procedure CreateParams(var Params: TCreateParams); override; end; implementation destructor TddgHintWindow.Destroy; begin FreeCurrentRegion; inherited Destroy; end; procedure TddgHintWindow.CreateParams(var Params: TCreateParams); { okienko podpowiedzi kontekstowej pozbawione jest obrzea } begin inherited CreateParams(Params); Params.Style := Params.Style and not ws_Border; // usu obrzee end; procedure TddgHintWindow.FreeCurrentRegion; { Regions, like other API objects, should be freed when you are { through using them. Note, however, that you cannot delete a { region which is currently set in a window, so this method sets { the window region to 0 before deleting the region object.

} } } }

{ Regiony, podobnie jak inne obiekty API, powinny zosta zwolnione po wykorzystaniu. Nie mona jednak usun obiektu regionu przypisanego aktualnie do okna, naley go wpierw usun z okna }

begin if FRegion <> 0 then begin SetWindowRgn(Handle, 0, True); DeleteObject(FRegion); FRegion := 0; end; end;

// // // //

jeeli zdefiniowano region... usu go z okna zwolnij obiekt regionu wyzeruj wskanik do regionu

procedure TddgHintWindow.ActivateHint(Rect: TRect; const AHint: string); { Aktywowanie podpowiedzi kontekstowej } begin with Rect do Right := Right + Canvas.TextWidth('WWWW');

// niewielki prawy margines

490

BoundsRect := Rect; FreeCurrentRegion; { utwrz region w ksztacie zaokrglonego prostokta } FRegion := CreateRoundRectRgn(0, 0, Width, Height, Width, Height); if FRegion <> 0 then SetWindowRgn(Handle, FRegion, True); // przypisz region do okna inherited ActivateHint(Rect, AHint); end; procedure TddgHintWindow.Paint; { Narysowanie zawartoci okienka podpowiedzi kontekstowej w odpowiedzi na komunikat WM_PAINT. }

var R: TRect; begin R := ClientRect;

// pobierz wsprzdne prostokta // opisanego na okienku podpowiedzi // obetnij 1 piksel z lewej strony // wypenij region tem

Inc(R.Left, 1); Canvas.Brush.Color := clInfoBk;

FillRgn(Canvas.Handle, FRegion, Canvas.Brush.Handle);

Canvas.Font.Color := clInfoText;

// kolor pierwszoplanowy

// wypisz tre podpowiedzi w sposb wyrodkowany DrawText(Canvas.Handle, PChar(Caption), Length(Caption), R, DT_NOPREFIX or DT_WORDBREAK or DT_CENTER or DT_VCENTER); end; var OldHintClass: THintWindowClass; function SetNewHintClass(AClass: THintWindowClass): THintWindowClass; var DoShowHint: Boolean; begin Result := HintWindowClass; // dotychczasowa klasa okna podpowiedzi DoShowHint := Application.ShowHint; if DoShowHint then Application.ShowHint := False; HintWindowClass := AClass; if DoShowHint then Application.ShowHint := True; end;

// zwolnij dotychczasowe okno podpowiedzi // przypisz now klas // i utwrz nowe okno podpowiedzi

initialization OldHintClass := SetNewHintClass(TddgHintWindow); finalization SetNewHintClass(OldHintClass); end.

Zwr uwag na wany fakt, i okno reprezentujce podpowied musi by pozbawione obrzea prostoktne obrzee eliptycznego regionu obnayoby natychmiast cay trik; brak obrzea jest wynikiem dodatkowej instrukcji obecnej w metodzie CreateParams(), wyczajcej flag WS_BORDER w polu Style parametrw tworzonego okna. Za wypisanie tekstu podpowiedzi odpowiedzialna jest metoda Paint() tekst wypisywany jest w centrum okienka podpowiedzi, w kolorze systemowym clInfoText, przeznaczonym wanie dla podpowiedzi kontekstowych.

491

Okienka eliptyczne Wspominalimy przed chwil o moliwoci tworzenia okienek o ksztacie rnym od prostoktnego, musimy jednak ucili t wypowied: ot w kontekcie obszaru zajmowanego na ekranie, okna Win32 API zawsze s prostoktami, a odstpstwo od prostoktnego ksztatu dotyczy tej czci okna, ktra podlega wywietlaniu i odwieaniu. Rozrnienie tych dwch kategorii moliwe jest dziki specyficznym obiektom API zwanym regionami. Z kadym oknem moe by (chocia nie musi) zwizany w danej chwili obiekt-region (co najwyej jeden); wszystkie operacje zwizane z wywietlaniem zawartoci okna ograniczone s tylko do jego biecego regionu. Win32 API udostpnia szereg funkcji do tworzenia regionw o rnorodnych ksztatach, midzy innymi:
CreateEllipticRgn(), CreateEllipticRgnIndirect() dla regionw eliptycznych; CreatePolygonRgn() dla regionw w ksztacie wieloktw; CreatePolyPolygonRgn() dla regionw tworzonych przez grup wieloktw, by moe

nakadajcych si;
CreateRectRgn(), CreateRectRgnIndirect() dla regionw prostoktnych CreateRoundRectRgn() dla regionw w ksztacie prostoktw z zaokrglonymi naronikami; ExtCreateRegion() dla regionu tworzonego przez transformacj istniejcego regionu; CombineRgn() dla regionu stanowicego kombinacj dwch regionw.

Dokadny opis wymienionych funkcji znajduje si w systemie pomocy Win32 API. Przypisanie stworzonego regionu do konkretnego okna odbywa si za pomoc procedury SetWindowRgn() od tej chwili wszystkie operacje wywietlania zwizane z tym oknem bd ograniczone tylko do zdefiniowanego regionu.

Ostrzeenie

Operujc regionami Win32 API, musisz by wiadom dwch efektw ubocznych. Po pierwsze jako e odwieaniu podlega jedynie region okna, okno to pozbawione jest zazwyczaj obrzea i paska tytuowego; nie mona wic takiego okna przesuwa, zamyka, maksymalizowa itp. bezporednio na ekranie da si to uczyni jedynie w sposb programowy. Po drugie region przypisany do okna jest wasnoci Win32 API, nie mona wic zwalnia zwizanego z nim obiektu ani go modyfikowa; wszelkie tego typu operacje musz by poprzedzone odczeniem regionu od okna, czego ilustracj jest chociaby metoda FreeCurrentRegion() klasy TddgHintWindow.

Zmiana domylnej klasy podpowiedzi Analizujc tre moduu RndHint.pas atwo zauwaysz, i w jego czci inicjacyjnej nastpuje zmiana dotychczasowej klasy okna podpowiedzi na TddgHintWindow; dotychczasowa klasa zapamitywana jest w celu pniejszego jej przywrcenia, ktre odbywa si w sekcji finalization. W taki oto sposb klasa TddgHintWindow staje si obowizujc klas podpowiedzi kontekstowych dla aplikacji uywajcej wspomnianego moduu. Przed podmian domylnej klasy podpowiedzi naley jednak zwolni jej (ewentualnie istniejce) okno wykonuje si to bardzo prosto, poprzez ustawienie na False waciwoci ShowHint obiektu Application. W momencie przypisania tej waciwoci wartoci True nastpuje tworzenie nowego okna, o klasie wskazywanej przez globaln zmienn HintWindowClass. Zastosowanie klasy TddgHintWindow Specyfik klasy TddgHintWindow jest sposb integrowania jej z aplikacjami, odmienny ni w przypadku zwykych komponentw. Ot z uwagi na to, i obiekt tej klasy (czyli po prostu okno podpowiedzi) tworzony jest w czci inicjacyjnej moduu, modu ten nie powinien by czci pakietu projektowego, za sam komponent nie powinien by umieszczany w palecie komponentw, bo i tak na nic si tam nie przyda. Aby

492

spowodowa zmian ksztatu podpowiedzi kontekstowych aplikacji, wystarczy wykona czynno znacznie prostsz mianowicie umieci nazw moduu RndHint w dyrektywie uses ktregokolwiek z moduw.

Animowane komponenty
Efektownym elementem wielu aplikacji s rnego rodzaju animacje, na przykad te o charakterze ozdobnym, towarzyszce okienkom About lub O programie (wystarczy wywietli okno About Delphi 6 i, przy wcinitym lewym klawiszu Alt, wpisa sowo team). Mona si bawi i jednoczenie uczy skonstruujemy wic za chwil komponent o podobnym dziaaniu.

Komponent TddgMarquee
Istot funkcjonowania komponentu TddgMarquee jest pionowe przewijanie dugiego tekstu, ktrego fragment obserwowany jest przez prostoktne okienko niczym widoczny fragment markizy sklepowej (std nazwa komponentu). Klas bazow dla komponentu jest TCustomPanel posiadajcy kilka potrzebnych elementw, midzy innymi estetyczne trjwymiarowe obrzee. acuchy skadajce si na przewijany tekst przechowywane s natomiast w licie typu TStringList. Zrozumienie zasady dziaania komponentu stanie si prostsze, jeeli potraktujemy wywietlany tekst nie jako cig acuchw, lecz jako bitmap. Komponent dokonuje wypisania swych acuchw na roboczej bitmapie i kopiuje pocztek tej bitmapy na swe wasne ptno (uywajc funkcji API BitBlt()); nastpnie przesuwa si o kilka pikseli w gb bitmapy roboczej i kopiuje jej inny fragment. Uzyskujemy w ten sposb symulacj przewijania. Proces ten powtarzany jest a do osignicia koca bitmapy roboczej. Podobnie jak w poprzednim rozdziale, rozpoczniemy od oglnego zarysu funkcjonalnego komponentu, obejmujcego wykorzystywane przez niego mechanizmy skadowe. S cztery takie mechanizmy: wypisywanie tekstu na bitmapie roboczej, kopiowanie fragmentw bitmapy roboczej na ptno komponentu, wewntrzny zegar regulujcy szybko symulowanego przewijania, niezbdne metody klasy komponentu, wraz z konstruktorem i destruktorem, pozostae waciwoci i metody.

Wykorzystanie roboczej bitmapy


Pierwszym pytaniem zwizanym z robocz bitmap jest pytanie o jej rozmiar; musi by wystarczajcy do przechowania caoci tekstu. Dla uproszczenia potraktujmy t bitmap jako prostoktny obszar; jego szeroko musi odpowiada szerokoci okna wywietlania (nie ma potrzeby przechowywania tekstu, ktry znika poza praw krawdzi). Problemem staje si natomiast wyznaczenie jego wysokoci mona j wyznaczy mierzc wysoko poszczeglnych linii tekstu za pomoc funkcji API GetTextMetric():
var Metrics: TTextMetric; begin GetTextMetrics(Canvas.Handle, Metrics);

Funkcja, otrzymujc kontekst urzdzenia, wypenia informacjami struktur typu TTextMetric zawierajc rnorodne informacje zwizane z wypisywaniem tekstu na jego ptnie; jedn z takich informacji jest wanie pikselowa wysoko znaku aktualnie wybranej czcionki zawiera j pole tmHeight.

493

Wskazwka

Nie nadaje si do powyszego celu metoda TextHeight() klasy TCanvas, poniewa oblicza ona wysoko pikselow konkretnej linii tekstu, nie dajc adnych informacji o uytej czcionce.

Inn uyteczn informacj zawiera pole tmInternalLeading okrela ono wielko odstpu midzyliniowego. Po dodaniu obydwu wymienionych pl otrzymujemy wysoko pojedynczej linii:
lineHi := Metrics.tmHeight + Metrics.InternalLeading;

Gdy pomnoymy t warto przez liczb linii tekstu i dodamy po jednym odstpie midzyliniowym przed pierwsz lini i po ostatniej linii, otrzymamy cakowit wysoko obszaru:
var VRect: TRect; { Prostokt VRect reprezentuje ca bitmap robocz } with VRect do begin Top := 0; Left := 0; Right := Width; Bottom := LineHi * FItems.Count + Height * 2; end;

Po utworzeniu bitmapy roboczej nastpuje ustawienie jej czcionki i koloru, zgodnie z ustawieniami samego komponentu oraz ustawienie stylu pdzla na bsClear, co spowoduje wyczyszczenie zawartoci bitmapy przy najbliszym wywoaniu FillRect():
Font := Self.Font; Brush.Color := Self.Color; FillRect(VRect); Brush.Style := bsClear;

Wskazwka

Ustawienie stylu pdzla na bsClear powoduje, i tekst wypisywany bdzie na przezroczystym tle, w przeciwnym razie to miaoby kolor pdzla (Canvas.Brush.Color).

Do wypisywania tekstu na roboczej bitmapie uyjemy funkcji API DrawText(), zapewniajcej waciwe wyrwnanie tekstu w poziomie; wyrwnanie to okrelone jest przez pole FJust nastpujcego typu:
TJustification = ( tjCenter, // wyrodkowanie tjLeft, // lewostronne dosunicie tjRight // prawostronne dosunicie );

Ponisza metoda wypisuje pojedyncz lini tekstu numer linii wskazuje drugi parametr; zwr uwag na tablic Flags, suc do przeliczenia wartoci typu TJustification na flagi wymagane przez funkcj DrawText().
procedure TddgMarquee.PaintLine(R: TRect; LineNum: Integer); { wypisanie pojedynczej linii na bitmapie roboczej } const Flags: array[TJustification] of DWORD = (DT_CENTER, DT_LEFT, DT_RIGHT); var S: string; begin { dla przejrzystoci przypisz acuch do zmiennej prostej } S := FItems.Strings[LineNum]; { wypisz acuch na bitmapie roboczej}

494

DrawText(MemBitmap.Canvas.Handle, PChar(S), Length(S), R, Flags[FJust] or DT_SINGLELINE or DT_TOP); end;

Wywietlanie komponentu
Kolejnym problemem jest wywietlenie waciwego fragmentu tekstu w kadrze przewijania komponentu. Wywietlenie to realizowane jest przez metod Paint() wywoywan w odpowiedzi na komunikat WM_PAINT. Zasadnicz czci tej metody jest skopiowanie fragmentu bitmapy roboczej na ptno komponentu; kopiowanie to realizowane jest przez funkcj API BitBlt(), otrzymujc uchwyt ptna bitmapy roboczej i uchwyt ptna komponentu. Jeeli jednak komponent nie jest aktualnie animowany, kopiowanie takie nie jest konieczne komponent posiada ju ustalon reprezentacj graficzn i wystarcza wwczas wywoanie odziedziczonej metody Paint(). Informacj o tym, czy trwa wanie animacja, czy nie, zawiera pole FActive:
procedure TddgMarquee.Paint; // odwieenie wygldu komponentu begin if FActive // czy trwa animacja? then { tak, kopiuj z bitmapy roboczej na ptno komponentu } BitBlt(Canvas.Handle, 0, 0, InsideRect.Right, InsideRect.Bottom, MemBitmap.Canvas.Handle, 0, CurrLine, srcCopy) else inherited Paint; end;

Zwr take uwag na pole CurrLine wskazuje ono pocztek kopiowanej porcji bitmapy. Zalenie od kierunku przewijania, jest ono inkrementowane lub dekrementowane a do osignicia koca (lub pocztku) bitmapy.

Animowanie komponentu
Kolejnym aspektem dziaalnoci komponentu TddgMarquee jest mechanizm wprawiajcy w ruch ca animacj. Mechanizmem tym s zdarzenia zegarowe OnTimer generowane przez wewntrzny komponent roboczy FTimer klasy TTimer; w ramach obsugi kadego z tych zdarze nastpuje modyfikacja pola CurrLine (zgodnie z kierunkiem przewijania) i skopiowanie nowej porcji z bitmapy roboczej. Utworzenie i zainicjowanie komponentu FTimer jest treci procedury DoTimer stanowicej cz konstruktora Create():
procedure DoTimer; { utworzenie wewntrznego zegara i ustawienie jego parametrw begin FTimer := TTimer.Create(Self); with FTimer do begin Enabled := False; Interval := TimerInterval; OnTimer := DoTimerOnTimer; end; end; }

Procedur obsugi zdarze zegarowych jest, jak wida, procedura DoTimerOnTimer. Jej dziaanie polega na uaktualnieniu wskanika pocztku wywietlanego obszaru (w bitmapie rdowej) i odwieeniu zawartoci okna.

495

Wskazwka

Przyporzdkowujc w kodzie programu procedury obsugi poszczeglnym zdarzeniom, musisz pamita o dwch istotnych zasadach:

procedura obsugi zdarzenia musi by metod jakiegokolwiek obiektu; nie moe to by samodzielna procedura ani funkcja

typ przypisywanej metody musi by zgodny z typem samego zdarzenia (pod wzgldem zestawu parametrw wywoania).

Tak wic procedur obsugi zdarzenia OnTimer moe by tylko metoda typu TNotifyEvent, tj. posiadajca pojedynczy, obiektowy parametr wywoania.

Wspomniana metoda DoTimerOnTimer ma nastpujc posta:


procedure TddgMarquee.DoTimerOnTimer(Sender: TObject); { niniejsza metoda obsuguje zdarzenie OnTimer zegara } begin IncLine; { odwieenie kadru } InvalidateRect(Handle, @InsideRect, False); end;

Wywoywana na pocztku metoda IncLine uaktualnia wskanik pocztku kopiowanej porcji bitmapy roboczej. Kontroluje ponadto, czy nie osignito koca (lub pocztku) bitmapy w takiej sytuacji animacja jest zatrzymywana:
procedure TddgMarquee.IncLine; { signicie do nastpnej/poprzedniej linii } begin if not FScrollDown then // czy przewijanie "do przodu"? begin { sprawd, czy moliwe jest signicie po nastpn lini } if FItems.Count * LineHi + ClientRect.Bottom ScrollPixels >= CurrLine then { przejd do nastpnej linii } Inc(CurrLine, ScrollPixels) else SetActive(False); // koniec bitmapy, zatrzymaj animacj end else begin // przewijanie "do tyu" { sprawd, czy moliwe jest signicie po poprzedni lini } if CurrLine >= ScrollPixels then { przejd do poprzedniej linii } Dec(CurrLine, ScrollPixels) else SetActive(False); // pocztek bitmapy, zatrzymaj animacj end; end;

Odwieenie zawartoci okna odbywa si za pomoc procedury API InvalidateRect(). Zrezygnowalimy z metody TCanvas.Invalidate(), gdy przemalowuje ona cae ptno komponentu, my za ograniczamy si do jego wntrza (bez stylizowanego obrzea); redukuje to w znacznym stopniu migotanie (moesz to sprawdzi), ktre prawie zawsze jest zjawiskiem niepodanym. Konstruktor TddgMarquee.Create() nie jest zbyt skomplikowany. Odpowiada za utworzenie i zainicjowanie wewntrznych komponentw roboczych listy Items oraz zegara FTimer:
constructor TddgMarquee.Create(AOwner: TComponent); procedure DoTimer;

496

{ utworzenie wewntrznego zegara i ustawienie jego parametrw begin FTimer := TTimer.Create(Self); with FTimer do begin Enabled := False; Interval := TimerInterval; OnTimer := DoTimerOnTimer; end; end; begin inherited Create(AOwner); FItems := TStringList.Create; DoTimer; { wartoci domylne pl } Width := 100; Height := 75; FActive := False; FScrollDown := False; FJust := tjCenter; BevelWidth := 3; end;

{ utworzenie listy acuchw } { ustawienie zegara }

Ponownie zwracamy uwag na konieczno wywoania odziedziczonego konstruktora (inherited Create()) zaniedbanie tej czynnoci uniemoliwia funkcjonowanie komponentu, m.in. z powodu braku ptna, niezdolnoci do reagowania na komunikaty Windows oraz niemonoci przechowywania zawartoci w strumieniu. Destruktor Destroy() jest jeszcze prostszy:
destructor TddgMarquee.Destroy; begin SetActive(False); FTimer.Free; // zwolnij zegar i list FItems.Free; inherited Destroy; end;

Zatrzymuje on najpierw (ewentualnie) trwajc animacj, zwalnia obiekty pomocnicze i w kocu wywouje odziedziczony destruktor Destroy().

Wskazwka

Naley przyj regu wywoywania odziedziczonego konstruktora na pocztku konstruktora przedefiniowanego, i analogicznie regu wywoywania odziedziczonego destruktora na kocu destruktora przedefiniowanego. Na wejciu do odziedziczonego destruktora stan zasobw obiektu jest wwczas taki sam, jak bezporednio po wykonaniu odziedziczonego konstruktora.

Istniej oczywicie wyjtki od tej zasady, s one jednak dosy rzadkie i zawsze musz by wyranie uzasadnione.

Uruchamianiem i zatrzymywaniem animacji zajmuje si metoda SetActive() wywoywana zarwno w ramach metody IncLine(), jak i destruktora Destroy(); jest ona metod dostpow waciwoci Active.
procedure TddgMarquee.SetActive(Value: Boolean); { zatrzymanie/uruchomienie animacji } begin if Value and (not FActive) and (FItems.Count > 0) then begin // uruchomienie animacji FActive := True; // ustaw flag aktywnoci MemBitmap := TBitmap.Create; // utwrz bitmap robocz

497

FillBitmap; FTimer.Enabled := True; end else if (not Value) and FActive begin FTimer.Enabled := False; // if Assigned(FOnDone) // then FOnDone(Self); FActive := False; // MemBitmap.Free; // Invalidate; // end; end;

// wypenij bitmap robocz // uruchom zegar then zatrzymaj zegar wygeneruj zdarzenie OnDone

wyzeruj flag aktywnoci zwolnij bitmap odwie wygld komponentu

Poza koniecznymi czynnociami manipulacyjnymi, jak uruchamianie (zatrzymywanie) zegara i tworzenie (zwalnianie) bitmapy, wykonuje on jeszcze jedn czynno istotn dla uytkownika w momencie zatrzymania animacji generowane jest zdarzenie OnDone, o ile przypisano mu procedur obsugi:
if Assigned(FOnDone) then FOnDone(Self); // wygeneruj zdarzenie OnDone

Zdarzenie to reprezentowane jest przez waciwo zdarzeniow odnoszc si bezporednio do pola zawierajcego wskanik do procedury obsugi:
property OnDone: TNotifyEvent read FOnDone write FOnDone;

Kompletn tre moduu implementujcego komponent TddgMarquee przedstawiamy na wydruku 12.2.

Wydruk 12.2. Implementacja komponentu TddgMarquee


unit Marquee; interface uses SysUtils, Windows, Classes, Forms, Controls, Graphics, Messages, ExtCtrls, Dialogs; const ScrollPixels = 3; TimerInterval = 50;

// wielko "skoku" przewijania // odstp czasu pomidzy kolejnymi "skokami"

type TJustification = (tjCenter, tjLeft, tjRight); EMarqueeError = class(Exception); TddgMarquee = class(TCustomPanel) private MemBitmap: TBitmap; InsideRect: TRect; FItems: TStringList; FJust: TJustification; FScrollDown: Boolean; LineHi : Integer; CurrLine : Integer; VRect: TRect; FTimer: TTimer; FActive: Boolean; FOnDone: TNotifyEvent; procedure SetItems(Value: TStringList); procedure DoTimerOnTimer(Sender: TObject); procedure PaintLine(R: TRect; LineNum: Integer); procedure SetLineHeight; procedure SetStartLine; procedure IncLine; procedure SetActive(Value: Boolean); protected procedure Paint; override;

498

procedure FillBitmap; virtual; public property Active: Boolean read FActive write SetActive; constructor Create(AOwner: TComponent); override; destructor Destroy; override; published property ScrollDown: Boolean read FScrollDown write FScrollDown; property Justify: TJustification read FJust write FJust default tjCenter; property Items: TStringList read FItems write SetItems; property OnDone: TNotifyEvent read FOnDone write FOnDone; { Publish inherited properties: } property Align; property Alignment; property BevelInner; property BevelOuter; property BevelWidth; property BorderWidth; property BorderStyle; property Color; property Ctl3D; property Font; property Locked; property ParentColor; property ParentCtl3D; property ParentFont; property Visible; property OnClick; property OnDblClick; property OnMouseDown; property OnMouseMove; property OnMouseUp; property OnResize; end; implementation constructor TddgMarquee.Create(AOwner: TComponent);

procedure DoTimer; { utworzenie wewntrznego zegara i ustawienie jego parametrw begin FTimer := TTimer.Create(Self); with FTimer do begin Enabled := False; Interval := TimerInterval; OnTimer := DoTimerOnTimer; end; end; begin inherited Create(AOwner); FItems := TStringList.Create; DoTimer; { wartoci domylne pl } Width := 100; Height := 75; FActive := False; FScrollDown := False; FJust := tjCenter; BevelWidth := 3; end; destructor TddgMarquee.Destroy; begin SetActive(False); FTimer.Free; // zwolnij zegar i list FItems.Free; inherited Destroy; end; procedure TddgMarquee.DoTimerOnTimer(Sender: TObject); { niniejsza metoda obsuguje zdarzenie OnTimer zegara } begin IncLine;

{ utworzenie listy acuchw } { ustawienie zegara }

499

{ odwieenie kadru } InvalidateRect(Handle, @InsideRect, False); end; procedure TddgMarquee.IncLine; { signicie do nastpnej/poprzedniej linii } begin if not FScrollDown then // czy przewijanie "do przodu"? begin { sprawd, czy moliwe jest signicie po nastpn lini } if FItems.Count * LineHi + ClientRect.Bottom ScrollPixels >= CurrLine then { przejd do nastpnej linii } Inc(CurrLine, ScrollPixels) else SetActive(False); // koniec bitmapy, zatrzymaj animacj end else begin // przewijanie "do tyu" { sprawd, czy moliwe jest signicie po poprzedni lini } if CurrLine >= ScrollPixels then { przejd do poprzedniej linii } Dec(CurrLine, ScrollPixels) else SetActive(False); // pocztek bitmapy, zatrzymaj animacj end; end; procedure TddgMarquee.SetItems(Value: TStringList); begin if FItems <> Value then FItems.Assign(Value); end; procedure TddgMarquee.SetLineHeight; { obliczenie wysokoci pojedynczej linii } var Metrics : TTextMetric; begin { pobierz metryk czcionki } GetTextMetrics(Canvas.Handle, Metrics); { dodaj odstp midzyliniowy } LineHi := Metrics.tmHeight + Metrics.tmInternalLeading; end; procedure TddgMarquee.SetStartLine; { obliczenie numeru linii pocztkowej } begin if not FScrollDown then CurrLine := 0 // przewijanie "do przodu" else CurrLine := VRect.Bottom - Height; // przewijanie "do tyu" end; procedure TddgMarquee.PaintLine(R: TRect; LineNum: Integer); { wypisanie pojedynczej linii na bitmapie roboczej } const Flags: array[TJustification] of DWORD = (DT_CENTER, DT_LEFT, DT_RIGHT); var S: string; begin { dla przejrzystoci przypisz acuch do zmiennej prostej } S := FItems.Strings[LineNum]; { wypisz acuch na bitmapie roboczej} DrawText(MemBitmap.Canvas.Handle, PChar(S), Length(S), R, Flags[FJust] or DT_SINGLELINE or DT_TOP); end; procedure TddgMarquee.FillBitmap; var y, i : Integer; R: TRect; begin

500

SetLineHeight; // oblicz wysoko pojedynczej linii { oblicz rozmiary bitmapy } VRect := Rect(0, 0, Width, LineHi * FItems.Count + Height * 2); { prostokt InsideRect reprezentuje wntrze obszaru } InsideRect := Rect(BevelWidth, BevelWidth, Width - (2 * BevelWidth), Height - (2 * BevelWidth)); R := Rect(InsideRect.Left, 0, InsideRect.Right, VRect.Bottom); SetStartLine; // inicjacja bitmapy roboczej MemBitmap.Width := Width; with MemBitmap do begin Height := VRect.Bottom; with Canvas do begin Font := Self.Font; Brush.Color := Color; FillRect(VRect); Brush.Style := bsClear; end; end; y := Height; i := 0; repeat R.Top := y; PaintLine(R, i); { zwiksz wsprzdn pionow o wysoko linii } inc(y, LineHi); inc(i); until i >= FItems.Count; // powtrz dla kadej linii end; procedure TddgMarquee.Paint; // odwieenie wygldu komponentu begin if FActive // czy trwa animacja? then { tak, kopiuj z bitmapy roboczej na ptno komponentu } BitBlt(Canvas.Handle, 0, 0, InsideRect.Right, InsideRect.Bottom, MemBitmap.Canvas.Handle, 0, CurrLine, srcCopy) else inherited Paint; end; procedure TddgMarquee.SetActive(Value: Boolean); { zatrzymanie/uruchomienie animacji } begin if Value and (not FActive) and (FItems.Count > 0) then begin // uruchomienie animacji FActive := True; // ustaw flag aktywnoci MemBitmap := TBitmap.Create; // utwrz bitmap robocz FillBitmap; // wypenij bitmap robocz FTimer.Enabled := True; // uruchom zegar end else if (not Value) and FActive then begin FTimer.Enabled := False; // zatrzymaj zegar if Assigned(FOnDone) // wygeneruj zdarzenie OnDone then FOnDone(Self); FActive := False; // wyzeruj flag aktywnoci MemBitmap.Free; // zwolnij bitmap Invalidate; // odwie wygld komponentu end; end; end.

501

Notatka

Zwr uwag na klauzul default w definicji waciwoci Justify; okrela ona warto tjCenter jako domyln warto waciwoci szczegowy opis klauzuli default znajduje si w poprzednim rozdziale.

Testowanie komponentu TddgMarquee


Przed umieszczeniem nowo stworzonego komponentu w palecie, konieczne jest jego przetestowanie w projekcie, w ktrym jego egzemplarz tworzony jest i zwalniany dynamicznie, bez zwizku z palet komponentw. Powody tego wyjanilimy w poprzednim rozdziale, poniej prezentujemy natomiast przykadowy modu jednego z takich projektw. Ten projekt znajduje si na zaczonym krku CD-ROM pod nazw TestMarq.dpr.

Wydruk 12.3. Przykadowy modu projektu testujcego poprawno komponentu TddgMarquee


unit Testu; interface uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, Marquee, StdCtrls, ExtCtrls; type TForm1 = class(TForm) Button1: TButton; Button2: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); private Marquee1: TddgMarquee; procedure MDone(Sender: TObject); public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.MDone(Sender: TObject); begin SysUtils.Beep; end; procedure TForm1.FormCreate(Sender: TObject); begin Marquee1 := TddgMarquee.Create(Self); with Marquee1 do begin Parent := Self; Top := 10; Left := 10; Height := 200; Width := 150; OnDone := MDone; Show; with Items do begin Add('Cesar Franck'); Add('Vincent d''Indy'); Add('Leon Boelman'); Add('Charles M. Widor'); Add('Louis Vierne'); Add('Charles Tournemire');

502

Add('Maurice Durufle'); Add('Jehan Alain'); Add('Joseph Jongen'); Add('Marcel Dupre'); end; end; end; procedure TForm1.Button1Click(Sender: TObject); begin Marquee1.Active := True; end; procedure TForm1.Button2Click(Sender: TObject); begin Marquee1.Active := False; end; end.

W miar testowania komponentu i usuwania pojawiajcych si bdw przychodzi taki moment, gdy (odpowiedzialny) autor zaczyna nabiera przekonania o celowoci zainstalowania go w palecie. Wykonuje si to w miar atwo: po wybraniu polecenia Component|Install Component z menu gwnego IDE pojawi si okno dialogowe suce do wskazania m.in. moduu rdowego komponentu oraz pakietu, w ktrym modu ten naley umieci. Mona wskaza dowolny pakiet, przedtem jednak naley uzupeni modu Marquee.pas o procedur Register(), ktrej posta i przeznaczenie omawialimy w poprzednim rozdziale. Komponent TddgMarquee umieszczony zosta (wraz z innymi komponentami przykadowymi) w pakiecie DdgDT6 znajdujcym si (w postaci rdowej i skompilowanej) na zaczonym krku CD-ROM. Modu Marquee.pas nie zawiera procedury Register(), gdy jest ona wsplna dla wszystkich komponentw i znajduje si w module DdgReg.pas.

Tworzenie edytorw waciwoci


W poprzednim rozdziale opisalimy rne kategorie waciwoci komponentw i sposb ich edytowania w ramach inspektora obiektw. Obecnie zajmiemy si szczegami tego procesu poniewa mechanizm edycji waciwoci ma swe odzwierciedlenie w kodzie jzyka Object Pascal. Mona w zwizku z tym oczekiwa, i obiektowy model dziedziczenia okae si przydatny take i tym razem. Jak niebawem zobaczysz, oczekiwanie to jest w peni uzasadnione: jeeli predefiniowany zestaw edytorw standardowych okae si niewystarczajcy na Twoje potrzeby, bdziesz mg na ich bazie definiowa nowe edytory, wykorzystujc wszelkie ju istniejce mechanizmy. Z punktu widzenia uytkownika Delphi, edycja waciwoci w inspektorze obiektw moe odbywa si przez edycj linii tekstu lub za pomoc okna dialogowego; niektre edytory waciwoci mog czy obydwa te sposoby. Proces tworzenia nowego mechanizmu edycyjnego daje si sprowadzi do nastpujcych etapw: 1. 2. 3. 4. 5. Zdefiniowanie nowej klasy edytora jako klasy pochodnej edytora ju istniejcego. Zdefiniowanie sposobu edycji waciwoci w postaci linii tekstowej. Nieobowizkowo: zdefiniowanie sposobu edycji opartego na wykorzystaniu okna dialogowego. Ustawienie atrybutw edytora. Rejestracja nowego edytora.

Definiowanie nowej klasy edytora


Klas bazow dla wszystkich edytorw waciwoci jest klasa TPropertyEditor. Znajome z inspektora obiektw mechanizmy edycyjne s jej klasami pochodnymi; ich opis przedstawia tabela 12.1, a ich kod rdowy znajduje si w module DesignEditors.pas.

503

Tabela 12.1. Standardowe klasy edytorw waciwoci Nazwa klasy edytora


TOrdinalProperty

Przeznaczenie Abstrakcyjna klasa bazowa dla edytorw waciwoci bdcych typami porzdkowymi Object Pascala, m.in. TIntegerProperty, TEnumProperty, TCharProperty.

TIntegerProperty

Domylny edytor dla wszelkich waciwoci cakowitoliczbowych (z wyjtkiem Int64). Domylny edytor dla typu Char i jego typw okrojonych, np. A .. Z. Domylny edytor dla typw wyliczeniowych. Domylny edytor zmiennoprzecinkowych. dla waciwoci

TCharProperty

TEnumProperty TFloatProperty

TStringProperty

Domylny edytor dla waciwoci bdcych acuchami znakw. Domylny edytor dla pojedynczego elementu waciwoci zbiorowej; element traktowany jest jak zmienna typu Boolean, ktrej warto okrela jego przynaleno do odnonej waciwoci. Domylny edytor dla waciwoci zbiorowych; jego funkcjonalno sprowadza si w gwnej mierze do wspdziaania indywidualnych edytorw klasy TSetElementProperty. Domylny edytor dla waciwoci obiektowych. Domylny edytor dla waciwoci definiujcych zdarzenia, bdcych w istocie wskanikami do metod komponentu.

TSetElementProperty

TSetProperty

TClassProperty TMethodProperty

TComponentProperty

Domylny edytor dla waciwoci bdcych komponentami; umoliwia jedynie wybr komponentu (w ramach tego samego formularza), ktry zostanie przypisany do odnonej waciwoci, nie pozwalajc jednak na edycj jego poszczeglnych waciwoci, nie powinien by wic mylony z edytorem TClassProperty. Domylny edytor dla waciwoci okrelajcej kolor (klasy TColor lub pochodnej). Domylny edytor dla waciwoci okrelajcej nazw czcionki; umoliwia wybr spord nazw czcionek zainstalowanych w systemie. Domylny edytor dla waciwoci okrelajcej czcionk. Oprcz wyboru czcionki w ramach standardowego dialogu umoliwia indywidualn edycj podwaciwoci, okrelajcych rne aspekty czcionki (nazw, wysoko, kolor itp.). Jako e wywodzi si z klasy TClassProperty, umoliwia edytowanie podwaciwoci. Domylny edytor dla waciwoci typu Int64. Klasa bazowa dla edytorw elementw, odwoujcych si

TColorProperty

TFontNameProperty

TFontProperty

TInt64Property TNestedProperty

504

do

edytora

waciwoci

TNestedProperty wywodzi TSetElementProperty. TInterfaceProperty TComponentNameProperty

macierzystej; z klasy si m.in. edytor

Domylny edytor dla odwoa do interfejsw. Edytor waciwoci Name; zapobiega jej wywietlaniu, jeeli zaznaczonych jest kilka komponentw. Domylny edytor daty pochodzcej z waciwoci typu TDateTime. Domylny edytor czasu dnia pochodzcego z waciwoci typu TDateTime. Domylny edytor waciwoci typu TDateTime. Domylny edytor waciwoci wariantowych.

TDateProperty

TTimeProperty

TDateTimeProperty TVariantProperty

Definiowany przez uytkownika nowy edytor stanowi oczywicie klas pochodn w stosunku do jednej z klas wyej wymienionych, przy czym wybr odpowiedniej klasy bazowej musi by podyktowany aspektami funkcjonalnymi edytowanej waciwoci: jeeli, na przykad, wartoci waciwoci s wycznie liczby parzyste (notabene nie da si w Pascalu zdefiniowa typu obejmujcego liczby parzyste), to najrozsdniejszym sposobem tworzenia jej edytora jest wzbogacenie klasy TIntegerProperty w dodatkowe mechanizmy weryfikacyjne. Dla edytorw w wysokim stopniu nietypowych moe jednak okaza si konieczna praca od podstaw, tj. definiowanie nowej klasy na bazie klasy TPropertyEditor.

Wskazwka

W wielu przypadkach standardowe edytory okazuj si wystarczajce, nawet jeeli w pierwszej chwili mogoby si wydawa inaczej. Na przykad dla typw okrojonych wystarczajce s edytory typw macierzystych typ 1..10 moe wic by edytowany za pomoc edytora TIntegerProperty (wiadomego granic typu okrojonego), dla typw wyliczeniowych automatycznie udostpniana jest rozwijalna lista elementw, itp. Przed przystpieniem do definiowania wasnego edytora naley wic wpierw dokadnie zidentyfikowa typ edytowanej waciwoci.

Edycja waciwoci w postaci linii tekstu


Moliwo edycji waciwoci w postaci linii tekstu jest obowizkowa dla kadego edytora, poniewa jest narzucona przez logik inspektora obiektw; edycja z wykorzystaniem okna dialogowego jest oczywicie nieobowizkowa. Waciwoci edytora, odpowiedzialn za reprezentacj znakow edytowanej waciwoci, jest Value wraz ze swymi metodami dostpowymi GetValue() oraz SetValue(). Oto przykad jej implementacji dla klasy TIntegerProperty:
TIntegerProperty = class(TOrdinalProperty) public function GetValue: string; override; procedure SetValue(const Value: string); override; end; procedure TIntegerProperty.SetValue(const Value: String); procedure Error(const Args: array of const); begin raise EPropertyError.CreateResFmt(@SOutOfRange, Args); end; var L: Int64;

505

begin L := StrToInt64(Value); with GetTypeData(GetPropType)^ do if OrdType = otULong then begin // weryfikacja waciwoci Cardinal if (L < Cardinal(MinValue)) or (L > Cardinal(MaxValue)) then // rozszerz do typu Int64 w celu poprawnego wywietlenia wartoci Error([Int64(Cardinal(MinValue)), Int64(Cardinal(MaxValue))]); end else if (L < MinValue) or (L > MaxValue) then Error([MinValue, MaxValue]); SetOrdValue(L); end;

Typ Integer zalicza si do typw porzdkowych, nic wic dziwnego, e edytor waciwoci cakowitoliczbowych TIntegerProperty wywodzi si z edytora TOrdinalProperty, przeznaczonego dla dowolnego typu porzdkowego. Metoda GetValue() jest banalnie prosta zwraca po prostu znakow reprezentacj waciwoci traktowanej jako liczba cakowita. Procedura SetValue() dokonuje wpierw weryfikacji, czy tekst zawarty w linii wejciowej identyfikuje jak liczb cakowit. Dodatkowa weryfikacja dotyczy zakresu wprowadzanej liczby, okrelonego przez typ edytowanej waciwoci; w przypadku przekroczenia dopuszczalnego zakresu generowany jest wyjtek. Klasa TPropertyEditor() definiuje kilka metod podobnych do GetValue()/SetValue(), a przeznaczonych do konwersji rnych typw waciwoci na posta znakow (i odwrotnie); klasy potomne edytorw dziedzicz oczywicie te metody, co dla projektanta jest do duym uatwieniem. Zestawiamy je w tabeli 12.2.

Tabela 12.2. Metody dokonujce znakowej konwersji waciwoci rnych typw Typ waciwoci Zmiennoprzecinkowa Zdarzeniowa Porzdkowa String Variant Metoda udostpniajcaMetoda interpretujca posta posta znakow znakow
GetFloatValue() GetMethodValue() GetOrdValue() GetStrValue() GetVarValue() SetFloatValue() SetMethodValue() SetOrdValue() SetStrValue() SetVarValue(), SetVarValueAt()

Tworzenie nowego edytora waciwoci zilustrujemy na przykadzie opisywanego w poprzednim rozdziale komponentu TddgPlanets, reprezentujcego Ukad Soneczny. Jego waciwo tablicowa PlanetName zawiera indeks wskazujcy aktualnie wybran planet w tablicy nazw planet. Elegancja Delphi wymaga jednak, aby w czasie edycji tej waciwoci w inspektorze obiektw moliwe byo operowanie nazwami planet obok moliwoci operowania ich indeksami; naley przy tym uwzgldni moliwo wpisywania tej samej nazwy pod rnymi postaciami WENUS, Wenus, wenus itp. Ponadto nazwa planety i odpowiadajcy jej indeks powinny by traktowane rwnorzdnie. Przyjrzyjmy si definicji typu waciwoci PlanetName:
type TPlanetName = type Integer; TddgPlanet = class(TComponent) private FPlanetName: TPlanetName; published property PlanetName: TPlanetName read FPlanetName write FPlanetName; end;

506

Zwr uwag na aliasowanie typu jego zadaniem jest w Object Pascalu realizacja zasady taki sam, cho nie ten sam. Wartoci typu TPlanetName s liczby cakowite, wic jest on taki sam, jak typ Integer; wymaga jednak innej interpretacji zwaszcza w kontekcie edytowania waciwoci nie jest wic ju ten sam. Opisany przed chwil scenariusz edycji waciwoci PlanetName nie wynika oczywicie z definicji samego komponentu TddgPlanet, lecz z klasy jej edytora, ktrej definicj przedstawiamy na wydruku 12.4. Wydruk 12.4. Kod rdowy edytora waciwoci typu TPlanetName TPlanetNameProperty
unit PlanetPE; interface uses Windows, SysUtils, DesignEditors; type TPlanetNameProperty = class(TIntegerProperty) public function GetValue: string; override; procedure SetValue(const Value: string); override; end; implementation const { nazwy kolejnych planet } PlanetNames: array[1..9] of String[7] = ('Merkury', 'Wenus', 'Ziemia', 'Mars', 'Jowisz', 'Saturn', 'Uran', 'Neptun', 'Pluton');

function TPlanetNameProperty.GetValue: string; begin Result := PlanetNames[GetOrdValue]; end; procedure TPlanetNameProperty.SetValue(const Value: String); var PName: string[7]; i, ValErr: Integer; begin PName := UpperCase(Value); i := 1; { znajd pozycj odpowiadajc nazwie zawartej w Value } while (PName <> UpperCase(PlanetNames[i])) and (i <= 9) do inc(i); if i <= 9 then // podano prawidow nazw planety begin SetOrdValue(i); Exit; end else begin { Nie podano poprawnej nazwy planety, ale by moe podano jej INDEKS } Val(Value, i, ValErr); if ValErr <> 0 then // ewidentny bd raise Exception.Create(Format('Nie syszaem nigdy o planecie %s.', [Value])); if (i < 1) or (i > 9) then raise Exception.Create('Takiej planety nie ma w NASZYM Ukadzie Sonecznym.'); SetOrdValue(i); end; end; end.

507

Przeanalizujmy pokrtce tre wydruku. Jako e typ waciwoci PlanetName jest aliasem typu Integer, jej edytor waciwoci wywodzi si z klasy TIntegerProperty. Zadaniem metody GetValue() jest udostpnienie okrelonej nazwy na podstawie indeksu. Indeks ten musi znajdowa si w zakresie 1 9, co z kolei zapewnia metoda SetValue(). Metoda SetValue() jest nieco bardziej skomplikowana. Poniewa zaoylimy, e wielko liter w nazwie planety nie odgrywa adnej roli, przeto nazwa wpisana przez uytkownika jest wstpnie normalizowana poprzez zamian na due litery; dotyczy to rwnie wzorcowych nazw w tablicy. Na zasadzie porwnywania wprowadzonej nazwy z nazwami wzorcowymi podejmowana jest prba znalezienia jej w tablicy; w przypadku powodzenia stosowny indeks podstawiany jest jako warto waciwoci. Nieznalezienie nazwy w tablicy nie musi jeszcze oznacza bdu. Przyjmuje si wic (w dobrej wierze), e uytkownik zamiast nazwy planety wprowadzi jej indeks; jest tak, jeeli zawarto linii wejciowej da si zinterpretowa jako liczba z zakresu 19. W przypadku pomylnej konwersji warto ta jest przyjmowana za warto waciwoci, w przeciwnym razie generowany jest stosowny wyjtek. To ju prawie wszystko; pozostaje tylko zaznajomienie inspektora obiektw z now klas edytora.

Rejestracja nowego edytora waciwoci


Aby nowy edytor waciwoci dostpny by dla inspektora obiektw, naley dokona jego rejestracji w systemie. Zadanie to spenia procedura RegisterPropertyEditor(), zdefiniowana nastpujco:
procedure RegisterPropertyEditor( PropertyType: PTypeInfo; ComponentClass: TClass; const PropertyName: string; EditorClass: TPropertyEditorClass);

Pierwszy parametr to wskanik do informacji o typie edytowanej waciwoci w strukturze RTTI; informacj t udostpnia funkcja TypeInfo(). Drugi i trzeci parametr okrelaj klas i nazw jej waciwoci, do ktrych inspektor obiektw bdzie stosowa rejestrowany wanie edytor; ostatni, czwarty parametr, okrela nazw rejestrowanego edytora. Dla edytora TPlanetNameProperty wywoanie rejestrujce wyglda bdzie nastpujco:

RegisterPropertyEditor(TypeInfo(TPlanetName), TddgPlanet, 'PlanetName', TPlanetNameProperty);

Wskazwka

Dla uproszczenia przykadu edytor TPlanetNameProperty rejestrowany jest dla konkretnej waciwoci w konkretnej klasie. Okrelajc klas komponentu (ComponentClass) jako NIL i podajc w miejsce nazwy waciwoci (PropertyName) pusty acuch spowodujemy, i inspektor obiektw bdzie uywa (domylnie) tego edytora dla kadej waciwoci typu TPlanetName, niezalenie od jej nazwy i klasy komponentu.

naszym

przykadowym

zestawie

komponentw

na

zaczonym

krku

CD-ROM

edytor

TPlanetNameProperty rejestrowany jest wraz z komponentami w module DdgReg.Pas. Rejestrowanie

komponentw wraz z dotyczcymi ich edytorami jest posuniciem susznym koncepcyjnie, lecz wie si ze wzrostem rozmiaru pakietu; naley pamita, i edytory waciwoci komponentu bywaj znacznie bardziej obszerne i skomplikowane ni sam komponent. Wszystko za, co deklarowane jest w sekcji publicznej (interface) moduu komponentu (np. procedura Register()) i wszystkie powizane z tym elementy staj si czci pakietu. W przypadku zoonych komponentw wskazane jest wic rejestrowanie ich edytorw w oddzielnych moduach.

508

Edycja waciwoci za pomoc okna dialogowego


W bardzo wielu przypadkach jednolinijkowa edycja waciwoci, nawet wspomagana dodatkowymi mechanizmami, okazuje si niewystarczajca. Jako przykad przytoczmy edycj czcionki (waciwo typu TFont) poszczeglne cechy czcionki dadz si co prawda wyrazi w postaci znakowej i wpisa w stosowne pola tekstowe, ale byoby niewtpliwie w zym gucie zmusza uytkownika do ich zapamitywania; programy przyjazne uytkownikowi preferuj raczej wybr spord potencjalnych moliwoci, wpisywanie pozostawiajc jako ostateczno. Zgodnie z t zasad inspektor obiektw umoliwia niestandardow edycj waciwoci za pomoc dialogu zaprojektowanego przez twrc komponentu (a raczej edytora tego komponentu); wybr czcionki dla waciwoci typu TFont jest tego sztandarowym przykadem zastosowano tutaj jeden ze standardowych dialogw.

Przykad: Edycja wiersza polece komponentu TddgRunButton


W poprzednim rozdziale opisywalimy komponent sucy do uruchamiania innych aplikacji TddgRunButton. Polecenie systemowe, powodujce uruchomienie aplikacji, stanowio zawarto jego waciwoci TCommandLine, bdcej acuchem znakw; dla wyranego wyodrbnienia typu waciwoci, jej typ zosta jednak zadeklarowany jako alias:
TCommandLine = type string;

W starym poczciwym DOS-ie polecenia dzieliy si na dwie kategorie: pierwsz stanowiy polecenia wewntrzne, nieodcznie zwizane z logik systemu (np. DIR, TYPE), natomiast polecenia drugiej grupy powodoway uruchomienie programw zawartych w plikach wykonywalnych, ktrych nazwy podawane byy jako tre polecenia. Wraz z pojawieniem si Windows pierwsza grupa polece stracia sw racj bytu (oczywicie z wyjtkiem sesji dosowych), druga grupa zostaa natomiast rozbudowana o mechanizm skojarze plikw z programami (klikajc plik *.DOC powodujemy uruchomienie Worda); standardy Windows wymagaj, by podanie nazwy pliku z rozszerzeniem stanowio polecenie uruchomienia skojarzonego z tym plikiem programu. Ponadto, poniewa wybr spord gotowych pozycji (plikw) jest dla uytkownika znacznie wygodniejszy ni konieczno wpisywania (nazwy pliku), oczywistym wymogiem wydaje si uzupenienie edycji wiersza polece o moliwo wyboru pliku za pomoc jednego ze standardowych dialogw. Tre moduu implementujcego wspomniany edytor (TCommandLineProperty) przedstawiamy na wydruku 12.5; jak pamitamy, komponent TddgRunButton posiada pewne mechanizmy weryfikacji poprawnoci wprowadzanego polecenia, nie ma wic potrzeby powiela ich w tworzonym edytorze. Wydruk 12.5. Implementacja edytora TCommandLineProperty
unit runbtnpe; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons, DesignEditors, DesignIntf, TypInfo; type { dziedziczenie z klasy TStringProperty - wiersz polece jest acuchem } TCommandLineProperty = class(TStringProperty) function GetAttributes: TPropertyAttributes; override; procedure Edit; override; end; implementation function TCommandLineProperty.GetAttributes: TPropertyAttributes; begin Result := [paDialog]; // edycja dialogowa end; procedure TCommandLineProperty.Edit; { W ramach edycji dialogowej uytkownik wybiera plik do wykonania ze standardowego okna otwarcia pliku }

509

var OpenDialog: TOpenDialog; begin { Utwrz obiekt TOpenDialog } OpenDialog := TOpenDialog.Create(Application); try { poka tylko pliki wykonywalne } OpenDialog.Filter := 'Pliki wykonywalne|*.EXE'; { gdy uytkownik wybierze plik, przypisz jego nazw do edytowanej waciwoci } if OpenDialog.Execute then SetStrValue(OpenDialog.FileName); finally OpenDialog.Free // Zwolnij obiekt TOpenDialog end; end;

end.

Poniewa wiersz polece jest w swej istocie acuchem znakw, przeto opisywany edytor wywodzi si z klasy TStringProperty. Z klasy tej dziedziczone s metody GetValue() i SetValue(), bo nasz nowy edytor nie wnosi nic nowego do edytowania wiersza polece jako linii tekstu. Za uruchomienie edycji dialogowej odpowiedzialna jest metoda GetAttributes(), a raczej zwracana przez ni warto stanowica zbir atrybutw. Atrybuty edytora waciwoci Jak ju pisalimy wczeniej, kady edytor waciwoci musi realizowa funkcj edycji waciwoci w postaci tekstowej. Niektre edytory mog jednake realizowa inne funkcje edycyjne, wane wic jest, by inspektor obiektw by o tych funkcjach poinformowany. Informacj tak udostpniaj atrybuty edytora, ktrych zbir przekazywany jest jako wynik metody GetAttributes(). Znaczenie poszczeglnych atrybutw edytora waciwoci przedstawia tabela 12.3.

Tabela 12.3. Atrybuty edytora waciwoci Atrybut


paValueList

Znaczenie Edytor umoliwia wybr spord oferowanych moliwoci; ich lista budowana jest za pomoc metody GetValues(); przykadem takiej waciwoci jest TForm.BorderStyle oraz grupa staych waciwoci TColor, czy te TCharSet. Edytor umoliwia wybranie (do dalszej edycji) podwaciwoci, wywietlanych poniej waciwoci oryginalnej i z niewielkim przesuniciem w prawo. Atrybut ten moe by ustawiony tylko cznie z paValueList. Przykadem waciwoci edytowanych w ten sposb s TOpenDialog.Options i TForm.Font. Z prawej strony pola wartoci waciwoci pojawia si przycisk z wielokropkiem (ellipsis), ktrego kliknicie powoduje wywoanie metody Edit(). W ten sposb edytowana jest np. waciwo Font. Umoliwia edycj danej waciwoci dla kilku komponentw jednoczenie (jednoczesne zaznaczenie kilku komponentw na formularzu nastpuje m.in. w wyniku zakrelenia mysz prostoktnego obszaru obejmujcego wybrane komponenty, przy nacinitym lewym przycisku). Nie mog by w ten sposb edytowane waciwoci o unikatowej (z zaoenia) wartoci dla kadego egzemplarza komponentu na przykad waciwo Name, nie naley

paSubProperties

paDialog

paMultiSelect

510

wic opatrywa ich edytorw atrybutem paMultiSelect waciwo tego atrybutu, ktrej edytor nie posiada nie zostanie udostpniona przez inspektor obiektw podczas edycji grupowej.
paAutoUpdate

Powoduje wywoywanie metody SetValue() po kadej zmianie edytowanej linii; przy braku atrybutu paAutoUpdate metoda SetValue() wywoywana jest dopiero po koczcym edycj naciniciu klawisza ENTER. Przykadem waciwoci opisywanego typu jest Caption jakiekolwiek jej zmiany widoczne s natychmiast w tytule aktywnego komponentu.

paFullWidthName

W inspektorze obiektw nie pojawia si warto waciwoci ca szeroko okna zajmuje jej nazwa. Inspektor obiektw dokonuje sortowania listy zbudowanej przez GetValues(). Niedopuszczalna jest zmiana wartoci waciwoci. Umoliwia przywrcenie waciwoci oryginalnej wartoci; edytory waciwoci zagniedonych i waciwoci zbiorowych nie powinny posiada tego atrybutu.

paSortList

paReadOnly paRevertable

paVolatileSubPrope Jakakolwiek zmiana wartoci waciwoci wymaga rties ponownego przeliczenia jej podwaciwoci zostan

one w tym celu zwinite i rozwinite ponownie.


paVCL

Edytor waciwoci jest komponentem biblioteki VCL (nie CLX). Inspektor obiektw nie wywietla tej waciwoci, jeli jest ona waciwoci zagniedon (podwaciwoci innej waciwoci).

paNotNestable

Wskazwka

Pouczajcym dowiadczeniem moe by przeanalizowanie moduu DesignEditors.Pas i sprawdzenie zestawu atrybutw w poszczeglnych klasach edytorw waciwoci.

wic edytor TCommandLineProperty realizuje funkcj penego dialogu, jego metoda GetAttributes() musi zwraca co najmniej flag paDialog. Powoduje ona, i obok waciwoci CommandLine pojawi si przycisk z wielokropkiem (ellipsis); kliknicie go spowoduje wywoanie metody Edit() realizujcej wspomniany dialog tu dialog otwarcia pliku. Rejestracja edytora Edytor TCommandLineProperty rejestrowany jest w module DdgReg.Pas wraz z komponentem TddgRunButton:
RegisterPropertyEditor(TypeInfo(TCommandLine), TddgRunButton, '', TCommandLineProperty);

Skoro

Jak atwo zauway, jest on rejestrowany dla dowolnej waciwoci komponentu TddgRunButton, ktra ma typ
TCommandLine.

511

Edytory komponentw
Projektowanie przez uytkownika niestandardowych sposobw edycji waciwoci komponentw nie wyczerpuje bynajmniej moliwoci Delphi w zakresie konfigurowania komponentw. Uytkownik ma rwnie wpyw (jeeli oczywicie tego chce) na caociowe zachowanie si komponentw w trakcie projektowania aplikacji: moe okrela zawarto menu kontekstowego (udostpnianego w wyniku kliknicia prawym przyciskiem myszy) oraz znaczenie jego opcji, moe rwnie zdefiniowa akcj powodowan przez dwukrotne kliknicie. Zachowanie si takich komponentw, jak TTable, TQuery i TStoredProc jest tego wymownym przykadem.

TComponentEditor
Nie kady uytkownik Delphi wiadom jest faktu, e wraz z wybraniem komponentu na formularzu tworzony jest obiekt stowarzyszonego z nim edytora. Wszystkie edytory komponentw wywodz si z klasy TComponentEditor, zadeklarowanej w module DesignEditors.pas nastpujco:
TComponentEditor = class(TBaseComponentEditor, IComponentEditor) private FComponent: TComponent; FDesigner: IDesigner; public constructor Create(AComponent: TComponent; ADesigner: IDesigner); override; procedure Edit; virtual; procedure ExecuteVerb(Index: Integer); virtual; function GetComponent: TComponent; function GetDesigner: IDesigner; function GetVerb(Index: Integer): string; virtual; function GetVerbCount: Integer; virtual; function IsInInlined: Boolean; procedure Copy; virtual; procedure PrepareItem(Index: Integer; const AItem: IMenuItem); virtual; property Component: TComponent read FComponent; property Designer: IDesigner read GetDesigner; end;

Oto krtki przegld waniejszych waciwoci i metod.

Waciwoci
Edytowany komponent reprezentowany jest przez waciwo Component. Deklarowanym jej typem jest TComponent, zatem odwoywanie si (w treci metod edytora) do edytowanego komponentu wymaga zazwyczaj rzutowania typu. Waciwo Designer stanowi wskazanie na interfejs IDesigner stanowicy cznik tworzonej aplikacji z projektantem formularzy. Definicja interfejsu IDesigner znajduje si w module DesignEditors.pas.

Metody
Metoda Edit() wywoywana jest w rezultacie dwukrotnego kliknicia wybranego komponentu; domylnym jej dziaaniem jest realizacja pierwszej opcji menu kontekstowego komponentu (ExecuteVerb(0)), o ile opcje takie w ogle istniej (GetVerbCount() > 0). Jeeli w czasie edycji komponentu nastpia np. zmiana ktrego z jego pl, konieczne jest powiadomienie o tym projektanta formularzy, za pomoc wywoania Designer.Modified. Wystpujcy w nazwach wspomnianych metod czon verb (czasownik) odnosi si do konkretnych akcji, ktre mog by wykonywane na edytowanym komponencie przez jego edytor; akcje te reprezentowane s przez kolejne opcje menu kontekstowego zwizanego z edytowanym komponentem. Informacje o dostpnych akcjach udostpniane s przez kilka metod edytora. I tak, metoda GetVerbCount() zwraca liczb akcji edycyjnych, za metoda GetVerb() zwraca tekst reprezentujcy akcj (o wskazanym indeksie) w menu kontekstowym.

512

Po wybraniu ktrej ze wspomnianych opcji menu kontekstowego wywoywana jest metoda ExecuteVerb() z indeksem opcji jako parametrem (pierwsza opcja ma indeks 0). W tej metodzie ukrywa si wic zdecydowana wikszo kodu okrelajcego specyficzny scenariusz poszczeglnych wariantw edycji komponentu. Metoda Copy() wywoywana jest kadorazowo po zakoczeniu kopiowania komponentu do schowka. Umoliwia ona wzbogacenie obrazu komponentu w schowku w dodatkowe dane, ktre bd co prawda zignorowane przez projektanta formularzy, ale mog mie znaczenie dla innych aplikacji. Domylnie tre tej metody jest pusta.

TDefaultEditor
Jeeli dla danej klasy komponentu nie zostanie zarejestrowany dedykowany jej edytor, to komponent bdzie obsugiwany przez edytor standardowy, reprezentowany przez klas TDefaultEditor. Klasa ta przedefiniowuje standardow metod TComponentEditor.Edit() w ten sposb, i dwukrotne kliknicie komponentu, zamiast wywoania ExecuteVerb(0) spowoduje wywoanie metody Edit() edytora waciwoci1 zdarzeniowej OnCreate, OnChange lub OnClick zalenie od tego, ktre z nich napotkane zostanie najwczeniej w deklaracji klasy; jeeli nie zostanie napotkane adne z nich, uwzgldniana jest pierwsza napotkana waciwo zdarzeniowa.

Przykad edycji komponentu


Rozpatrzmy nastpujcy komponent:
type TComponentEditorSample = class(TComponent) protected procedure SayHello; virtual; procedure SayGoodbye; virtual; end; procedure TComponentEditorSample.SayHello; begin MessageDlg('Witam! Co sycha ?', mtInformation, [mbOk], 0); end; procedure TComponentEditorSample.SayGoodbye; begin MessageDlg('Do miego zobaczenia!', mtInformation, [mbOk], 0); end;

Definicja komponentu nie wymaga komentarza; aby uczyni go nieco ciekawszym, skonstruujmy dla niego specjalizowany edytor. Jak przed chwil wyjanialimy, edytor taki musi implementowa co najmniej trzy metody: GetVerbCount(), GetVerb() i ExecuteVerb(). Dla naszego komponentu przewidzielimy dwie akcje edycyjne, polegajce na wywoaniu jego metod (odpowiednio) SayHello i SayGoodBye:

type TSampleEditor = class(TComponentEditor) private procedure ExecuteVerb(Index: Integer); override; function GetVerb(Index: Integer): string; override; function GetVerbCount: Integer; override; end; ... procedure TSampleEditor.ExecuteVerb(Index: Integer); begin case Index of 0: TComponentEditorSample(Component).SayHello;

Nie myli z metod Edit() edytora komponentu (przyp. tum.).

513

1: TComponentEditorSample(Component).SayGoodbye; end; end; function TSampleEditor.GetVerb(Index: Integer): string; begin case Index of 0: Result := 'Przywitanie'; 1: Result := 'Poegnanie'; end; end; function TSampleEditor.GetVerbCount: Integer; begin Result := 2; // liczba dodatkowych opcji menu kontekstowego end; end.

Metoda GetVerbCount() zwraca oczywicie warto 2, za etykiety opcji dla obydwu akcji okrelone s przez metod GetVerb(). Kada z akcji polega na wywoaniu odpowiedniej metody edytowanego komponentu on sam dostpny jest za porednictwem waciwoci Component edytora; zwr uwag na niezbdne rzutowanie jej typu na typ edytowanego komponentu.

Rejestracja edytora komponentu


Podobnie jak komponenty i edytory ich waciwoci, take edytory komponentw wymagaj rejestracji w rodowisku IDE. Rejestracj edytora komponentu wykonuje procedura o nazwie RegisterComponentEditor, zdefiniowana nastpujco:
procedure RegisterComponentEditor(ComponentClass:TComponentClass; ComponentEditor: TComponentEditorClass);

Pierwszy parametr jej wywoania okrela klas edytowanego komponentu, drugi natomiast klas rejestrowanego edytora. Kompletny modu, zawierajcy definicj komponentu TComponentEditorSample i jego edytora TSampleEditor, wraz z niezbdn procedur rejestracyjn, jest przedstawiony na wydruku 12.6.

Wydruk 12.6. Przykad rejestracji komponentu i jego edytora


unit CompEdit; interface uses SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs, DesignIntf; type TComponentEditorSample = class(TComponent) protected procedure SayHello; virtual; procedure SayGoodbye; virtual; end; TSampleEditor = class(TComponentEditor) private procedure ExecuteVerb(Index: Integer); override; function GetVerb(Index: Integer): string; override; function GetVerbCount: Integer; override; end; procedure Register; implementation { TComponentEditorSample } procedure TComponentEditorSample.SayHello; begin MessageDlg('Witam! Co sycha ?', mtInformation, [mbOk], 0);

514

end; procedure TComponentEditorSample.SayGoodbye; begin MessageDlg('Do miego zobaczenia!', mtInformation, [mbOk], 0); end; { TSampleEditor } const vHello = 'Przywitanie'; vGoodbye = 'Poegnanie'; procedure TSampleEditor.ExecuteVerb(Index: Integer); begin case Index of 0: TComponentEditorSample(Component).SayHello; 1: TComponentEditorSample(Component).SayGoodbye; end; end; function TSampleEditor.GetVerb(Index: Integer): string; begin case Index of 0: Result := vHello; 1: Result := vGoodbye; end; end; function TSampleEditor.GetVerbCount: Integer; begin Result := 2; end; procedure Register; begin RegisterComponents('Samples', [TComponentEditorSample]); RegisterComponentEditor(TComponentEditorSample, TSampleEditor); end; end.

Przechowywanie w strumieniach niepublikowanych danych komponentu


Jak miae okazj przeczyta w poprzednim rozdziale, Delphi automatycznie zapisuje w pliku .DFM i odczytuje z niego obraz opublikowanych (published) waciwoci komponentu. Za chwil pokaemy, w jaki sposb mona uruchomi zapis i odczyt rwnie danych niepublikowanych.

Okrelenie strumieniowalnego charakteru waciwoci


Odzwierciedleniem struktury komponentu pod wzgldem obrazu jego zawartoci w strumieniu jest jego metoda DefineProperties(). Pojawia si ona po raz pierwszy w klasie TPersistent jako metoda nie posiadajca treci; jej stosowne przedefiniowywanie w klasach pochodnych umoliwia przechowywanie obiektw tych klas w strumieniu.
procedure DefineProperties (Filer: TFiler); virtual;

Jedynym parametrem wywoania metody DefineProperties() jest obiekt klasy TFiler, symbolizujcej oglnie pojt wymian danych pomidzy komponentem a strumieniem zawierajcym jego obraz. Wrd elementw dziedziczonych przez dan klas z jej klasy macierzystej znajduje si zazwyczaj jej metoda DefineProperties(), tak wic przedefiniowujc t ostatni w klasie pochodnej nie zapominaj o wywoaniu jej odziedziczonej postaci (inherited).

515

Po wywoaniu metody odziedziczonej nastpuje okrelenie scenariusza zapisu (odczytu) waciwoci charakterystycznego dla klasy pochodnej. Scenariusz ten skada si z nastpujcych kolejno wywoa metod obiektu TFiler:
procedure DefineProperty(const Name: string; ReadData:TReaderProc; WriteData: TWriterProc; HasData: Boolean);virtual; procedure DefineBinaryProperty(const Name: string; ReadData, WriteData: TStreamProc; HasData: Boolean);virtual;

Kade z tych wywoa dotyczy jednej z waciwoci klasy (lub jednego z jej pl), przy czym
DefineProperty() odnosi si do standardowych typw danych, jak acuchy, liczby cakowite, wartoci boolowskie, znaki, liczby zmiennoprzecinkowe i typy wyliczeniowe, za DefineBinaryProperty() dotyczy

niesformatowanych (amorficznych) danych binarnych, jak grafika czy materia dwikowy. W obydwu przypadkach pierwszy parametr wywoania zawiera nazw, ktra identyfikowa bdzie przechowywan waciwo w strumieniu nazwa ta nie musi by identyczna z oryginaln nazw waciwoci w deklaracji, musi by jednak unikatowa dla kadej waciwoci, wcznie z waciwociami odziedziczonymi. Parametry ReadData i WriteData reprezentuj procedury transmisyjne, dokonujce fizycznego odczytu/zapisu waciwoci ze/do strumienia, natomiast ostatni parametr HasData jest wyraeniem boolowskim okrelajcym warunek, ktry musi by speniony, by dana waciwo w ogle bya reprezentowana w strumieniu.

Dla danych sformatowanych przewidziano dwa rodzaje procedur transmisyjnych, osobno dla odczytu i zapisu:
Type TReaderProc = procedure(Reader: TReader) of object; TWriterProc = procedure(Writer: TWriter) of object;

Jak atwo zauway, procedury transmisyjne musz by metodami jakiego obiektu, nie za niezalenymi procedurami. Klasy TReader i TWriter s pochodnymi klasy TFiler i zawieraj dodatkowe metody wspomagajce (odpowiednio) odczyt i zapis standardowych typw danych. Dla danych amorficznych przewidziano tylko jeden rodzaj procedury transmisyjnej, wsplny dla odczytu i zapisu:
Type TStreamProc = procedure (Stream: TStream) of object;

Parametrem jej wywoania jest abstrakcyjny obiekt reprezentujcy strumie danych; daje to uytkownikowi nieskrpowan moliwo operowania caymi obszarami surowych danych, za pomoc odpowiednich metod klasy TStream.

Przykad uycia DefineProperty()


Na wydruku 12.7 znajduje si kod moduu implementujcego przykadowy komponent, przechowujcy w strumieniu zawarto dwch prywatnych pl, bdcych (kolejno) acuchem (FString) i liczb cakowit (FInteger). Wydruk 12.7. Przykad zastosowania DefineProperty()
unitDefProp; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs; type TDefinePropTest = class(TComponent) private FString: String; FInteger: Integer; procedure ReadStrData(Reader: TReader); procedure WriteStrData(Writer: TWriter); procedure ReadIntData(Reader: TReader); procedure WriteIntData(Writer: TWriter); protected procedure DefineProperties(Filer: TFiler); override;

516

public constructor Create(AOwner: TComponent); override; end; implementation constructor TDefinePropTest.Create(AOwner: TComponent); begin inherited Create(AOwner); { Nadaj polom danych przykadow zawarto } FString := 'Wynikiem dziaania jest...'; FInteger := 42; end; procedure TDefinePropTest.DefineProperties(Filer: TFiler); begin inherited DefineProperties(Filer); { wywoania definiujce sposb zapisu i odczytu pl} Filer.DefineProperty('StringProp', ReadStrData, WriteStrData, FString <> ''); Filer.DefineProperty('IntProp', ReadIntData, WriteIntData, True); end; { Metody transmisyjne } procedure TDefinePropTest.ReadStrData(Reader: TReader); // odczyt pola FString; begin FString := Reader.ReadString; end; procedure TDefinePropTest.WriteStrData(Writer: TWriter); // zapis pola FString; begin Writer.WriteString(FString); end; procedure TDefinePropTest.ReadIntData(Reader: TReader); // odczyt pola FInteger; begin FInteger := Reader.ReadInteger; end; procedure TDefinePropTest.WriteIntData(Writer: TWriter); // zapis pola FInteger; begin Writer.WriteInteger(FInteger); end; end.

W powyszym przykadzie odziedziczona metoda DefineProperties() uzupeniona zostaa dwoma wywoaniami DefineProperty(). Pierwsze z tych wywoa informuje, i w strumieniu przechowywana bdzie porcja danych identyfikowana przez nazw StringProp; za jej odczyt (odpowiednio: zapis) odpowiedzialne bd metody ReadStrData (odpowiednio: WriteStrData). Porcji tej nie naley jednak tworzy, jeeli pole FString zawiera acuch pusty. Na podobnej zasadzie drugie ze wspomnianych wywoa definiuje tworzon bezwarunkowo porcj o nazwie IntProp, obsugiwan przez metody ReadIntData i WriteIntData.

Ostrzeenie

Naley uwaa, by nie pomyli metod ReadString() i WriteString() klas TReader i TWriter z podobnie brzmicymi metodami ReadStr() i WriteStr(); gdy spowodowaoby to zniszczenie pliku .DFM.

517

Przykad uycia DefineBinaryProperty() komponent TddgWaveFile


Jak ju wczeniej wspominalimy, przykadem danych niesformatowanych, predestynowanych do obsugi za pomoc metody DefineBinaryProperty(), s rnego rodzaju wzorce graficzne oraz dwikowe i rzeczywicie: znakomita wikszo komponentw VCL wykorzystuje t metod do przechowywania ikon swych komponentw, e wspomnimy tu o waciwoci Icon formularza albo waciwoci Glyph przycisku TBitBtn.
Notatka

Komponent TddgWaveFile jest komponentem w peni funkcjonalnym, posiada m.in. specjalizowany edytor pozwalajcy na przesuchanie przypisanego wzorca dwikowego podczas projektowania aplikacji, bez koniecznoci jej uruchamiania. Jego moliwociami zajmiemy si nieco pniej, obecnie skoncentrujemy si na mechanizmie przechowywania wzorca dwikowego w strumieniu.

Metoda DefineBinaryPropety() nie rozrnia (w przeciwiestwie do metody DefineProperty()) typu funkcji odczytujcej i zapisujcej obydwie te funkcje posiadaj wsplny typ (TStreamProc) i s metodami klasy TStream, reprezentujcej strumie danych.

Metoda DefineProperties() komponentu TddgWaveFile zdefiniowana jest nastpujco:


procedure TddgWaveFile.DefineProperties(Filer: TFiler); { definiuje materia dwikowy, wskazywany przez pole FData, jako porcj danych niesformatowanych okrelanych nazw "Data" } function DoWrite: Boolean; begin if Filer.Ancestor <> nil then Result := not (Filer.Ancestor is TddgWaveFile) or not Equal(TddgWaveFile(Filer.Ancestor)) else Result := not Empty; end;

begin inherited DefineProperties(Filer); Filer.DefineBinaryProperty('Data', ReadData, WriteData, DoWrite); end;

Jak wida, definiowana jest tu porcja danych identyfikowana przez nazw Data, obsugiwana przez procedury transmisyjne ReadData() i WriteData(); jej obecno w strumieniu zalena jest od wyniku zwracanego przez funkcj DoWrite(), ktr zajmiemy si za chwil.

Dziaanie ReadData i WriteData ogranicza si do (odpowiednio) odczytu danych komponentu ze strumienia i zapisu danych w strumieniu:
procedure TddgWaveFile.ReadData(Stream: TStream); { odczytuje dane ze strumienia } begin LoadFromStream(Stream); end;

procedure TddgWaveFile.WriteData(Stream: TStream); { zapisuje dane komponentu do strumienia }

518

begin SaveToStream(Stream); end;

Metoda LoadFromStream() sprawdza wpierw, czy do komponentu zostay ju zaadowane jakie dane; jeeli jest tak istotnie, zwalnia przydzielon dla nich pami, przydziela j ponownie w rozmiarze odpowiadajcym rozmiarowi strumienia i wczytuje do niej dane zawarte w strumieniu:
procedure TddgWaveFile.LoadFromStream(S: TStream); { Wczytuje dane ze strumienia, zwalniajc pami zajt przez ew. dane zaadowane wczeniej } begin if not Empty then FreeMem(FData, FDataSize); FDataSize := 0; FData := AllocMem(S.Size); FDataSize := S.Size; S.Read(FData^, FDataSize); end;

Metoda SaveToStream() jest nieco prostsza: zapisuje dane w strumieniu, sprawdziwszy jednak uprzednio, czy w ogle jest co zapisywa:
procedure TddgWaveFile.SaveToStream(S: TStream); { Zapisuje dane do strumienia } begin if not Empty then S.Write(FData^, FDataSize); end;

Funkcja DoWrite(), decydujca o tym, czy dane maj by w ogle zapisywane, rozwizuje tu pewien problem, ktry pojawi si wraz z moliwoci dziedziczenia formularzy (Visual Form Inheritance), na przykad z repozytorium, za pomoc opcji Inherit. Nie mona wykluczy sytuacji, i komponent TddgWaveFile jest czci formularza macierzystego i jako taki jest dziedziczony przez formularz projektu. Jeeli dziedziczenie takie istotnie ma miejsce, a ponadto dane komponentu w formularzu macierzystym s identyczne z danymi komponentu w formularzu projektu, to najprawdopodobniej nie modyfikowalimy jeszcze zawartoci komponentu w projekcie. Jeeli teraz zamknlibymy projekt i zmienili zawarto komponentu w formularzu macierzystym, to mielibymy prawo oczekiwa, i po ponownym otwarciu projektu zmiany te uwidoczni si w komponencie znajdujcym si na formularzu tego projektu. Innymi sowy dopki nie dokonamy adnej zmiany w komponencie znajdujcym si w formularzu projektu, mamy prawo oczekiwa penej jego zgodnoci z komponentem w formularzu macierzystym; komponent ma wwczas dziedziczy sw warto z formularza macierzystego, nie za odczytywa j ze strumienia. Funkcja DoWrite() rozpoczyna wic od sprawdzenia, czy komponent TddgWaveFile zosta odziedziczony z formularza macierzystego w takiej sytuacji komponent ten jest wskazywany przez waciwo Ancestor obiektu TFiler. Jeeli komponent w formularzu macierzystym rwnie jest komponentem klasy TddgWaveFile i jeeli jego zawarto jest identyczna z zawartoci podlegajc zapisowi do strumienia to przyjmuje si, i komponent w projekcie nie by jeszcze modyfikowany (cho wcale nie musi to by prawda, lecz dla prostoty przyjmijmy tak uproszczon wersj). Warto przy tym zwrci uwag na to, w jaki sposb metoda Equal() porwnuje zawarto obydwu komponentw. Te i inne szczegy wyczyta mona z kodu rdowego komponentu, ktry prezentujemy na wydruku 12.8. Wydruk 12.8. Modu implementujcy komponent TddgWaveFile
unit Wavez; interface uses SysUtils, Classes; type

519

{ rozrnienie typw z punktu widzenia edytora waciwoci } TWaveFileString = type string; EWaveError = class(Exception); TWavePause = (wpAsync, wpsSync); TWaveLoop = (wlNoLoop, wlLoop); TddgWaveFile = class(TComponent) private FData: Pointer; FDataSize: Integer; FWaveName: TWaveFileString; FWavePause: TWavePause; FWaveLoop: TWaveLoop; FOnPlay: TNotifyEvent; FOnStop: TNotifyEvent; procedure SetWaveName(const Value: TWaveFileString); procedure WriteData(Stream: TStream); procedure ReadData(Stream: TStream); protected procedure DefineProperties(Filer: TFiler); override; public destructor Destroy; override; function Empty: Boolean; function Equal(Wav: TddgWaveFile): Boolean; procedure LoadFromFile(const FileName: String); procedure LoadFromStream(S: TStream); procedure Play; procedure SaveToFile(const FileName: String); procedure SaveToStream(S: TStream); procedure Stop; published property WaveLoop: TWaveLoop read FWaveLoop write FWaveLoop; property WaveName: TWaveFileString read FWaveName write SetWaveName; property WavePause: TWavePause read FWavePause write FWavePause; property OnPlay: TNotifyEvent read FOnPlay write FOnPlay; property OnStop: TNotifyEvent read FOnStop write FOnStop; end; implementation uses MMSystem, Windows; { TddgWaveFile } destructor TddgWaveFile.Destroy; { zapewnia zwolnienie przydzielonej pamici } begin if not Empty then FreeMem(FData, FDataSize); inherited Destroy; end; function StreamsEqual(S1, S2: TMemoryStream): Boolean; begin Result := (S1.Size = S2.Size) and CompareMem(S1.Memory, S2.Memory, S1.Size); end; procedure TddgWaveFile.DefineProperties(Filer: TFiler); { definiuje materia dwikowy, wskazywany przez pole FData, jako porcj danych niesformatowanych okrelanych nazw "Data" }

function DoWrite: Boolean; begin if Filer.Ancestor <> nil then Result := not (Filer.Ancestor is TddgWaveFile) or not Equal(TddgWaveFile(Filer.Ancestor)) else Result := not Empty; end;

520

begin inherited DefineProperties(Filer); Filer.DefineBinaryProperty('Data', ReadData, WriteData, DoWrite); end; function TddgWaveFile.Empty: Boolean; begin Result := FDataSize = 0; end; function TddgWaveFile.Equal(Wav: TddgWaveFile): Boolean; var MyImage, WavImage: TMemoryStream; begin Result := (Wav <> nil) and (ClassType = Wav.ClassType); if Empty or Wav.Empty then begin Result := Empty and Wav.Empty; Exit; end; if Result then begin MyImage := TMemoryStream.Create; try SaveToStream(MyImage); WavImage := TMemoryStream.Create; try Wav.SaveToStream(WavImage); Result := StreamsEqual(MyImage, WavImage); finally WavImage.Free; end; finally MyImage.Free; end; end; end; procedure TddgWaveFile.LoadFromFile(const FileName: String); { Wczytanie materiau z pliku; zwr uwag, i niniejsza metoda nie ustala tytuu nagrania }

var F: TFileStream; begin F := TFileStream.Create(FileName, fmOpenRead); try LoadFromStream(F); finally F.Free; end; end; procedure TddgWaveFile.LoadFromStream(S: TStream); { Wczytuje dane ze strumienia, zwalniajc pami zajt przez ew. dane zaadowane wczeniej } begin if not Empty then FreeMem(FData, FDataSize); FDataSize := 0; FData := AllocMem(S.Size); FDataSize := S.Size; S.Read(FData^, FDataSize); end; procedure TddgWaveFile.Play; { Odtwarza materia dwikowy wskazywany przez FData, uywajc parametrw zapisanych w FWaveLoop i FWavePause }

521

const LoopArray: array[TWaveLoop] of DWORD = (0, SND_LOOP); PauseArray: array[TWavePause] of DWORD = (SND_ASYNC, SND_SYNC); begin { Upewnij si, e komponent zawiera dane } if Empty then raise EWaveError.Create('Brak danych do odtwarzania'); if Assigned(FOnPlay) then FOnPlay(Self); // wygeneruj zdarzenie { rozpocznij odtwarzanie } if not PlaySound(FData, 0, SND_MEMORY or PauseArray[FWavePause] or LoopArray[FWaveLoop]) then raise EWaveError.Create('Bd odtwarzania danych'); end; procedure TddgWaveFile.ReadData(Stream: TStream); { odczytuje dane ze strumienia } begin LoadFromStream(Stream); end; procedure TddgWaveFile.SaveToFile(const FileName: String); { zapisuje dane w pliku dyskowym } var F: TFileStream; begin F := TFileStream.Create(FileName, fmCreate); try SaveToStream(F); finally F.Free; end; end; procedure TddgWaveFile.SaveToStream(S: TStream); { Zapisuje dane do strumienia } begin if not Empty then S.Write(FData^, FDataSize); end; procedure TddgWaveFile.SetWaveName(const Value: TWaveFileString); { ustalenie tytuu nagrania poczone z jego adowaniem z pliku } begin if Value <> '' then begin FWaveName := ExtractFileName(Value); { nie dokonuj odczytu z pliku, jeeli trwa adowanie ze strumienia } { sprawd, czy istnieje plik o podanej nazwie } if (not (csLoading in ComponentState)) and FileExists(Value) then LoadFromFile(Value); end else begin { ustawienie pustego tytuu jest rwnoznaczne z brakiem danych i uzasadnia zwolnienie przydzielonej dla nich pamici } FWaveName := ''; if not Empty then FreeMem(FData, FDataSize); FDataSize := 0; end; end; procedure TddgWaveFile.Stop; { Zatrzymuje biece odtwarzanie } begin if Assigned(FOnStop) then FOnStop(Self); // wygeneruj zdarzenie PlaySound(Nil, 0, SND_PURGE);

522

end; procedure TddgWaveFile.WriteData(Stream: TStream); { zapisuje dane komponentu do strumienia } begin SaveToStream(Stream); end; end.

Kategoryzacja waciwoci
Poczwszy od Delphi 5, waciwoci komponentw VCL mog by specyfikowane jako nalece do poszczeglnych kategorii i sortowane wedug tych kategorii w oknie inspektora obiektw. Zaliczenie waciwoci do okrelonej kategorii nastpuje w wyniku wywoania funkcji RegisterPropertyInCategory(); analogiczn czynno w stosunku do zbioru waciwoci wykonuje funkcja RegisterPropertiesInCategory(). Obydwie funkcje zdefiniowane s w module DesignIntf.Pas. Funkcja RegisterPropertyInCategory()jest funkcj przecian, dziki czemu stosuje si j do wielu rodzajw waciwoci. Pierwszym parametrem wywoania kadego jej aspektu jest kategoria waciwoci, dalsze parametry s specyficzne dla danego aspektu:
function RegisterPropertyInCategory(ACategoryClass: TPropertyCategoryClass; const APropertyName: string): TPropertyFilter; overload; function RegisterPropertyInCategory(ACategoryClass: TPropertyCategoryClass; AComponentClass: TClass; const APropertyName: string): TPropertyFilter; overload; function RegisterPropertyInCategory(ACategoryClass: TPropertyCategoryClass; APropertyType: PTypeInfo; const APropertyName: string): TPropertyFilter; overload; function RegisterPropertyInCategory(ACategoryClass: TPropertyCategoryClass; APropertyType: PTypeInfo): TPropertyFilter; overload;

Ta funkcja przystosowana jest ponadto do interpretacji znakw blankietowych (wildcards) na przykad moemy zaliczy do okrelonej kategorii wszystkie waciwoci o nazwie rozpoczynajcej si od Data, specyfikujc nazw waciwoci jako Data* (zajrzyj do opisu konstruktora TMask.Create() w systemie pomocy, gdzie opisane s dopuszczalne postaci wyrae blankietowych). Funkcja RegisterPropertiesInCategory() rwnie jest funkcj przecion:
function RegisterPropertiesInCategory(ACategoryClass: TPropertyCategoryClass; const AFilters: array of const): TPropertyCategory; overload; function RegisterPropertiesInCategory(ACategoryClass: TPropertyCategoryClass; AComponentClass: TClass; const AFilters: array of string): TPropertyCategory; overload; function RegisterPropertiesInCategory(ACategoryClass: TPropertyCategoryClass; APropertyType: PTypeInfo; const AFilters: array of string): TPropertyCategory; overload;

Klasy kategorii waciwoci


Typ TPropertyCategoryClass jest metaklas obejmujc klasy zgodne z klas TPropertyCategory, bazow dla wszystkich klas kategorii:
TPropertyCategoryClass = class of TPropertyCategory;

523

TPropertyCategory = class(TObject) private FList: TObjectList; FMatchCount: Integer; FEditor: TPropertyEditor; FEnabled, FVisible: Boolean; FGroup: Integer; FName: string; protected function GetFilter(Index: Integer): TPropertyFilter; public constructor Create(const AName: string); destructor Destroy; override; function Add(AFilter: TPropertyFilter): TPropertyFilter; function Count: Integer; function Match(const APropertyName: string; AComponentClass: TClass; APropertyType: PTypeInfo): Boolean; procedure ClearMatches; procedure FreeEditorGroup(AGroup: Integer); property Filters[Index: Integer]: TPropertyFilter read GetFilter; property MatchCount: Integer read FMatchCount; property Visible: Boolean read FVisible write FVisible; property Editor: TPropertyEditor read FEditor write FEditor; property Name: string read FName; end;

Delphi 6 definiuje 12 standardowych klas kategorii, uwzgldniajcych charakter i przeznaczenie rnorodnych waciwoci; wymieniamy je w tabeli 12.4.

Tabela 12.4. Standardowe klasy kategorii waciwoci w Delphi 6 Klasa kategorii


TActionCategory

Charakter waciwoci Waciwoci zwizane z akcjami wykonywanymi w trakcie wykonania programu Waciwoci zwizane z operacjami bazodanowymi Waciwoci zwizane z operacjami przecigania oraz dokowania Waciwoci zwizane z systemem pomocy i podpowiedziami Waciwoci okrelajce wygld kontrolki w czasie projektowania Waciwoci zwizane z przestarzaymi operacjami

Przykady waciwoci
TControl.Enabled, TControl.Hint

TDatabaseCategory TDragNDropCategory

TQuery.DataBase, TQuery.SQL TControl.DragCursor, TControl.DragKind

THelpCategory

TWinControl.Hint, TWinControl.HelpContext

TLayoutCategory TLegacyCategory TLinkageCategory TLocaleCategory TLocalizableCategory TMiscellaneousCategory TVisualCategory

TControl.Top, TControl.Left TWinControl.Ctl3D, TWinControl.ParentCtl3D

TDataSource.DataSet Waciwoci zapewniajce zwizek pomidzy komponentami

Waciwoci zwizane z ustawieniami midzynarodowymi Waciwoci rnicowane w narodowych wersjach aplikacji Waciwoci nie zarejestrowane jawnie w adnej innej kategorii Waciwoci zwizane z wygldem komponentu w czasie wykonania programu

TControl.BiDiMode, TControl.ParentBiDiMode

Tytuy komponentw (Caption) Nazwy komponentw(Name), TSpeedButton.AllowAllUp

TScrollBox.Align, TScrollBox.Visible

524

TInputCategory

Waciwoci zwizane z wprowadzaniem danych (poza kontekstem bazodanowym)

TEdit.Enabled, TEdit.ReadOnly

Ponisza

instrukcja

dokonuje

zaliczenia

waciwoci

Keen

komponentu

TNeato

do

kategorii

TActionCategory:
RegisterPropertyInCategory(TActionCategory, TNeato, 'Keen');

Powinna si ona znale w treci procedury Register(), najlepiej w module definiujcym komponent TNeato. Przyporzdkowywanie okrelonym waciwociom okrelonych kategorii nie jest w aden sposb uregulowane ani ograniczone przez zasady Object Pascala w szczeglnoci, pojedyncza waciwo moe nalee do dowolnej liczby kategorii.

Definiowanie wasnych kategorii waciwoci


Wykorzystujc naturalne dla modelu obiektowego dziedziczenie, uytkownik ma moliwo definiowania wasnych kategorii waciwoci. Zdefiniowanie nowej klasy kategorii sprowadza si do zaimplementowania jej wirtualnych metod Name() i Description(). W charakterze przykadu zdefiniujemy kategori TSoundCategory obejmujc niektre waciwoci komponentu TddgWaveFile. Prezentowany na wydruku 12.9 modu zawiera implementacj tej kategorii, jak rwnie implementacje edytora komponentu i edytora jego waciwoci WaveName.

Wydruk 12.9. Implementacja kategorii TSoundCategory oraz edytorw: komponentu TddgWaveFile i jego waciwoci WaveName
unit WavezEd; interface uses PropertyCategories, DesignEditors, DesignIntf; type { Edytor waciwoci TddgWaveFile.WaveName } TWaveFileStringProperty = class(TStringProperty) public procedure Edit; override; function GetAttributes: TPropertyAttributes; override; end; { Edytor komponentu TddgWaveFile. na etapie projektowania } Umoliwia odtwarzanie nagrania

TWaveEditor = class(TComponentEditor) private procedure EditProp(const Prop: IProperty); public procedure Edit; override; procedure ExecuteVerb(Index: Integer); override; function GetVerb(Index: Integer): string; override; function GetVerbCount: Integer; override; end; implementation uses TypInfo, Wavez, Classes, Controls, Dialogs; { TWaveFileStringProperty } procedure TWaveFileStringProperty.Edit; { Dialogowa edycja nazwy nagrania, umoliwiajca wybr pliku

525

z okna dialogowego } begin with TOpenDialog.Create(nil) do try { ustaw parametry dialogu } Filter := 'Pliki Wav|*.wav|Wszystkie pliki|*.*'; DefaultExt := '*.wav'; { domylna nazwa pliku } FileName := GetStrValue; { Wykonaj dialog i przypisz waciwoci pobran nazw } if Execute then SetStrValue(FileName); finally Free; end; end; function TWaveFileStringProperty.GetAttributes: TPropertyAttributes; { wskazuje na edycj w formie dialogu } begin Result := [paDialog]; end; { TWaveEditor } const VerbCount = 2; VerbArray: array[0..VerbCount - 1] of string[9] = ('Odtwrz', 'Zatrzymaj'); procedure TWaveEditor.Edit; { Niniejsza metoda wywoywana jest w rezultacie dwukrotnego kliknicia komponentu. Celem poniszych instrukcji jest uruchomienie dialogu edycyjnego waciwoci WaveName, za porednictwem funkcji EditProp }

var Components: IDesignerSelections; begin Components := TDesignerSelections.Create; Components.Add(Component); GetComponentProperties(Components, tkAny, Designer, EditProp, nil); end; procedure TWaveEditor.EditProp(const Prop: IProperty); { wywoywana przez edytor komponentu } begin Prop.Edit; Designer.Modified; // poinformuj projektanta formularzy // o dokonaniu zmian w komponencie end; procedure TWaveEditor.ExecuteVerb(Index: Integer); begin case Index of 0: TddgWaveFile(Component).Play; 1: TddgWaveFile(Component).Stop; end; end; function TWaveEditor.GetVerb(Index: Integer): string; begin Result := VerbArray[Index]; end; function TWaveEditor.GetVerbCount: Integer; begin Result := VerbCount; end;

526

end.

Do kategorii TSoundCategory zaliczone zostan trzy waciwoci zwizane bezporednio z odtwarzanym nagraniem, co odzwierciedla wywoanie funkcji rejestrujcej:
RegisterPropertiesInCategory(TSoundCategory, TddgWaveFile, ['WaveLoop', 'WaveName', 'WavePause'] );

Kolekcje komponentw klasy TCollection i TCollectionItem


Czstym zjawiskiem jest grupowanie komponentw w kolekcje, ktre nastpnie staj si waciwociami dla innych komponentw prostym tego przykadem moe by waciwo Lines komponentu TMemo. Sam mechanizm grupowania jest na tyle powszechny, e uzasadnione okazao si zwrcenie uwagi na te jego aspekty, ktre wynikaj z samej istoty grupowania i istniej niezalenie od rodzaju grupowanych komponentw. Jednym z takich zagadnie jest zapis grupy komponentw w strumieniu zapis pojedynczego komponentu nie jest bowiem w Delphi niczym wyjtkowym. Wspomniana klasa TStrings stanowi w tym kontekcie pewien wyjtek; stworzono j m.in. po to, aby zautomatyzowa zapis (i odczyt) w strumieniu acuchw znakw, nie bdcych przecie obiektami (w sensie Object Pascala) oraz pozbawionych waciwego obiektom automatyzmu w tym wzgldzie. Obiektami przeznaczonymi do grupowania innych obiektw, a w szczeglnoci komponentw, s w Pascalu kolekcje (collections) istniejce ju w wersji 6.0 Turbo Pascala (engines Turbo Vision) i znacznie udoskonalone w Object Pascalu. Bazowa klasa wszystkich kolekcji nosi w Object Pascalu nazw TCollection i wywodzi si z klasy TPersistent, dziki czemu wszystkie kolekcje mog by waciwociami rnych komponentw. Bazow klas dla elementw kolekcji jest klasa TCollectionItem, rwnie wywodzca si z TPersistent. Najbardziej urzekajc cech kolekcji jest atwo przechowywania jej w strumieniu: wszystko, co trzeba w tym celu uczyni, to upublicznienie tych waciwoci elementw (bdcych obiektami pochodnymi do TCollectionItem), ktre maj takiemu przechowaniu podlega; caa reszta wykonywana jest automatycznie przez mechanizmy wywodzce si z klasy TCollection.

Przykadem komponentu wykorzystujcego kolekcj jest pasek statusu TStatusBar; jego waciwo Panels jest wanie kolekcj klasy TStatusPanels:
TStatusPanels = class(TCollection) private FStatusBar: TCustomStatusBar; function GetItem(Index: Integer): TStatusPanel; procedure SetItem(Index: Integer; Value: TStatusPanel); protected function GetOwner: TPersistent; override; procedure Update(Item: TCollectionItem); override; public constructor Create(StatusBar: TCustomStatusBar); function Add: TStatusPanel; function AddItem(Item: TStatusPanel; Index: Integer): TStatusPanel; function Insert(Index: Integer): TStatusPanel; property Items[Index: Integer]: TStatusPanel read GetItem write default; end;

SetItem;

Elementami tej kolekcji s obiekty klasy TStatusPanel:

527

TStatusPanel = class(TCollectionItem) private FText: string; FWidth: Integer; FAlignment: TAlignment; FBevel: TStatusPanelBevel; FBiDiMode: TBiDiMode; FParentBiDiMode: Boolean; FStyle: TStatusPanelStyle; FUpdateNeeded: Boolean; procedure SetAlignment(Value: TAlignment); procedure SetBevel(Value: TStatusPanelBevel); procedure SetBiDiMode(Value: TBiDiMode); procedure SetParentBiDiMode(Value: Boolean); procedure SetStyle(Value: TStatusPanelStyle); procedure SetText(const Value: string); procedure SetWidth(Value: Integer); function IsBiDiModeStored: Boolean; protected function GetDisplayName: string; override; public constructor Create(Collection: TCollection); override; procedure Assign(Source: TPersistent); override; procedure ParentBiDiModeChanged; function UseRightToLeftAlignment: Boolean; function UseRightToLeftReading: Boolean; published property Alignment: TAlignment read FAlignment write SetAlignment default taLeftJustify; property Bevel: TStatusPanelBevel read FBevel write SetBevel default pbLowered; property BiDiMode: TBiDiMode read FBiDiMode write SetBiDiMode stored IsBiDiModeStored; property ParentBiDiMode: Boolean read FParentBiDiMode write SetParentBiDiMode default True; property Style: TStatusPanelStyle read FStyle write SetStyle default psText; property Text: string read FText write SetText; property Width: Integer read FWidth write SetWidth; end;

Zwr uwag na to, e parametrem konstruktora TStatusPanel.Create() jest wskazanie na kolekcj, do ktrej dany element przynaley (wskazanie to zapamitywane jest w polu FCollection). Take obiekt TStatusPanels zapamituje w polu FStatusBar wskazanie na swego waciciela. Opublikowane waciwoci obiektu TStatusPanel podlegaj automatycznemu zapisowi w strumieniu, a organizacja zapisu kolejnych elementw jest wanie charakterystyczn cech kolekcji. Co prawda skupilimy si na jednym aspekcie kolekcji wsppracy ze strumieniem lecz nie jest to jej jedyna i najwaniejsza cecha; o jej uytecznoci stanowi przede wszystkim efektywno zarzdzania elementami ich dodawanie, usuwanie, zwalnianie i przechowywanie w strumieniu; o szczegach moesz przeczyta w systemie pomocy Delphi. Zastosowanie kolekcji zilustrujemy przykadem grupowania przyciskw TddgRunButton, opisanych w rozdziale 11. Stworzymy komponent TddgLaunchPad, pozwalajcy na uruchamianie wybranej aplikacji za pomoc jednego z przyciskw umieszczonych na pasku narzdziowym. Komponent ten wywodzi si z klasy TScrollBox. Jedn z jego waciwoci jest kolekcja RunButtons, zawierajca obiekty klasy TRunBtnItem.

Przykadowy element kolekcji klasa TRunBtnItem


Kady element kolekcji jako obiekt TRunBtnItem stanowi obudow dla przycisku TddgRunButton, bdcego elementem listy, jedna z jego waciwoci jest wic wskazaniem na w przycisk:

type TRunBtnItem = class(TCollectionItem) private FCommandLine: String; // Polecenie FLeft: Integer; // Pozycja przycisku FTop: Integer; // "" FRunButton: TRunButton; // Wskazanie na przycisk

528

public constructor Create(Collection: TCollection); override; published { Opublikowane waciwoci podlegajce strumieniowaniu } property CommandLine: String read FCommandLine write SetCommandLine; property Left: Integer read FLeft write SetLeft; property Top: Integer read FTop write SetTop; end;

Wskazanie na odnony przycisk (FRunButton) jest jednak waciwoci prywatn. Mona by sdzi, e jej opublikowanie byoby rozsdniejsze, gdy rozwizaoby wszystkie problemy ze strumieniowaniem przycisku. To prawda, lecz pojawia si pewien problem: ot strumieniowanie przebiega nieco inaczej w stosunku do komponentw (przycisk), ni w stosunku do innych pochodnych klasy TPersistent (element kolekcji) odczyt komponentu ze strumienia poczony jest z tworzeniem jego egzemplarza, natomiast dla innych klas wywodzcych si z TPersistent odczytywana zawarto wpisywana jest do istniejcego egzemplarza. W zwizku z tym poszczeglne przyciski TddgRunButton nie s przechowywane w strumieniu, lecz dynamicznie tworzone przez konstruktory elementw TRunBtnItem.

TRunButtons kolekcja komponentu


Kolejnym krokiem jest zdefiniowanie, na bazie klasy TCollection, nowej kolekcji przechowujcej obiekty TRunBtnItem. Kolekcj t nazwalimy TRunButtons:
type TRunButtons = class(TCollection) private FLaunchPad: TLaunchPad; // wskazanie na waciciela function GetItem(Index: Integer): TRunBtnItem; procedure SetItem(Index: Integer; Value: TRunBtnItem); protected procedure Update(Item: TCollectionItem); override; public constructor Create(LaunchPad: TLaunchPad); function Add: TRunBtnItem; procedure UpdateRunButtons; property Items[Index: Integer]: TRunBtnItem read GetItem write SetItem; default; end;

Zwr uwag na specyficzne waciwoci i metody zwizane z obsug elementw kolekcji TRunBtnItem; stanowi one jej waciwo tablicow. Kolekcja zawiera ponadto wskazanie na przedmiotowy pasek przyciskw (FLaunchPad). Blisze zwizki pomidzy paskiem, kolekcj i jej elementami stan si bardziej zrozumiae, gdy przyjrzymy si ich szczegom implementacyjnym.

Implementacja wsppraca kolekcji i jej elementw z komponentem macierzystym


Tworzenie, zwalnianie i zapis w strumieniu kolekcji, jej elementw i przyciskw polece ilustruje kod przedstawiony na wydruku 12.10.

Wydruk 12.10. Implementacja komponentu TddgLaunchPad


unit LnchPad; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, RunBtn, ExtCtrls;

529

type TddgLaunchPad = class; TRunBtnItem = class(TCollectionItem) private FCommandLine: string; // Polecenie FLeft: Integer; // Pozycja przycisku FTop: Integer; // FRunButton: TddgRunButton; // Reference to a TddgRunButton FWidth: Integer; // Wskazanie na przycisk FHeight: Integer; procedure SetCommandLine(const Value: string); procedure SetLeft(Value: Integer); procedure SetTop(Value: Integer); public constructor Create(Collection: TCollection); override; destructor Destroy; override; procedure Assign(Source: TPersistent); override; property Width: Integer read FWidth; property Height: Integer read FHeight; published { Waciwoci publikowane, podlegajce strumieniowaniu } property CommandLine: String read FCommandLine write SetCommandLine; property Left: Integer read FLeft write SetLeft; property Top: Integer read FTop write SetTop; end; TRunButtons = class(TCollection) private FLaunchPad: TddgLaunchPad; // Wskazanie na komponent-waciciela function GetItem(Index: Integer): TRunBtnItem; procedure SetItem(Index: Integer; Value: TRunBtnItem); protected procedure Update(Item: TCollectionItem); override; public constructor Create(LaunchPad: TddgLaunchPad); function Add: TRunBtnItem; procedure UpdateRunButtons; property Items[Index: Integer]: TRunBtnItem read GetItem write SetItem; default; end; TddgLaunchPad = class(TScrollBox) private FRunButtons: TRunButtons; TopAlign: Integer; LeftAlign: Integer; procedure SetRunButtons(Value: TRunButtons); procedure UpdateRunButton(Index: Integer); public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override; published property RunButtons: TRunButtons read FRunButtons write SetRunButtons; end; implementation { TRunBtnItem } constructor TRunBtnItem.Create(Collection: TCollection); { parametr Collection reprezentuje kolekcj bdc wacicielem tworzonego elementu } begin inherited Create(Collection); { Tworzenie i inicjacja egzemplarza przycisku oraz deklaracja paska jako waciciela przycisku }

FRunButton := TddgRunButton.Create(TRunButtons(Collection).FLaunchPad);

530

FRunButton.Parent := TRunButtons(Collection).FLaunchPad; FWidth := FRunButton.Width; // szeroko i wysoko przycisku FHeight := FRunButton.Height; // end; destructor TRunBtnItem.Destroy; begin FRunButton.Free; // zwolnij obiekt przycisku inherited Destroy; // end; procedure TRunBtnItem.Assign(Source: TPersistent); { Niniejsza metoda realizuje kopiowanie charakterystyczne dla klasy TRunBtnItem. Jeeli element jest innej klasy, realizowane jest standardowe kopiowanie (inherited Assign) } begin if Source is TRunBtnItem then begin { acuchy znakw (do ktrych zalicza si CommandLine) s w Delphi wskanikami, standardowa Assign() spowodowaaby wic powielenie wskazania na istniejcy acuch; aby tego unikn, dokonujemy jawnego kopiowania acucha - czyni to metoda dostpowa waciwoci CommandLine } CommandLine := TRunBtnItem(Source).CommandLine; { Skopiowanie pozostaych pl } FLeft := TRunBtnItem(Source).Left; FTop := TRunBtnItem(Source).Top; end else inherited Assign(Source); end; procedure TRunBtnItem.SetCommandLine(const Value: string); { This is the write accessor method for TRunBtnItem.CommandLine. It ensures that the private TddgRunButton instance, FRunButton, gets assigned the specified string from Value } { Niniejsza metoda jest metod dostpow ustawiajc waciwo TRunBtnItem.CommandLine. } begin if FRunButton <> nil then begin FCommandLine := Value; FRunButton.CommandLine := FCommandLine; { Ponisza instrukcja powoduje wywoanie metody Update dla przycisku } Changed(False); end; end; procedure TRunBtnItem.SetLeft(Value: Integer); begin if FRunButton <> nil then begin FLeft := Value; FRunButton.Left := FLeft; end; end; procedure TRunBtnItem.SetTop(Value: Integer); begin if FRunButton <> nil then begin FTop := Value; FRunButton.Top := FTop; end;

531

end; { TRunButtons } constructor TRunButtons.Create(LaunchPad: TddgLaunchPad); { Po utworzeniu elementu konstruktor zapamituje wskazanie na pasek przyciskw. } begin inherited Create(TRunBtnItem); FLaunchPad := LaunchPad; end; function TRunButtons.GetItem(Index: Integer): TRunBtnItem; { Zwraca wskazanie na egzemplarz TRunBtnItem znajdujcy si na pozycji Index } begin Result := TRunBtnItem(inherited GetItem(Index)); end; procedure TRunButtons.SetItem(Index: Integer; Value: TRunBtnItem); { Wstawia podany element na wskazan pozycj } begin inherited SetItem(Index, Value) end; procedure TRunButtons.Update(Item: TCollectionItem); { Metoda ta jest wywoywana przy kadorazowej zmianie ktregokolwiek elementu kolekcji (przycisku). Oryginalnie jest to metoda abstrakcyjna i wymaga zdefiniowania w kadej klasie pochodnej. W tym przypadku wykorzystana jest do odwieenia wygldu przyciskw }

begin if Item <> nil then FLaunchPad.UpdateRunButton(Item.Index); end; procedure TRunButtons.UpdateRunButtons; { Niniejsza metoda powoduje zaktualizowanie (odwieenie obrazu) kadego przycisku. Jest wywoywana m.in. przez metod Update kolekcji. } var i: integer; begin for i := 0 to Count - 1 do FLaunchPad.UpdateRunButton(i); end; function TRunButtons.Add: TRunBtnItem; { Niniejsza metoda przedefiniowuje zachowanie metody Add z klasy macierzystej w ten sposb, i zamiast amorficznego wskanika zwraca TYPOWANY wskanik do elementu } begin Result := TRunBtnItem(inherited Add); end; { TddgLaunchPad } constructor TddgLaunchPad.Create(AOwner: TComponent); { Tworzy kolekcj i inicjuje zmienne okrelajce pooenie przyciskw } begin

532

inherited Create(AOwner); FRunButtons := TRunButtons.Create(Self); TopAlign := 0; LeftAlign := 0; end; destructor TddgLaunchPad.Destroy; begin FRunButtons.Free; // Zwalnia kolekcj inherited Destroy; // end; procedure TddgLaunchPad.GetChildren(Proc: TGetChildProc; Root: TComponent); { Przedefiniowanie tej metody jest konieczne, aby unikn zbdnego zapisywania w strumieniu obrazu przyciskw. Informacje niezbdne do utworzenia przyciskw zawarte s w (zapisywanych) elementach kolekcji, dlatego te naley wyeliminowa zapis tych zawaszczonych komponentw, ktre s klasy TRunButton. } var I: Integer; begin for I := 0 to ControlCount - 1 do { zignoruj obiekty klasy TddgRunButton } if not (Controls[i] is TddgRunButton) then Proc(TComponent(Controls[I])); end; procedure TddgLaunchPad.SetRunButtons(Value: TRunButtons); begin FRunButtons.Assign(Value); end; procedure TddgLaunchPad.UpdateRunButton(Index: Integer); { Metoda ta odpowiada za rozmieszczenie przyciskw na pasku. Jeeli jest to konieczne, pasek staje si wielorzdowy. Metoda uruchamiana jest w momencie dodawania i usuwania przycisku. W dalszym cigu mona zmniejszy rozmiar paska poniej rozmiarw pojedynczego przycisku }

begin // pozycja pierwszego przycisku if Index = 0 then begin TopAlign := 0; LeftAlign := 0; end; { Przy niewystarczajcej szerokoci tworzony jest nastpny wiersz } if (LeftAlign + FRunButtons[Index].Width) > Width then begin TopAlign := TopAlign + FRunButtons[Index].Height; LeftAlign := 0; end; FRunButtons[Index].Left := LeftAlign; FRunButtons[Index].Top := TopAlign; LeftAlign := LeftAlign + FRunButtons[Index].Width; end; end.

533

Implementacja obiektu TRunBtnItem


Konstruktor TRunBtnItem.Create() tworzy egzemplarz przycisku TddgRunButton i przypisuje wskazanie na niego polu FRunButton. Kady element kolekcji posiada swj wasny egzemplarz przycisku. Pewnego komentarza wymagaj dwie ponisze linie konstruktora:
FRunButton := TddgRunButton.Create(TRunButtons(Collection).FLaunchPad); FRunButton.Parent := TRunButtons(Collection).FLaunchPad;

Pierwsza linia powoduje utworzenie obiektu TddgRunButton i ustanowienie relacji wasnoci z nadrzdnym paskiem narzdziowym, ktry jest jednoczenie wacicielem kolekcji. Jak zapewne pamitasz, wacicielem komponentu moe by tylko inny komponent, czyli obiekt wywodzcy si z klasy TComponent; ani kolekcja, ani jej elementy nie speniaj tego warunku, nie mog by wic wacicielami przyciskw TddgRunButton. Pojawia si tu jednak pewien problem. Wacicielem przycisku jest co prawda komponent nadrzdny (TddgLaunchPad), lecz tworzenie jego egzemplarza odbywa si w konstruktorze elementu kolekcji TRunBtnItem. Podczas zapisu formularza w strumieniu, zostan w nim zapisane rwnie przyciski TddgRunButton; podczas odczytu formularza nastpi ich podwojenie oprcz zestawu odczytanego ze strumienia powstanie drugi ich zestaw, utworzony przez konstruktory elementw kolekcji. Std wniosek, i naley zapobiec zapisywaniu przyciskw w strumieniu; naley w tym celu przedefiniowa metod GetChildren() paska narzdziowego tak, by zawaszczone przez niego komponenty klasy TddgRunButton nie byy brane pod uwag:
procedure TddgLaunchPad.GetChildren(Proc: TGetChildProc; Root: TComponent); var I: Integer; begin for I := 0 to ControlCount - 1 do if not (Controls[i] is TddgRunButton) then Proc(TComponent(Controls[I])); end;

Takie przedefiniowanie automatycznie zapobiega jeszcze jednej niepodanej rzeczy mianowicie zwalnianiu przyciskw przez pasek narzdziowy podczas jego destrukcji; przyciski s zwalniane w destruktorach elementw kolekcji i nie mona zwalnia ich powtrnie. Druga z prezentowanych instrukcji konstruktora czyni pasek narzdziowy kontrolk rodzicielsk w stosunku do przyciskw, co zapewnia utrzymanie ich waciwej reprezentacji graficznej. Znaczenie pozostaych metod, bdcych w wikszoci metodami dostpowymi waciwoci, wyjanione jest w komentarzach towarzyszcych kodowi rdowemu.

Implementacja kolekcji TRunButtons


Konstruktor Create() po utworzeniu kolekcji przypisuje wskazanie na komponent nadrzdny (ktrym jest pasek narzdziowy) polu FLaunchPad; jest ono wielokrotnie wykorzystywane w treci moduu. Metoda Update()wywoywana jest wwczas, gdy ktrykolwiek element kolekcji zasygnalizuje zmian. Powoduje ona wywoanie metody UpdateRunButton()paska narzdziowego, ustawiajcej pewne jego geometryczne waciwoci, w efekcie czego nastpuje ponowne rozmieszczenie przyciskw na panelu paska. Pozostae metody s metodami dostpowymi waciwoci kolekcji; znaczenie waniejszych z nich zostao wyjanione za pomoc komentarzy.

Implementacja paska narzdziowego TddgLaunchPad


Konstruktor i destruktor paska to procedury nieskomplikowane; utworzeniu (zwolnieniu) egzemplarza paska towarzyszy jednoczesne utworzenie (zwolnienie) kolekcji. Przedefiniowanie metody GetChildren() ma zwizek z niepodanymi konsekwencjami faktu, i pasek narzdziowy jest wacicielem przyciskw TddgRunButton; o konsekwencjach tych pisalimy omawiajc implementacj obiektw TRunBtnItem. Metoda UpdateRunButton() a raczej jej iteracyjne wywoanie powoduje odwieenie wygldu paska. Tre metody wyklucza chowanie si przyciskw poza pionowymi krawdziami komponentu; moliwe jest natomiast jego pionowe przewijanie, jako e wywodzi si on z klasy TScrollBox.

534

Edycja kolekcji i jej elementw w dialogowym edytorze waciwoci


Po szczegowym opisie komponentu TddgLaunchPad, kolekcji TRunButtons oraz jej elementw TRunBtnItem, zaprezentujemy teraz edytor umoliwiajcy manipulowanie zawartoci tej kolekcji, czyli dodawanie do niej i usuwanie z niej przyciskw TddgRunButton. Ten edytor jest dialogowym edytorem waciwoci RunButtons paska narzdziowego; bezporednio manipuluje elementami kolekcji TRunButtons w oknie dialogowym wywietlane s etykiety poszczeglnych przyciskw, ukrywajce si pod waciwoci CommandLine poszczeglnych elementw. Obecne w oknie dialogowym przyciski umoliwiaj dodawanie i usuwanie polece, zatwierdzanie i odrzucanie zmian, jak rwnie testowanie skutkw wybranego polecenia. Implementacj wspomnianego edytora przedstawiamy na wydruku 12.11. Wydruk 12.11. Implementacja edytora kolekcji przyciskw
unit LPadPE; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Buttons, RunBtn, StdCtrls, LnchPad, DesignIntf, DesignEditors, ExtCtrls, TypInfo; type { formularz okna dialogowego } TLaunchPadEditor = class(TForm) PathListBox: TListBox; AddBtn: TButton; RemoveBtn: TButton; CancelBtn: TButton; OkBtn: TButton; Label1: TLabel; pnlRBtn: TPanel; procedure PathListBoxClick(Sender: TObject); procedure AddBtnClick(Sender: TObject); procedure RemoveBtnClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure CancelBtnClick(Sender: TObject); private TestRunBtn: TddgRunButton; FLaunchPad: TddgLaunchPad; // kopia zapasowa paska narzdziowego FRunButtons: TRunButtons; // wskazanie na kolekcj Modified: Boolean; procedure UpdatePathListBox; end; { Klasa edytora waciwoci } TRunButtonsProperty = class(TPropertyEditor) function GetAttributes: TPropertyAttributes; override; function GetValue: string; override; procedure Edit; override; end;

{ Funkcja wywoywana przez edytor waciwoci. } function EditRunButtons(RunButtons: TRunButtons): Boolean; implementation {$R *.DFM} function EditRunButtons(RunButtons: TRunButtons): Boolean; { Tworzy egzemplarz klasy TLaunchPadEditor bezporednio modyfikujcy kolekcj }

535

begin with TLaunchPadEditor.Create(Application) do try FRunButtons := RunButtons; { Tworzenie kopii zapasowej kolekcji na wypadek anulowania edycji } FLaunchPad.RunButtons.Assign(RunButtons); { Narysuj okno z przyciskami TRunBtnItems. } UpdatePathListBox; ShowModal; // wywietl formularz dialogowy Result := Modified; finally Free; end; end; { TLaunchPadEditor } procedure TLaunchPadEditor.FormCreate(Sender: TObject); begin { Tworzenie kopii zapasowej paska narzdziowego na wypadek anulowania edycji } FLaunchPad := TddgLaunchPad.Create(Self); // Create the TddgRunButton instance and align it to the // enclosing panel.

TestRunBtn := TddgRunButton.Create(Self); TestRunBtn.Parent := pnlRBtn; // Utwrz egzemplarz TddgRunButton i wyrwnaj go do panelu TestRunBtn.Width := pnlRBtn.Width; TestRunBtn.Height := pnlRBtn.Height; end; procedure TLaunchPadEditor.FormDestroy(Sender: TObject); begin TestRunBtn.Free; FLaunchPad.Free; // zwolnij kopi zapasow paska narzdziowego end; procedure TLaunchPadEditor.PathListBoxClick(Sender: TObject); { Po klikniciu elementu uczy go elementem biecym } begin if PathListBox.ItemIndex > -1 then TestRunBtn.CommandLine := PathListBox.Items[PathListBox.ItemIndex]; end; procedure TLaunchPadEditor.UpdatePathListBox; { Inicjuje ponownie list edytora - PathListBox } var i: integer; begin PathListBox.Clear; // wyczy list for i := 0 to FRunButtons.Count - 1 do PathListBox.Items.Add(FRunButtons[i].CommandLine); end; procedure TLaunchPadEditor.AddBtnClick(Sender: TObject); { Przy dodawaniu nowego przycisku zapytuje o ciek, nastpnie tworzy nowy przycisk i dodaje go do kolekcji } var OpenDialog: TOpenDialog; begin OpenDialog := TOpenDialog.Create(Application);

536

try OpenDialog.Filter := 'Pliki wykonywalne|*.EXE'; if OpenDialog.Execute then begin { Dodaj do listy PathListBox. } PathListBox.Items.Add(OpenDialog.FileName); FRunButtons.Add; // utwrz obiekt TRunBtnItem { Uczy nowo dodany przycisk biecym } PathListBox.ItemIndex := FRunButtons.Count - 1;

{ Ustaw wiersz polece wyrnionego przycisku jako kopi wybranego } FRunButtons[PathListBox.ItemIndex].CommandLine := PathListBox.Items[PathListBox.ItemIndex]; { Zasymuluj kliknicie listy, aby uaktualni ikon przycisku testowego } PathListBoxClick(nil); Modified := True; end; finally OpenDialog.Free end; end; procedure TLaunchPadEditor.RemoveBtnClick(Sender: TObject); { Usu wybrany przycisk z okna dialogowego i z kolekcji } var i: integer; begin i := PathListBox.ItemIndex; if i >= 0 then begin PathListBox.Items.Delete(i); // usu element z listy FRunButtons[i].Free; // usu element z kolekcji TestRunBtn.CommandLine := ''; // wyczy etykiet przycisku testowego Modified := True; end; end; procedure TLaunchPadEditor.CancelBtnClick(Sender: TObject); { Anulowanie operacji: odtworzenie kolekcji z kopii zapasowej i zamknicie modalnego formularza } begin FRunButtons.Assign(FLaunchPad.RunButtons); Modified := False; ModalResult := mrCancel; end; { TRunButtonsProperty } function TRunButtonsProperty.GetAttributes: TPropertyAttributes; begin Result := [paDialog]; // dostpna edycja w formie penego dialogu end; procedure TRunButtonsProperty.Edit; { Specyficzne okno dialogowe; wywouje metod edycji przycisku "EditRunButton()", przekazujc mu adres przycisku, ktrego indeks otrzymuje si za pomoc metody GetOrdValue(). Nastpnie odwiea okno dialogowe za pomoc metody TRunButtons.UpdateRunButtons } begin

537

if EditRunButtons(TRunButtons(GetOrdValue)) then Modified; TRunButtons(GetOrdValue).UpdateRunButtons; end; function TRunButtonsProperty.GetValue: string; { Znakowa reprezentacja waciwoci jest ujt w nawiasy nazw klasy "TRunButtons" } begin Result := Format('(%s)', [GetPropType^.Name]); end; end.

Prezentowany modu zawiera definicj dwch edytorw. S to: edytor komponentu TLaunchPadEditor i edytor waciwoci-kolekcji TRunButtonsProperty. Zajmiemy si wpierw drugim z nich. Edytor TRunButtonsProperty nie odbiega zbytnio od rozpatrywanych dotychczas. Przedefiniowalimy metody GetAttributes(), Edit() i GetValue(). Metoda GetAttributes() ustawia flag paDialog, co oznacza moliwo edycji waciwoci w postaci okna dialogowego (w tym wypadku moliwo jedyn). Odzwierciedla to ikona wielokropka (ellipsis) na kocu linii waciwoci w inspektorze obiektw. Metoda GetValue() zwraca ujt w nawiasy nazw klasy waciwoci, uzyskan z informacji RTTI za pomoc funkcji GetPropType(). Nazwa ta jest jedynie wywietlana, bez moliwoci edycji (bo i po co?). I wreszcie metoda Edit() waciwy dialog wywouje funkcj edycji przycisku EditRunButtons(); jej parametrem jest wskazanie na przycisk uruchamiajcy, ktrego indeks rwny jest numerowi wybranej waciwoci, uzyskiwanej za pomoc funkcji GetOrdValue(). Po zakoczeniu funkcji edytujcej wywoywana jest procedura UpdateRunButton(). Funkcja EditRunButtons() tworzy egzemplarz komponentu TLaunchPadEditor oraz przypisuje jego polu FRunButtons wskazanie na kolekcj. Nastpnie tworzona jest kopia zapasowa caego komponentu, a jej wskazanie zawiera pole FLaunchPad; kopia ta jest wykorzystywana w przypadku wybrania przycisku Anuluj do odtworzenia stanu paska narzdziowego sprzed edycji. Metoda PathListBoxClick() symuluje kliknicie listy PathListBox. Powoduje to uaktualnienie przycisku testowego (TestRunBtn) jego ikony oraz polecenia, zgodnie z wybran pozycj. Dodanie nowego polecenia nastpuje w procedurze AddButtonClick(), obsugujcej zdarzenie OnClick przycisku powodujcego dodawanie nowych pozycji wywietlone zostaje standardowe okno dialogowe pozwalajce wybra plik wykonywalny. Analogicznie, procedura RemoveBtnClick() usuwa wybrany przycisk z kolekcji. Anulowanie edycji polega na przepisaniu do komponentu oryginalnego utworzonej na pocztku edycji kopii zapasowej; wykonuje to metoda CancelBtnClick(). Jak wic wida z przytoczonych przykadw, kolekcje Delphi to idealne narzdzie do wygodnego, efektywnego grupowania komponentw; jeeli spotkasz si z tak potrzeb w przyszoci, to wanie masz gotowe rozwizanie!

Podsumowanie
Niniejszy rozdzia by drugim z kolei omawiajcym problematyk tworzenia nowych komponentw w Delphi. Przedstawilimy w nim zaawansowane mechanizmy towarzyszce projektowaniu nowych komponentw, m.in. niestandardowe podpowiedzi, animacj komponentu, edytory komponentw i ich waciwoci, kategoryzacj waciwoci oraz kolekcje komponentw. Przedstawione przykady w wyrany sposb przemawiaj na korzy znajomoci mechanizmw interfejsu Win32 API, na ktrym opiera si mechanizm zdarze Delphi; poza tym niektre elementy interfejsu (np. nieprostoktne podpowiedzi) nie posiadaj odpowiednika w komponentach Delphi i posugiwanie si procedurami API jest w tym przypadku nieodzowne.

538

Rozdzia 13.

Komponenty midzyplatformowe
Trzy poprzednie rozdziay powicone byy architekturze i projektowaniu komponentw VCL, przeznaczonych dla platformy MS Windows. W niniejszym rozdziale zajmiemy si podstawami projektowania komponentw CLX, umoliwiajcych tworzenie aplikacji zarwno dla Windows, jak i dla Linuksa. Tak si szczliwie skada, i znaczna cz umiejtnoci nabyta podczas projektowania komponentw VCL okae si przydatna rwnie w odniesieniu do komponentw CLX.

CLX co to jest?
CLX wymawiane najczciej jako clicks to akronim od Component Library for Cross-Platform, czyli midzyplatformowej biblioteki komponentw; pojawi si po raz pierwszy w zwizku z Kyliksem wywodzcym si z Delphi narzdziem typu RAD dla Linuksa. Biblioteka CLX jest jednak czym wicej ni tylko adaptacj VCL na gruncie Linuksa: jej obecno w Delphi 6 umoliwia (po raz pierwszy w historii Delphi) przekroczenie granic Windows i tworzenie aplikacji zgodnych zarwno z Delphi, jak i Kyliksem. Ponadto biblioteka VCL (w Delphi) utosamiana bywa raczej z komponentami wizualnymi (rwnie w nazwie), co nie powinno dziwi wobec faktu, i komponenty te stanowi wiksz jej cz (i jednoczenie lwi cz palety komponentw). Tymczasem architektura CLX jest nieco bardziej zoona, bo oprcz wizualnych komponentw grupy VisualCLX zawiera take komponenty BaseCLX, DataCLX i NetCLX.
BaseCLX to grupa klas i moduw wsplnych dla Delphi 6 i Kyliksa nale do niej m.in. moduy System, SysUtils i Classes, okrelane w Delphi (od pocztku) mianem biblioteki RTL. Mimo i moduy te stanowi

mog skadniki aplikacji obydwu typw VCL i CLX za aplikacj CLX zwyko si uwaa tak, ktrej strona wizualna zrealizowana zostaa na podstawie klas grupy VisualCLX.
VisualCLX stanowi odmian tego, co wikszo programistw skonna jest uwaa (w Delphi) za VCL, jednak oparta jest nie na standardowych kontrolkach Windows z bibliotek User32.DLL czy ComCtl32.DLL, lecz na tzw. widetach1 zawartych w bibliotece Qt. Do grupy DataCLX zaliczaj si komponenty zapewniajce dostp do danych za pomoc nowej technologii dbExpress, natomiast nowe, midzyplatformowe oblicze WebBrokera ucieleniaj komponenty grupy NetCLX.

W niniejszym rozdziale skoncentrujemy si gwnie na VisualCLX, ze szczeglnym uwzgldnieniem tworzenia nowych komponentw na bazie tej architektury. Opiera si ona (jak ju wczeniej wspominalimy) na bibliotece

To spolszczona posta angielskiego terminu widget, ktry jest zlepkiem sw Visual Gadget (przyp. tum.).

551

Qt (cute) firmy Troll Tech, stanowicej niezalen od konkretnej platformy bibliotek klas C++, realizujcych funkcjonalno widetw skadajcych si na interfejs uytkownika. cilej w chwili obecnej biblioteka Qt

zawiera elementy charakterystyczne dla rodowisk MS Windows oraz X Window System, moe wic by wykorzystywana zarwno na potrzeby aplikacji windowsowych, jak i linuksowych; na jej podstawie zrealizowano wanie linuksowy meneder okien KDE. Biblioteka Qt nie jest bynajmniej jedyn dostpn midzyplatformow bibliotek klas; to, i Borland zdecydowa si wanie na ni, wynika z kilku istotnych przyczyn. Po pierwsze, jej klasy podobne s w duym stopniu do klas komponentw VCL na przykad ich waciwoci zrealizowane zostay z udziaem metod dostpowych Getxxxx/Setxxxx, po drugie wykorzystuj podobny do VCL mechanizm powiadamiania o zdarzeniach (tzw. sygnay). Wreszcie po trzecie jej widety to nic innego jak standardowe kontrolki interfejsu uytkownika, speniajce t sam rol co standardowe kontrolki Windows. To wszystko pozwolio na stworzenie komponentw biblioteki CLX przez nadbudowanie pascalowych otoczek wok gotowych widetw zamiast budowania caej architektury od zera.

Architektura CLX
Jak przed chwil stwierdzilimy, VisualCLX jest grup klas Object Pascala zbudowanych na bazie funkcjonalnoci widetw biblioteki Qt co stanowi analogi do komponentw VCL zbudowanych na bazie standardowych kontrolek Windows i biblioteki Windows API. Podobiestwo to nie jest bynajmniej przypadkowe, lecz wynika z jednego z celw projektowych: atwoci przystosowywania istniejcych aplikacji VCL do architektury CLX. Rysunki 13.1 i 13.2 przedstawiaj hierarchiczn struktur klas w obydwu tych rodowiskach; przyciemnione prostokty na rysunku 13.1 wyrniaj podstawowe klasy biblioteki VCL.

Ju na pierwszy rzut oka wida rnic pomidzy obydwiema hierarchiami w architekturze CLX pojawiy si nowe (w stosunku do VCL) klasy, niektre zostay przesunite do innych gazi. Rnice te zaznaczone zostay na rysunku 13.2 za pomoc rozjanionych prostoktw. I tak, na przykad, komponent zegarowy (TTimer) nie wywodzi si ju bezporednio z klasy TComponent, lecz z nowej klasy THandleComponent, stanowicej klas bazow do obsugi wszystkich tych przypadkw, gdy komponent niewizualny wymaga dostpu do uchwytu (handle) jakiej kontrolki Qt. Innym przykadem jest etykieta TLabel, nie bdca ju kontrolk graficzn, lecz wywodzca si z klasy TFrameControl, ktra wykorzystuje rnorodne moliwoci ksztatowania obrzea widetw Qt.

552

Rysunek 13.1. Hierarchia klas VCL

Nieprzypadkowe jest take podobiestwo nazw klas bazowych kontrolek wizualnych TWinControl (VCL) i TWidgetControl (CLX): charakterystyczny dla Windows czon Win ustpi miejsca charakterystycznemu dla CLX Widget. Majc na wzgldzie atwo przenoszenia kodu rdowego Borland zdefiniowa klas
TWinControl take w bibliotece CLX, stanowi ona jednak tylko synonim klasy TWidgetControl. Mona

byo oczywicie unikn tej nieco mylcej w skutkach (zwaszcza dla niewiadomego uytkownika) operacji i utworzy dwa oddzielne moduy dla obydwu grup kontrolek, a pniej odrnia je za pomoc symboli kompilacji warunkowej; utrudnioby to jednak przenoszenie kodu rdowego (identyfikator TWinControl straciby racj bytu, a jego systematyczna zmiana na TWidgetControl wymagaaby dodatkowej fatygi), za w aplikacjach midzyplatformowych konieczne byoby utrzymywanie dwch identyfikatorw na oznaczenie klasy bazowej kontrolek.

Notatka

Zwr uwag, i utrzymywanie w pojedynczym pliku kodu dla obydwu typw komponentw (VCL i CLX) jest czym jakociowo rnym od tworzenia kodu uniwersalnego komponentu CLX, dajcego si uy zarwno w Delphi 6, jak i w Kyliksie (takimi komponentami zajmiemy si w dalszej czci rozdziau).

553

Rysunek 13.2. Hierarchia klas CLX

Na szczcie rnice przedstawione na rysunku 13.2 nie maj zbyt duego znaczenia dla twrcw aplikacji, poniewa wikszo komponentw VCL posiada na gruncie CLX identycznie nazwane odpowiedniki. Nie maj jednak takiego szczcia twrcy nowych komponentw zmiany w hierarchii klas maj dla nich znaczenie zasadnicze.
Wskazwka

Struktur hierarchii klas mona atwo zobaczy za pomoc przegldarki obiektw (Object Browser) w Delphi 6 i w Kyliksie; jednak ze wzgldu na synonim TWinControl, uzyskamy dwie identyczne hierarchie (dla TWidgetControl i TWinControl).

Pomidzy VCL i CLX istnieje jeszcze wicej podobiestw, ktrych nie sposb uwzgldni na przedstawionych rysunkach. Na przykad znane z VCL ptno (Canvas) ma w CLX niemal identyczn natur i wykorzystywane jest w bardzo zbliony sposb, cho oczywicie rnice pomidzy obydwoma rodowiskami przesdzaj o jego odmiennej implementacji: w VCL jest ono otoczk kontekstu urzdzenia, za w CLX analogicznego mechanizmu zwanego malarzem (painter), mimo to obydwa te mechanizmy reprezentowane s przez t sam waciwo Handle. Ponadto, z uwagi na wymg atwoci przenoszenia kodu, niemal identycznie wygldaj interfejsy komponentw w obydwu grupach pod wzgldem repertuaru waciwoci publicznych (public) i publikowanych (published) oraz zdarze (OnClick, OnChange, OnKeyPress) i ich metod dyspozycyjnych (Click(), Change() i KeyPress()).

554

Z Windows do Linuksa
Mimo wielu podobiestw pomidzy analogicznymi elementami komponentw VCL i CLX, istniejce midzy tymi rodowiskami rnice daj zna o sobie tym wyraniej, im blisza staje si zaleno konkretnego elementu od mechanizmu charakterystycznego tylko dla jednego ze rodowisk. I tak, w rodowisku Kyliksa traci sens wikszo odwoa do Win32 API; mechanizmy typowe jedynie dla Windows jak np. MAPI musz by zastpione rwnowanymi mechanizmami linuksowymi, a uywajce ich komponenty nie nadaj si po prostu do przeniesienia na platform linuksow. Z kolei niektre problemy rozwizywane przez funkcje biblioteki RTL musz by rozwizane w inny sposb przykadem moe by czuo Linuksa na wielko liter w nazwach plikw; Pascal, niewraliwy na wielko liter w identyfikatorach, staje si pod Kyliksem wraliwy na wielko liter w nazwach moduw w dyrektywach uses! Linux pozbawiony jest te wielu znanych z Windows mechanizmw systemowych nie ma tu technologii COM, s jednak obsugiwane interfejsy; nie ma te dokowania okien, dwukierunkowej (bidirectional) obsugi tekstu, lokalizowania charakterystycznego dla krajw azjatyckich itp. Pewnym problemem dla autorw aplikacji i komponentw jest istnienie oddzielnych moduw, dedykowanych tylko okrelonym platformom, na przykad kod kontrolek windowsowych znajduje si w pliku Controls.pas, za kod dla widetw CLX w pliku QControls.pas. Stwarza to moliwo pomieszania obydwu rodowisk w sytuacji, gdy komponent CLX lub aplikacja przeznaczona dla Kyliksa opracowywane s w rodowisku Delphi 6. Tak skonstruowany komponent, jeeli zawiera elementy typowe wycznie dla VCL, bdzie bez problemu pracowa w Delphi 6, najczciej jednak odmwi wsppracy pod Kyliksem. Mona unikn takiej sytuacji, gdy, za rad Borlanda, komponenty i aplikacje przeznaczone dla Kyliksa bdziemy opracowywa pod Kyliksem niestety, rodowisko Kyliksa jest (w zgodnej opinii programistw) mniej komfortowe od Delphi 6.
Wskazwka

Wobec opisanych konsekwencji rnic pomidzy VCL i CLX, nie wydaje si uzasadnione uywanie komponentw CLX w aplikacjach przeznaczonych wycznie dla Windows.

Nie ma komunikatw
Linux (a raczej podsystem X Window) nie implementuje typowego dla Windows mechanizmu komunikatw; w efekcie nie do zaakceptowania jest w Kyliksie kod rdowy odwoujcy si do identyfikatorw w rodzaju wm_LButtonDown, wm_SetCursor czy wm_Char. Reagowaniem na zachodzce w systemie zdarzenia zajmuj si w bibliotece Qt wyspecjalizowane klasy dzieje si tak niezalenie od platformy systemowej, tak wic komponent CLX nie jest zdolny reagowa na komunikaty nawet pod Windows; zamiast znanych z Delphi metod z klauzul message (np. CMTextChanged()), powinien on korzysta z rwnowanych metod dynamicznych (TextChanged()), co wyjanimy dokadniej w nastpnym punkcie.

Przykadowe komponenty
W niniejszym punkcie przyjrzymy si nieco dokadniej przykadom transformacji komponentw VCL na rwnowane komponenty CLX. Na pocztek zajmiemy si popularnym spinerem to pomocniczy komponent wsppracujcy najczciej z polem edycyjnym, dokonujcy jego automatycznej inkrementacji lub dekrementacji; realizuje on wiele interesujcych mechanizmw (jak specyficzne rysowanie), wspprac z klawiatur i mysz, przyjmowanie i utrat skupienia (focus) itp. Trzy kolejne komponenty to pochodne bazowego spinera. Pierwszy z nich wzbogacony jest o obsug myszy i wywietlanie specyficznych kursorw ju na etapie projektowania, drugi realizuje wspprac z list obrazkw (ImageList), trzeci natomiast wsppracuje z kontrolk reprezentujc pole bazy danych.
Wskazwka

Wszystkie prezentowane w tym rozdziale moduy nadaj si do wykorzystania zarwno w Delphi 6, jak i w Kyliksie.

555

Podstawa komponent TddgSpinner


Rysunek 13.3 przedstawia trzy egzemplarze komponentu TddgSpinner na formularzu aplikacji CLX. W odrnieniu od pionowo uoonych strzaek, charakterystycznych dla windowsowego spinera, komponent ten posiada odpowiednie przyciski w ukadzie poziomym: inkrementujcy i dekrementujcy.

Rysunek 13.3. Komponent TddgSpinner pomocny przy wprowadzaniu liczb cakowitych Wydruk 13.1 przedstawia kompletny kod rdowy moduu QddgSpin.pas implementujcego komponent TddgSpinner. Podobnie jak spiner windowsowy, wywodzi si on z klasy TCustomControl tyle e w tym przypadku jest to klasa CLX i komponent moe by uywany zarwno w Windows, jak i pod Linuksem. Cho migracja na platform CLX rzadko wie si ze zmian nazw komponentw, to jednak zasad jest poprzedzanie nazwy moduu VCL liter Q dla podkrelenia zalenoci tego moduu od biblioteki Qt.
Notatka

Kady z prezentowanych wydrukw zawiera wykomentowane linie stanowice cz kodu VCL; komentarze oznaczone dodatkowo jako VCL > CLX podkrelaj elementy specyficzne dla przenoszenia komponentu z VCL do CLX.

Wydruk 13.1. QddgSpin.Pas kod rdowy komponentu TddgSpinner


{================================================================== QddgSpin Niniejszy modu implementuje komponent TddgSpinner z biblioteki CLX, zawierajcy poziomo uoone przyciski zmieniajce wprowadzan warto (w odrnieniu od windowsowego spinera posiadajcego pionowo uoone strzaki). Komponent ten wywodzi si z CLX-owej wersji TCustomControl i demonstruje wiele interesujcych moliwoci, jak przyjmowanie i utrata skupienia, specyficzne rysowanie i wspprac z mysz

Copyright 2001 by Ray Konopka ==================================================================} unit QddgSpin; interface uses SysUtils, Classes, Types, Qt, QControls, QGraphics; (* Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, ImgList; *)

556

type TddgButtonType = ( btMinus, btPlus ); TddgSpinnerEvent = procedure (Sender: TObject; NewValue: Integer; var AllowChange: Boolean ) of object; TddgSpinner = class( TCustomControl ) private // Pola komponentu FValue: Integer; FIncrement: Integer; FButtonColor: TColor; FButtonWidth: Integer; FMinusBtnDown: Boolean; FPlusBtnDown: Boolean; // Wskaniki do procedur zdarzeniowych FOnChange: TNotifyEvent; FOnChanging: TddgSpinnerEvent; (* // VCL->CLX:

ponisze metody komunikacyjne nie s dostpne w CLX

// Obsuga komunikatu Windows procedure WMGetDlgCode( var Msg: TWMGetDlgCode ); message wm_GetDlgCode; // Obsuga komunikatu komponentu procedure CMEnabledChanged( var Msg: TMessage ); message cm_EnabledChanged; *) protected procedure Paint; override; procedure DrawButton( Button: TddgButtonType; Down: Boolean; Bounds: TRect ); virtual; // Metody obsugi procedure DecValue( Amount: Integer ); virtual; procedure IncValue( Amount: Integer ); virtual; function CursorPosition: TPoint; function MouseOverButton( Btn: TddgButtonType ): Boolean; // VCL->CLX: EnabledChanged zastpuje metod komunikacjn // cm_EnabledChanged // procedure EnabledChanged; override; // Nowe metody obsugi zdarze procedure Change; dynamic; function CanChange( NewValue: Integer ): Boolean; dynamic; // przedefiniowane metody obsugi zdarze procedure DoEnter; override; procedure DoExit; override; procedure KeyDown(var Key: Word; Shift: TShiftState); override; procedure MouseDown( Button: TMouseButton; Shift: TShiftState; X, Y: Integer ); override; procedure MouseUp( Button: TMouseButton; Shift: TShiftState; X, Y: Integer ); override; (* // VCL->CLX:

ponisze deklaracje zostay zmienione w CLX

function DoMouseWheelDown( Shift: TShiftState; MousePos: TPoint ): Boolean; override; function DoMouseWheelUp( Shift: TShiftState; MousePos: TPoint ): Boolean; override; *) function DoMouseWheelDown( Shift: TShiftState; const MousePos: TPoint ): Boolean; override; function DoMouseWheelUp( Shift: TShiftState; const MousePos: TPoint ): Boolean; override;

557

// Metody dostpowe waciwoci procedure SetButtonColor( Value: TColor ); virtual; procedure SetButtonWidth( Value: Integer ); virtual; procedure SetValue( Value: Integer ); virtual; public // Nie zapomnij o klauzuli override w konstruktorze constructor Create( AOwner: TComponent ); override; published // Nowe deklaracje waciwoci property ButtonColor: TColor read FButtonColor write SetButtonColor default clBtnFace; property ButtonWidth: Integer read FButtonWidth write SetButtonWidth default 18; property Increment: Integer read FIncrement write FIncrement default 1; property Value: Integer read FValue write SetValue; // Nowe deklaracje zdarze property OnChange: TNotifyEvent read FOnChange write FOnChange; property OnChanging: TddgSpinnerEvent read FOnChanging write FOnChanging; // Odziedziczone waciwoci i zdarzenia property Color; (* property DragCursor; // VCL->CLX: waciwo niedostpna w CLX *) property DragMode; property Enabled; property Font; property Height default 18; property HelpContext; property Hint; property ParentShowHint; property PopupMenu; property ShowHint; property TabOrder; property TabStop default True; property Visible; property Width default 80; property property property property property property property property property property property property property end; OnClick; OnDragDrop; OnDragOver; OnEndDrag; OnEnter; OnExit; OnKeyDown; OnKeyPress; OnKeyUp; OnMouseDown; OnMouseMove; OnMouseUp; OnStartDrag;

implementation

558

{===================================} {== Metody komponentu TddgSpinner ==} {===================================} constructor TddgSpinner.Create( AOwner: TComponent ); begin inherited Create( AOwner ); // Inicjacja pl FButtonColor := clBtnFace; FButtonWidth := 18; FValue := 0; FIncrement := 1; FMinusBtnDown := False; FPlusBtnDown := False; // Inicjacja odziedziczonych waciwoci Width := 80; Height := 18; TabStop := True; // VCL->CLX: TWidgetControl ustawia swj kolor na clNone Color := clWindow; // VCL->CLX: InputKeys zastpuje metod obsugi komunikatu // wm_GetDlgCode InputKeys := InputKeys + [ ikArrows ]; end;

{== Metody dostpowe waciwoci ==} procedure TddgSpinner.SetButtonColor( Value: TColor ); begin if FButtonColor <> Value then begin FButtonColor := Value; Invalidate; end; end; procedure TddgSpinner.SetButtonWidth( Value: Integer ); begin if FButtonWidth <> Value then begin FButtonWidth := Value; Invalidate; end; end;

procedure TddgSpinner.SetValue( Value: Integer ); begin if FValue <> Value then begin if CanChange( Value ) then begin FValue := Value; Invalidate; // Wygeneruj zdarzenie Change Change; end; end; end;

{== Metody zwizane z wywietlaniem ==} procedure TddgSpinner.Paint; var R: TRect; YOffset: Integer; S: string; XOffset: Integer; begin

// VCL->CLX:

dodane dla CLX

559

inherited Paint; with Canvas do begin Font := Self.Font; Pen.Color := clBtnShadow; if Enabled then Brush.Color := Self.Color else begin Brush.Color := clBtnFace; Font.Color := clBtnShadow; end; // Wywietl warto (* // VCL->CLX: SetTextAlign niedostpne w CLX SetTextAlign( Handle, ta_Center or ta_Top ); *)

// funkcja GDI

R := Rect( FButtonWidth - 1, 0, Width - FButtonWidth + 1, Height ); Canvas.Rectangle( R.Left, R.Top, R.Right, R.Bottom ); InflateRect( R, -1, -1 );

S := IntToStr( FValue ); YOffset := R.Top + ( R.Bottom - R.Top Canvas.TextHeight( S ) ) div 2; // VCL->CLX: Oblicz offset (niedostpna funkcja SetTextAlign) XOffset := R.Left + ( R.Right - R.Left Canvas.TextWidth( S ) ) div 2; (* // VCL->CLX:

Zmie wywoanie TextRect (niedostpna funkcja SetTextAlign)

TextRect( R, Width div 2, YOffset, S ); *) TextRect( R, XOffset, YOffset, S ); DrawButton( btMinus, FMinusBtnDown, Rect( 0, 0, FButtonWidth, Height ) ); DrawButton( btPlus, FPlusBtnDown, Rect( Width - FButtonWidth, 0, Width, Height ) ); if Focused then begin Brush.Color := Self.Color; DrawFocusRect( R ); end; end; end; {= TddgSpinner.Paint =}

procedure TddgSpinner.DrawButton( Button: TddgButtonType; Down: Boolean; Bounds: TRect ); begin with Canvas do begin if Down then // ustaw kolor ta Brush.Color := clBtnShadow else Brush.Color := FButtonColor; Pen.Color := clBtnShadow; Rectangle( Bounds.Left, Bounds.Top, Bounds.Right, Bounds.Bottom ); if Enabled then begin (* // w CLX clActiveCaption ustawione jest na clActiveHighlightedText Pen.Color := clActiveCaption; Brush.Color := clActiveCaption; *) Pen.Color := clActiveBorder; Brush.Color := clActiveBorder;

560

end else begin Pen.Color := clBtnShadow; Brush.Color := clBtnShadow; end; if Button = btMinus then // wywietl przycisk dekrementujcy begin Rectangle( 4, Height div 2 - 1, FButtonWidth - 4, Height div 2 + 1 ); end else // wywietl przycisk inkrementujcy begin Rectangle( Width - FButtonWidth + 4, Height div 2 - 1, Width - 4, Height div 2 + 1 ); Rectangle( Width - FButtonWidth div 2 - 1, ( Height div 2 ) - (FButtonWidth div 2 - 4), Width - FButtonWidth div 2 + 1, ( Height div 2 ) + (FButtonWidth div 2 - 4) ); end; Pen.Color := clWindowText; Brush.Color := clWindow; end; end; {= TddgSpinner.DrawButton =}

procedure TddgSpinner.DoEnter; begin inherited DoEnter; // kontrolka przyjmuje skupienie - wywietl j ponownie, // by ukaza sygnalizujc to ramk Repaint; end; procedure TddgSpinner.DoExit; begin inherited DoExit; // kontrolka traci skupienie - wywietl j ponownie, // by usun ramk sygnalizujc skupienie Repaint; end;

// VCL->CLX: //

EnabledChanged zastpuje metod obsugi komunikatu cm_EnabledChanged

procedure TddgSpinner.EnabledChanged; begin inherited; // odwie obraz kontrolki stosownie do jej stanu Repaint; end;

{== Metody obsugi zdarze ==} {================================================================== TddgSpinner.CanChange Ta metoda obsuguje zdarzenie OnChanging; zwr uwag, i jest ona funkcj, nie procedur jak w VCL. Warto przypisywana zmiennej Result ustalana jest domylnie przed wywoaniem metody zdefiniowanej przez uytkownika. ==================================================================} function TddgSpinner.CanChange( NewValue: Integer ): Boolean; var AllowChange: Boolean; begin AllowChange := True; if Assigned( FOnChanging ) then FOnChanging( Self, NewValue, AllowChange ); Result := AllowChange; end;

561

procedure TddgSpinner.Change; begin if Assigned( FOnChange ) then FOnChange( Self ); end;

// zwr uwag, i ponisze metody modyfikuj waciwo Value, // nie za bezporednio pole FValue procedure TddgSpinner.DecValue( Amount: Integer ); begin Value := Value - Amount; end; procedure TddgSpinner.IncValue( Amount: Integer ); begin Value := Value + Amount; end;

{== Metody wsppracy z klawiatur ==} (* // VCL->CLX: //

Ponisz metod zastpuje przypisanie wartoci do pola InputKeys (w konstruktorze)

procedure TddgSpinner.WMGetDlgCode( var Msg: TWMGetDlgCode ); begin inherited; Msg.Result := dlgc_WantArrows; // kontrolka zdolna jest obsugiwa // klawisze strzaek end; *)

procedure TddgSpinner.KeyDown( var Key: Word; Shift: TShiftState ); begin inherited KeyDown( Key, Shift ); // VCL->CLX: // zmiana identyfikatorw w CLX przedrostek "vk_" zmieni si na "Key_"

case Key of Key_Left, Key_Down: DecValue( FIncrement ); Key_Up, Key_Right: IncValue( FIncrement ); end; end;

{== metody obsugi myszy ==} function TddgSpinner.CursorPosition: TPoint; begin GetCursorPos( Result ); Result := ScreenToClient( Result ); end;

function TddgSpinner.MouseOverButton(Btn: TddgButtonType): Boolean; var R: TRect; begin // uzyskaj granice odpowiedniego przycisku if Btn = btMinus then R := Rect( 0, 0, FButtonWidth, Height ) else R := Rect( Width - FButtonWidth, 0, Width, Height ); // czy kursor znajduje si w wyznaczonym obszarze? Result := PtInRect( R, CursorPosition ); end;

562

procedure TddgSpinner.MouseDown( Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin inherited MouseDown( Button, Shift, X, Y ); if not ( csDesigning in ComponentState ) then SetFocus; // przenie skupienie na spiner // tylko w czasie wykonania programu if ( Button = mbLeft ) and ( MouseOverButton(btMinus) or MouseOverButton(btPlus) ) then begin FMinusBtnDown := MouseOverButton( btMinus ); FPlusBtnDown := MouseOverButton( btPlus ); Repaint; end; end;

procedure TddgSpinner.MouseUp( Button: TMouseButton; Shift: TShiftState; X, Y: Integer ); begin inherited MouseUp( Button, Shift, X, Y ); if Button = mbLeft then begin if MouseOverButton( btPlus ) then IncValue( FIncrement ) else if MouseOverButton( btMinus ) then DecValue( FIncrement ); FMinusBtnDown := False; FPlusBtnDown := False; Repaint; end; end;

function TddgSpinner.DoMouseWheelDown( Shift: TShiftState; const MousePos: TPoint ): Boolean; begin inherited DoMouseWheelDown( Shift, MousePos ); DecValue( FIncrement ); Result := True; end;

function TddgSpinner.DoMouseWheelUp( Shift: TShiftState; const MousePos: TPoint ): Boolean; begin inherited DoMouseWheelUp( Shift, MousePos ); IncValue( FIncrement ); Result := True; end; end.

Jak wida, kod rdowy moduu niewiele rni si od wersji w VCL. Mimo i rnic jest niewiele, wikszo z nich jest jednak niezwykle istotna. Po pierwsze, zwr uwag na nazewnictwo moduw: Qt, QControls i QGraphics; pojawi si te nowy modu Types wsplny dla VCL i CLX. Po drugie, mimo i deklaracja klasy komponentu niewiele odbiega w swej postaci od wersji VCL pod wzgldem deklaracji pl i procedur zdarzeniowych nie ma w niej metod obsugujcych komunikaty, (wm_GetDlgCode i cm_EnableChanged). Kontrolka TControl (w CLX) zamiast wysya komunikat cm_EnableChanged przy zmianie jej waciwoci Enabled, wywouje po prostu (dynamiczn) metod EnableChanged() tote do niej przeniesiona zostaa tre wyeliminowanej metody komunikacyjnej. Podczas tworzenia komponentu czstym problemem jest obsuga klawiszy strzaek (na klawiaturze); w przypadku komponentu TddgSpinner powoduj one zmian reprezentowanej przez komponent wartoci. W

563

bibliotece VCL informacja o zestawie klawiszy obsugiwanych przez kontrolk przekazywana bya za pomoc komunikatu wm_GetDlgCode; w CLX nie ma komunikatw i trzeba znale rwnowane rozwizanie zastpcze. Tak si szczliwie skada, i kontrolka TWidgetControl definiuje w tym celu waciwo InputKeys, ktrej w konstruktorze przypisuje si stosown warto (i poszerza domylny repertuar obsugiwanych klawiszy o klawisze strzaek). Wynika std praktyczny wniosek, i komponenty VCL uywajce raczej procedur zdarzeniowych (i metod zarzdzajcych zdarzeniami) ni komunikatw atwiej poddaj si przenoszeniu na platform CLX. Po trzecie w konstruktorze, oprcz ustawienia waciwoci InputKeys, dokonywana jest korekta standardowego ustawienia koloru kontrolki. W VCL kontrolka TWinControl dziedziczy swj kolor od kontrolki macierzystej (co symbolizuje warto clWindow), tymczasem w CLX konstruktor klasy TWidgetControl ustawia kolor kontrolki na clNone; konieczna jest wic zmiana tego koloru na clWindow. Na pocztku niniejszego rozdziau stwierdzilimy, i umiejtnoci nabyte w trakcie projektowania komponentw VCL przydadz si w duym stopniu przy projektowaniu komponentw CLX. Istotnie deklaracje waciwoci, metody dostpowe, a nawet procedury zdarzeniowe nie rni si zasadniczo od tych uywanych przez komponenty VCL. Pewnym wyjtkiem w tym wzgldzie jest metoda Paint(), wymagajca nieco wicej przerbek. Pierwsz przyczyn tego stanu rzeczy jest nieobecno w CLX funkcji SetTextAlign(), ktra w wersji VCL dokonywaa wyrodkowania wywietlanego tekstu. Funkcja ta wymaga kontekstu urzdzenia GDI, dostpnego w VCL pod waciwoci Canvas.Handle i nieobecnego w CLX, gdzie wspomniana waciwo ma cakiem inne znaczenie wskazuje na obiekt odpowiedzialny za wywietlanie (painter). Odpowiednie pooenie tekstu mona jednak wyliczy rcznie, za pomoc dostpnych geometrycznych metod ptna. Kolejna ingerencja zwizana jest z kolorem, w ktrym (domylnie) zostayby wywietlone przyciski. Na obydwu platformach jest to kolor clActiveCaption, jednak w CLX warto ta utosamiana jest z clActiveHighlightedText (w module QGraphics.pas).
Wskazwka

Wszelkie operacje wykonywane na ptnie komponentu CLX poza jego metod Paint() musz by poprzedzone wywoaniem metody Canvas.Start(); po zakoczeniu rysowania naley wywoa metod Canvas.Stop().

Ostatnim z nieprzenonych elementw VCL, przysparzajcym czasem mnstwa kopotw, s kody wirtualnych klawiszy vk_xxxx, stanowice cz Windows API. CLX definiuje w ich miejsce cakowicie nowy zestaw staych rozpoczynajcych si od przedrostka Key_. W przypadku naszego komponentu nie jest to jednak duym problemem, ze wzgldu na ubogi repertuar klawiszy (4) obsugiwanych w sposb specyficzny. I tak oto uzyskalimy uniwersalny komponent, przydatny zarwno w Delphi 6, jak i w rodowisku Kyliksa. Najbardziej spektakularnym aspektem tej uniwersalnoci jest moliwo uycia tego samego kodu rdowego w obydwu rodowiskach!

Interakcja ze rodowiskiem komponent TddgDesignSpiner


Jak wida, przenoszenie komponentu do rodowiska CLX nie musi by wcale trudne (chocia odkrycie waciwoci InputKeys wymagao nieco wysiku). Kiedy jednak przystpimy do rozbudowy komponentu CLX, rnice pomidzy VCL i CLX stan si bardzo wyrane. Wydruk 13.2 przedstawia kod rdowy komponentu TddgDesignSpiner, pochodnego w stosunku do TddgSpinner. Na rysunku 13.4 wida wyranie, jak kursor myszy zmienia swj wygld, gdy znajdzie si nad jednym z przyciskw; rysunek 13.5 pokazuje natomiast zmian reprezentowanej wartoci na etapie projektowania (poprzez kliknicie jednego z przyciskw).

564

Rysunek 13.4. Zmiana wygldu kursora wywoana przez komponent TddgDesignSpiner

Rysunek 13.5. Edycja waciwoci komponentu TddgDesignSpiner na etapie projektowania aplikacji Wydruk 13.2. QddgDsnSpn.Pas kod rdowy komponentu TddgDesignSpiner
{================================================================== QddgDsnSpn Niniejszy modu implementuje komponent TddgDesignSpinner, wywodzcy si z TddgSpinner i dokonujcy zmiany wygldu kursora w czasie, gdy kursor ten znajduje si nad jednym z przyciskw. Moliwa jest ponadto zmiana waciwoci Value na etapie projektowania, poprzez kliknicie przycisku Copyright 2001 by Ray Konopka ==================================================================} unit QddgDsnSpn; interface uses SysUtils, Classes, Qt, QddgSpin; (* Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, ddgSpin; *) type TddgDesignSpinner = class( TddgSpinner ) private

565

// VCL->CLX: Custom cursor stored in QCursorH field FThumbCursor: QCursorH; (* // VCL->CLX: // // Obsuga kursorw i interakcja z IDE obsugiwane s w CLX zupenie inaczej ni w VCL. Poniszy blok jest specyficzny dla VCL

FThumbCursor: HCursor; // Obsuga komunikatu Windows procedure WMSetCursor( var Msg : TWMSetCursor ); message wm_SetCursor; // Obsuga komunikatu komponentu procedure CMDesignHitTest( var Msg: TCMDesignHitTest ); message cm_DesignHitTest; *) protected procedure Change; override; // VCL->CLX: Ponisze metody s przedefiniowane w CLX procedure MouseMove( Shift: TShiftState; X, Y: Integer ); override; function DesignEventQuery( Sender: QObjectH; Event: QEventH ): Boolean; override; public constructor Create( AOwner: TComponent ); override; destructor Destroy; override; end; implementation (* // VCL->CLX: CLX nie obsuguje zasobw kursorowych {$R DdgDsnSpn.res} // przycza zasb zawierajcy kursor *) uses Types, QControls, QForms; // VCL->CLX: //

// VCL->CLX:

moduy CLX

Two arrays of bytes (one for the image and one for the mask) are used to represent custom cursors in CLX

// VCL->CLX: //

Ponisze dwie tablice reprezentuj w CLX bitmapy kursora

const Bits: array[0..32*4-1] of Byte = ( $00, $30, $00, $00, $00, $48, $00, $00, $48, $00, $00, $00, $48, $00, $00, $48, $00, $00, $00, $4E, $00, $00, $49, $C0, $00, $00, $49, $30, $00, $49, $28, $00, $03, $49, $24, $04, $C0, $24, $00, $04, $40, $04, $02, $40, $04, $00, $02, $00, $04, $01, $00, $04, $00, $01, $00, $04, $00, $80, $08, $00, $00, $40, $08, $00, $40, $08, $00, $00, $20, $10, $00, $20, $10, $00, $00, $7F, $F8, $00, $7F, $F8, $00, $00, $7F, $E8, $00, $7F, $F8, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00,

$00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 );

Mask: array[0..32*4-1] of Byte = ( $00, $30, $00, $00, $00, $78, $00, $00, $00, $78, $00, $00, $00, $78, $00, $00, $00, $78, $00, $00, $00, $7E, $00, $00,

566

$00, $00, $07, $03, $01, $00, $00, $00, $00, $00, $00, $00, $00,

$7F, $7F, $FF, $FF, $FF, $FF, $7F, $3F, $7F, $7F, $00, $00, $00,

$C0, $F8, $FC, $FC, $FC, $F8, $F8, $F0, $F8, $F8, $00, $00, $00,

$00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00,

$00, $03, $07, $03, $01, $00, $00, $00, $00, $00, $00, $00, $00,

$7F, $7F, $FF, $FF, $FF, $7F, $3F, $7F, $7F, $00, $00, $00, $00,

$F0, $FC, $FC, $FC, $FC, $F8, $F0, $F8, $E8, $00, $00, $00, $00,

$00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 );

{===============================} {== Metody TddgDesignSpinner ==} {===============================} constructor TddgDesignSpinner.Create( AOwner: TComponent ); var BitsBitmap: QBitmapH; MaskBitmap: QBitmapH; begin inherited Create( AOwner ); (* // VCL->CLX: W CLX nie ma adowania kursora z zasobu FThumbCursor := LoadCursor( HInstance, 'DdgDSNSPN_BTNCURSOR' ); *) // VCL->CLX: Tworzenie kursora na podstawie tablic BitsBitmap := QBitmap_create( 32, 32, @Bits, False ); MaskBitmap := QBitmap_create( 32, 32, @Mask, False ); try FThumbCursor := QCursor_create( BitsBitmap, MaskBitmap, 8, 0 ); finally QBitmap_destroy( BitsBitmap ); QBitmap_destroy( MaskBitmap ); end; end;

destructor TddgDesignSpinner.Destroy; begin (* VCL->CLX: QCursor_Destroy zamiast DestroyCursor DestroyCursor( FThumbCursor ); // zwolnij obiekt GDI *) QCursor_Destroy( FThumbCursor ); inherited Destroy; end;

// gdy kursor znajdzie si nad jednym z przyciskw, zmie jego wygld (* // VCL->CLX: w CLX nie ma komunikatw procedure TddgDesignSpinner.WMSetCursor( var Msg: TWMSetCursor ); begin if MouseOverButton( btMinus ) or MouseOverButton( btPlus ) then SetCursor( FThumbCursor ) else inherited; end; *) // VCL->CLX: Przedefiniowanie metody MouseMove w celu obsugi zmiany kursora

procedure TddgDesignSpinner.MouseMove( Shift: TShiftState; X, Y: Integer ); begin if MouseOverButton( btMinus ) or MouseOverButton( btPlus ) then QWidget_setCursor( Handle, FThumbCursor ) else QWidget_UnsetCursor( Handle );

567

inherited; end;

(* // VCL->CLX: // W CXL nie ma komunikatw. Uyj w zamian metody DesignEventQuery.

procedure TddgDesignSpinner.CMDesignHitTest( var Msg: TCMDesignHitTest ); begin

// // // // // //

Obsugujc ten komunikat pozwalamy na zmian waciwoci Value na etapie projektowania za pomoc lewego przycisku myszy. Gdy kursor myszy znajdzie si nad jednym z przyciskw, wartoci zwrotn komunikatu bdzie 1 - stanowi to dla Delphi instrukcj, by przekazywa do komponentu obsug zdarze zwizanych z mysz

if MouseOverButton( btMinus ) or MouseOverButton( btPlus ) then Msg.Result := 1 else Msg.Result := 0; end; *)

function TddgDesignSpinner.DesignEventQuery( Sender: QObjectH; Event: QEventH ): Boolean; var MousePos: TPoint; begin Result := False; if ( Sender = Handle ) and ( QEvent_type(Event) in [QEventType_MouseButtonPress, QEventType_MouseButtonRelease, QEventType_MouseButtonDblClick]) then begin // Note: bieca pozycja kursora myszy nie jest w tym przypadku // istotna, pokazujemy jednak, jak j obliczy. MousePos := Point( QMouseEvent_x( QMouseEventH( Event ) ), QMouseEvent_y( QMouseEventH( Event ) ) ); if MouseOverButton( btMinus ) or MouseOverButton( btPlus ) then Result := True else Result := False; end; end;

procedure TddgDesignSpinner.Change; var Form: TCustomForm; begin inherited Change;

// Uaktualnij wywietlan w inspektorze obiektw warto // waciwoci Value if csDesigning in ComponentState then begin Form := GetParentForm( Self ); (* // VCL->CLX: Form.Designer zastpiono przez DesignerHook if ( Form <> nil ) and ( Form.Designer <> nil ) then Form.Designer.Modified; *)

568

if ( Form <> nil ) and ( Form.DesignerHook <> nil ) then Form.DesignerHook.Modified; end; end; end.

Po przeanalizowaniu komentarzy ponownie mona zauway wyeliminowanie kodu zwizanego z systemem komunikatw Windows. Dla kontrolki Windows sygnaem do zmiany wygldu kursora jest otrzymanie przez ni komunikatu wm_SetCursor, w odpowiedzi na co powinna wywoa funkcj SetCursor() z odpowiednim parametrem. W CLX nie ma komunikatw, trzeba zatem kontrolowa pooenie kursora za pomoc zdarzenia OnMouseMove; gdy kursor znajdzie si nad jednym z przyciskw, naley nada kursorowi dany wygld za pomoc funkcji QWidget_setCursor(), w przeciwnym razie zapewni jego ksztat domylny przez wywoanie funkcji QWidget_UnsetCursor(). Osobnym problemem jest samo okrelenie ksztatu kursora. W Windows robi si to bardzo prosto, na przykad przez wczytanie odpowiedniego zasobu za pomoc funkcji LoadCursor(). W bibliotece Qt przeciona funkcja QCursor_create() udostpnia rnorodne sposoby tworzenia kursorw, nie przewidujc jednak wykorzystania w tym celu zasobw. Rozwizaniem zastpczym (ktre wykorzystalimy w naszym przykadzie) jest wwczas zdefiniowanie dwch bitmap, z ktrych pierwsza okrela rozmieszczenie czarnych i biaych pikseli w obrazie kursora, druga natomiast stanowi mask okrelajc jego przezroczyste regiony. Kolejne zadanie polega na spowodowaniu przejcia przez komponent (niektrych) zdarze myszy na etapie projektowania. Komponent VCL sygnalizuje gotowo do takiej obsugi, zwracajc warto 1 w odpowiedzi na komunikat cm_DesignHitTest. W CLX analogiczne zadanie spenia dynamiczna metoda DesignEventQuery(), zwracajca wynik typu Boolean; komponent powinien j przedefiniowa stosownie do swej specyfiki. I ostatni problem skoro komponent TddgDesignSpiner posiada zdolno modyfikowania swej waciwoci Value, modyfikacja ta nie moe odbywa si bez wiedzy inspektora obiektw. W VCL mechanizmem zapewniajcym aplikacji czno z projektantem formularzy jest interfejs ukrywajcy si pod waciwoci Designer formularza bdcego wacicielem komponentu; w CLX analogiczna waciwo nosi nazw DesignerHook. Aby zasygnalizowa zmian zawartoci komponentu, naley wywoa metod Modified wspomnianego interfejsu, w ramach obsugi zdarzenia OnChange robi si to tak samo w VCL i CLX.

Wykorzystanie bitmap komponent TddgImgListSpinner


Kolejne rozszerzenie naszego komponentu (ktry nosi teraz nazw TddgImgListSpinner) polega na wyposaeniu go w bitmapy okrelajce wygld obydwu przyciskw. Bitmapy te stanowi elementy listy typu ImageList (patrz rysunek 13.6) i wywietlane s zamiast zwykych znakw + i .

569

Rysunek 13.6. Bitmapy nadajce wygld przyciskom komponentu TddgImgListSpinner Kod rdowy moduu implementujcego komponent jest przedstawiony na wydruku 13.3; w porwnaniu z wydrukiem 13.2 zmiany wynikajce z przejcia na platform CLX s znacznie mniejsze. Wydruk 13.3. QddgILSpin.pas kod rdowy komponentu TddgImgListSpinner
{================================================================== QddgILSpin

Niniejszy modu implementuje komponent TddgImgListSpinner wywodzcy si z TddgDesignSpinner. Wygld jego przyciskw okrelony jest przez bitmapy stanowice zawarto stowarzyszonej z nim listy. Copyright 2001 by Ray Konopka ==================================================================} unit QddgILSpin; interface uses Classes, Types, QddgSpin, QddgDsnSpn, QImgList; (* Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, ddgSpin, ddgDsnSpn, ImgList; *) type TddgImgListSpinner = class( TddgDesignSpinner ) private FImages: TCustomImageList; FImageIndexes: array[ 1..2 ] of Integer; FImageChangeLink: TChangeLink; // Wewntrzne procedury obsugi zdarze procedure ImageListChange( Sender: TObject ); protected procedure Notification( AComponent : TComponent; Operation : TOperation ); override; procedure DrawButton( Button: TddgButtonType; Down: Boolean; Bounds: TRect ); override; procedure CalcCenterOffsets( Bounds: TRect; var L, T: Integer); procedure CheckMinSize;

570

// Metody dostpowe waciwoci procedure SetImages( Value: TCustomImageList ); virtual; function GetImageIndex( PropIndex: Integer ): Integer; virtual; procedure SetImageIndex( PropIndex: Integer; Value: Integer ); virtual; public constructor Create( AOwner: TComponent ); override; destructor Destroy; override; published property Images: TCustomImageList read FImages write SetImages; property ImageIndexMinus: Integer index 1 read GetImageIndex write SetImageIndex; property ImageIndexPlus: Integer index 2 read GetImageIndex write SetImageIndex; end; implementation uses QGraphics;

// VCL->CLX:

Modu CLX

{================================} {== Metody TddgImgListSpinner ==} {================================} constructor TddgImgListSpinner.Create( AOwner: TComponent ); begin inherited Create( AOwner ); FImageChangeLink := TChangeLink.Create; FImageChangeLink.OnChange := ImageListChange; // poniewa uytkownik komponentu nie ma bezporedniego dostpu // do obiektu TChangeLink, nie moe samodzielnie przypisywa // procedur obsugi jego zdarzeniom FImageIndexes[ 1 ] := -1; FImageIndexes[ 2 ] := -1; end;

destructor TddgImgListSpinner.Destroy; begin FImageChangeLink.Free; inherited Destroy; end;

procedure TddgImgListSpinner.Notification( AComponent: TComponent; Operation: TOperation ); begin inherited Notification( AComponent, Operation ); if ( Operation = opRemove ) and ( AComponent = FImages ) then SetImages( nil ); // wywoanie metody dostpowej end;

function TddgImgListSpinner.GetImageIndex( PropIndex: Integer ): Integer; begin Result := FImageIndexes[ PropIndex ]; end; procedure TddgImgListSpinner.SetImageIndex( PropIndex: Integer; Value: Integer ); begin if FImageIndexes[ PropIndex ] <> Value then begin

571

FImageIndexes[ PropIndex ] := Value; Invalidate; end; end;

procedure TddgImgListSpinner.SetImages( Value: TCustomImageList ); begin if FImages <> nil then FImages.UnRegisterChanges( FImageChangeLink ); FImages := Value; if FImages <> nil then begin FImages.RegisterChanges( FImageChangeLink ); FImages.FreeNotification( Self ); CheckMinSize; end; Invalidate; end; procedure TddgImgListSpinner.ImageListChange( Sender: TObject ); begin if Sender = Images then begin CheckMinSize; // Wywoaj Update, zamiast Invalidate, by zapobiec // nadmiernemu migotaniu Update; end; end; procedure TddgImgListSpinner.CheckMinSize; begin // zapewnij tak wielko kadego z przyciskw, by pomieci // ca bitmap if FImages.Width > ButtonWidth then ButtonWidth := FImages.Width; if FImages.Height > Height then Height := FImages.Height; end;

procedure TddgImgListSpinner.DrawButton( Button: TddgButtonType; Down: Boolean; Bounds: TRect ); var L, T: Integer; begin with Canvas do begin Brush.Color := ButtonColor; Pen.Color := clBtnShadow; Rectangle( Bounds.Left, Bounds.Top, Bounds.Right, Bounds.Bottom ); if Button = btMinus then // wywietl bitmap przycisku "minus" begin if ( Images <> nil ) and ( ImageIndexMinus <> -1 ) then begin (* // VCL->CLX: Lista ImageList w CLX nie umoliwia wyboru stylu rysowania // uyj w zamian waciwoci BkColor. if Down then FImages.DrawingStyle := dsSelected else FImages.DrawingStyle := dsNormal; *) if Down then FImages.BkColor := clBtnShadow else FImages.BkColor := clBtnFace; CalcCenterOffsets( Bounds, L, T );

572

(* // VCL->CLX: TImageList.Draw ma w CLX inne parametry FImages.Draw( Canvas, L, T, ImageIndexMinus, Enabled ); *) FImages.Draw( Canvas, L, T, ImageIndexMinus, itImage, Enabled ); end else inherited DrawButton( Button, Down, Bounds ); end else // wywietl bitmap przycisku "plus" begin if ( Images <> nil ) and ( ImageIndexPlus <> -1 ) then begin (* // VCL->CLX: Lista ImageList w CLX nie umoliwia wyboru stylu rysowania // uyj w zamian waciwoci BkColor. if Down then FImages.DrawingStyle := dsSelected else FImages.DrawingStyle := dsNormal; *) if Down then FImages.BkColor := clBtnShadow else FImages.BkColor := clBtnFace; CalcCenterOffsets( Bounds, L, T ); (* // VCL->CLX: TImageList.Draw ma w CLX inne parametry FImages.Draw( Canvas, L, T, ImageIndexPlus, Enabled ); *) FImages.Draw( Canvas, L, T, ImageIndexPlus, itImage, Enabled ); end else inherited DrawButton( Button, Down, Bounds ); end; end; end; {= TddgImgListSpinner.DrawButton =}

procedure TddgImgListSpinner.CalcCenterOffsets( Bounds: TRect; var L, T: Integer ); begin if FImages <> nil then begin L := Bounds.Left + ( Bounds.Right - Bounds.Left ) div 2 ( FImages.Width div 2 ); T := Bounds.Top + ( Bounds.Bottom - Bounds.Top ) div 2 ( FImages.Height div 2 ); end; end; end.

Lista obrazkw TImageList, w VCL stanowica otoczk standardowej kontrolki implementowanej w bibliotece ComCtl32.Dll, w CLX zaimplementowana zostaa w zupenie inny sposb, za pomoc tzw. prymityww graficznych biblioteki Qt. T now implementacj zawiera modu QImgList, ktry tym samym zastpi na licie uses modu ImgList. Nie zmieni si jednak zasadniczo sposb korzystania z listy, ani te nazwa jej klasy, czego wymiern korzyci jest niezmieniona deklaracja klasy komponentu (w stosunku do jego wersji VCL). Identyczne s te metody komponentu w obydwu wersjach, z jednym wszake wyjtkiem. Wyjtkiem tym jest metoda DrawButton(), rnica w stosunku do VCL wynika z faktu, i w CLX lista TImageList nie operuje pojciem stanu przycisku (zwolniony, nacinity, itp.) reprezentowanego w VCL przez waciwo DrawingStyle, stan ten naley wic rozrnia samodzielnie poprzez zmian koloru ta, ktry reprezentowany jest przez waciwo BkColor. Ponadto metoda TImageList.Draw() ma w CLX nieco inne parametry ni w VCL, inne jest wic jej wywoanie.

573

Wsppraca z baz danych komponent TddgDBSpinner


Skoro nadalimy estetyczny wygld przyciskom komponentu, warto teraz poeksperymentowa z przechowywan przez niego wartoci, a raczej z jej rdem. Kolejny komponent pochodny TddgDBSpinner czerpie t warto z pola zbioru danych, ktre reprezentowane jest przez jego waciwoci DataSource i DataField. Rysunek 13.7 przedstawia komponent TddgDBSpinner skojarzony z polem VenueNo zbioru Events.

Rysunek 13.7. Komponent TddgDBSpinner uywany do edycji pola bazy danych Kod rdowy moduu implementujcego komponent zosta przedstawiony na wydruku 13.4. Wydruk 13.4. QddgDBSpin.Pas kod rdowy komponentu TddgDBSpinner
{================================================================== QddgDBSpin Niniejszy modu implementuje komponent TddgDBSpinner. Ilustruje on sposb skojarzenia komponentu TddgImgListSpinner z polem bazy danych. Copyright 2001 by Ray Konopka ==================================================================} unit QddgDBSpin; interface uses SysUtils, Classes, Qt, QddgILSpin, DB, QDBCtrls; (* Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, ddgILSpin, DB, DBCtrls; *) type TddgDBSpinner = class( TddgImgListSpinner ) private FDataLink: TFieldDataLink; // zapewnia dostp do danych // wewntrzne procedury zdarzeniowe procedure DataChange( Sender: TObject ); procedure UpdateData( Sender: TObject ); procedure ActiveChange( Sender: TObject ); (* // VCL->CLX: w CLX nie ma komunikatw procedure CMExit( var Msg: TCMExit ); message cm_Exit;

574

procedure CMDesignHitTest( var Msg: TCMDesignHitTest ); message cm_DesignHitTest; *) protected procedure Notification( AComponent : TComponent; Operation : TOperation ); override; procedure CheckFieldType( const Value: string ); virtual; // przedefiniowane metody zarzdzajce zdarzeniami procedure Change; override; procedure KeyPress( var Key : Char ); override; // VCL->CLX: DoExit zamiast CMExit procedure DoExit; override; // VCL->CLX: DesignEventQuery zamiast CMDesignHitTest function DesignEventQuery( Sender: QObjectH; Event: QEventH ): Boolean; override; // przedefiniowane metody modyfikujce waciwo Value procedure DecValue( Amount: Integer ); override; procedure IncValue( Amount: Integer ); override; // metody dostpowe waciwoci function GetField: TField; virtual; function GetDataField: string; virtual; procedure SetDataField( const Value: string ); virtual; function GetDataSource: TDataSource; virtual; procedure SetDataSource( Value: TDataSource ); virtual; function GetReadOnly: Boolean; virtual; procedure SetReadOnly( Value: Boolean ); virtual; // dostp do pola i do cznika danych property Field: TField read GetField; property DataLink: TFieldDataLink read FDataLink; public constructor Create( AOwner: TComponent ); override; destructor Destroy; override; published property DataField: string read GetDataField write SetDataField; property DataSource: TDataSource read GetDataSource write SetDataSource; // ta metoda steruje dostpnoci cznika do zapisu property ReadOnly: Boolean read GetReadOnly write SetReadOnly default False; end; type EInvalidFieldType = class( Exception ); resourcestring SInvalidFieldType = 'Skojarzone pole danych musi mie typ ' + 'Integer, Smallint, Word lub Float';

implementation uses Types;

// VCL->CLX:

Modu CLX

{===========================} {== Metody TddgDBSpinner ==} {===========================} constructor TddgDBSpinner.Create( AOwner: TComponent );

575

begin inherited Create( AOwner ); FDataLink := TFieldDataLink.Create; // poinformuj cznik danych, i spiner jest kontrolk // stowarzyszon z polem danych FDataLink.Control := Self; // przyporzdkuj procedury zdarzeniowe; uytkownik nie ma bezporedniego // dostpu do cznika i nie moe tego zrobi samodzielnie FDataLink.OnDataChange := DataChange; FDataLink.OnUpdateData := UpdateData; FDataLink.OnActiveChange := ActiveChange; end;

destructor TddgDBSpinner.Destroy; begin FDataLink.Free; FDataLink := nil; inherited Destroy; end;

procedure TddgDBSpinner.Notification( AComponent: TComponent; Operation: TOperation ); begin inherited Notification( AComponent, Operation ); if ( Operation = opRemove ) and ( FDataLink <> nil ) and ( AComponent = FDataLink.DataSource ) then begin DataSource := nil; // porednio wywouje SetDataSource end; end;

function TddgDBSpinner.GetField: TField; begin Result := FDataLink.Field; end;

function TddgDBSpinner.GetDataField: string; begin Result := FDataLink.FieldName; end; procedure TddgDBSpinner.SetDataField( const Value: string ); begin CheckFieldType( Value ); FDataLink.FieldName := Value; end;

function TddgDBSpinner.GetDataSource: TDataSource; begin Result := FDataLink.DataSource; end; procedure TddgDBSpinner.SetDataSource( Value: TDataSource ); begin if FDatalink.DataSource <> Value then begin FDataLink.DataSource := Value; // Wywoanie FreeNotification jest konieczne, poniewa komponent // reprezentujcy zbir danych moe znajdowa si w innym formularzu // lub module danych

if Value <> nil then Value.FreeNotification( Self );

576

end; end;

function TddgDBSpinner.GetReadOnly: Boolean; begin Result := FDataLink.ReadOnly; end; procedure TddgDBSpinner.SetReadOnly( Value: Boolean ); begin FDataLink.ReadOnly := Value; end;

procedure TddgDBSpinner.CheckFieldType( const Value: string ); var FieldType: TFieldType; begin // sprawd, czy skojarzone pole bazy danych jest typu // ftInteger, ftSmallInt, ftWord lub ftFLoat - jeeli nie, // wygeneruj wyjtek if ( Value <> '' ) and ( FDataLink <> nil ) and ( FDataLink.Dataset <> nil ) and ( FDataLink.Dataset.Active ) then begin FieldType := FDataLink.Dataset.FieldByName( Value ).DataType; if ( FieldType <> ftInteger ) and ( FieldType <> ftSmallInt ) and ( FieldType <> ftWord ) and ( FieldType <> ftFloat ) then begin raise EInvalidFieldType.Create( SInvalidFieldType ); end; end; end;

procedure TddgDBSpinner.Change; begin // poinformuj cznik, e zmieniy si dane if FDataLink <> nil then FDataLink.Modified; inherited Change; // Generuje zdarzenie OnChange end;

procedure TddgDBSpinner.KeyPress( var Key: Char ); begin inherited KeyPress( Key ); if Key = #27 then // czy nacinito ESC ? begin FDataLink.Reset; // tak, anuluj to // Esc key pressed Key := #0; // nie nastpio zakoczenie edycji end; end;

nacinicie,

by

procedure TddgDBSpinner.DecValue( Amount: Integer ); begin if ReadOnly or not FDataLink.CanModify then begin // uniemoliwienie niedozwolonych zmian (* // VCL->CLX: MessageBeep is a Windows API function MessageBeep( 0 ) *) Beep; end else begin // ustaw zbir danych w tryb edycji i zmodyfikuj warto

577

if FDataLink.Edit then inherited DecValue( Amount ); end; end;

procedure TddgDBSpinner.IncValue( Amount: Integer ); begin if ReadOnly or not FDataLink.CanModify then begin // uniemoliwienie niedozwolonych zmian (* // VCL->CLX: MessageBeep is a Windows API function MessageBeep( 0 ) *) Beep; end else begin // ustaw zbir danych w tryb edycji i zmodyfikuj warto if FDataLink.Edit then inherited IncValue( Amount ); end; end;

{================================================================== TddgDBSpinner.DataChange

Niniejsza metoda moe zosta wywoana z rozmaitych powodw: 1. 2. 3. 4. 5. 6. Zmienia si warto pola stowarzyszonego z kontrolk Stowarzyszony zbir danych przeczany jest w tryb edycji Zmienia si zawarto stowarzyszonego zbioru danych Zmienia si biecy rekord w zbiorze danych Rekord zostaje zresetowany przez wywoanie metody Cancel Waciwo DataField zmienia wskazanie na inne pole

==================================================================} procedure TddgDBSpinner.DataChange( Sender: TObject ); begin if FDataLink.Field <> nil then Value := FDataLink.Field.AsInteger; end;

{================================================================== TddgDBSpinner.UpdateData Niniejsza metoda wywoywana jest w sytuacji, gdy zawarto skojarzonego pola i zawarto kontrolki wymagaj synchronizacji. Wywoywana tylko wtedy, gdy kontrolka jest w stanie zmienia zawarto pola. ==================================================================} procedure TddgDBSpinner.UpdateData( Sender: TObject ); begin FDataLink.Field.AsInteger := Value; end;

{================================================================== TddgDBSpinner.ActiveChange Niniejsza metoda wywoywana jest w momencie zamykania lub otwierania zbioru danych, tj. gdy zmienia si jego waciwo Active. Nowy stan otwarcia zbioru danych mona odczyta z waciwoci FDataLink.Active ==================================================================} procedure TddgDBSpinner.ActiveChange( Sender: TObject ); begin

578

// przy otwieraniu zbioru danych sprawd typ skojarzonego pola if ( FDataLink <> nil ) and FDataLink.Active then CheckFieldType( DataField ); end;

(* // VCL->CLX:

DoExit zamiast CMExit

procedure TddgDBSpinner.CMExit( var Msg: TCMExit ); begin try // Prba uaktualnienia rekordu, gdy spiner traci skupienie FDataLink.UpdateRecord; except SetFocus; // nie pozwl na utrat skupienia, // gdy aktualizacja si nie powioda raise; end; inherited; end; *) // ponw wyjtek

procedure TddgDBSpinner.DoExit; begin try // Prba uaktualnienia rekordu, gdy spiner traci skupienie FDataLink.UpdateRecord; except SetFocus; // nie pozwl na utrat skupienia, // gdy aktualizacja si nie powioda

raise; end; inherited; end;

// ponw wyjtek

(* // VCL->CLX:

DesignEventQuery zamiast CMDesignHitTest

procedure TddgDBSpinner.CMDesignHitTest(var Msg: TCMDesignHitTest); begin // Tym razem naley zablokowa moliwo edycji wartoci // na etapie projektowania, gdy wizaoby si to z koniecznoci // przestawienia skojarzonego zbioru danych w tryb edycji Msg.Result := 0; end; *) function TddgDBSpinner.DesignEventQuery( Sender: QObjectH; Event: QEventH ): Boolean; begin // Tym razem naley zablokowa moliwo edycji wartoci // na etapie projektowania, gdy wizaoby si to z koniecznoci // przestawienia skojarzonego zbioru danych w tryb edycji Result := False; end; end.

Kojarzenie komponentu z polem danych przebiega tu niemal identycznie jak w VCL. Mamy wic cznik z polem danych (TFieldDataLink) i obsug zdarze OnDataChange i OnUpdateData; konkretne pole reprezentowane jest przez waciwoci DataSource i DataField, za dopuszczalno zmian w zbiorze danych kontrolowana jest przez waciwo ReadOnly.

579

Notatka

Zwr uwag, i zamiast moduu DBCtrls wykorzystywany jest modu QDBCtrls. Obydwa te moduy implementuj klas TFieldDataLink, jednake prba uycia moduu DBCtrls pod Kyliksem da w efekcie mnstwo bdw syntaktycznych.

Jedyna rnica w stosunku do VCL wynika (znowu) z nieobecnoci komunikatw w CLX i dotyczy komunikatu cm_Exit, w odpowiedzi na ktry wikszo komponentw bazodanowych dokonuje aktualizacji kontrolowanych przez siebie pl, wywoujc metod UpdateRecord. W CLX analogiczn rol peni metoda DoExit(). Jak pamitamy, przodek naszego bazodanowego komponentu TddgImgListSpinner posiada moliwo modyfikacji kontrolowanej przez siebie wielkoci na etapie projektowania. W stosunku do komponentw bazodanowych mechanizm taki traci jednak racj bytu z prostego powodu: ot rozpoczcie edycji odnonej wartoci (tu ukrywajcej si pod waciwoci Value) spowoduje przeczenie skojarzonego zbioru danych w tryb edycji, ktrego nie da si opuci na etapie projektowania. Komponent TddgDBSpinner konsekwentnie odegnuje si wic od wszelkich prb samodzielnej obsugi zdarze na etapie projektowania, niezmiennie zwracajc False jako wynik metody DesignEventQuery().

Edytory rodowiskowe CLX


Edytory komponentw CLX i ich waciwoci implementowane s podobnie, jak ich odpowiedniki w VCL, cho oczywicie wystpuj pewne rnice. Na przykad modu DsgnIntf zmieni sw nazw na DesignIntf; ponadto w wikszoci przypadkw konieczne bdzie doczenie do listy uses moduu DesignEditors, gdy modu DesignIntf zawiera definicje interfejsw wykorzystywanych przez projektanta formularzy i inspektor obiektw, za w module DesignEditors zawarta jest implementacja podstawowych klas edytorw komponentw i edytorw waciwoci. Niestety, nie wszystkie charakterystyczne dla VCL mechanizmy IDE przeniesione zostay do CLX nie ma na przykad edytorw waciwoci charakteryzujcych si specyficzn form graficzn (owner-drawing property editors). Generalnie implementacj edytorw charakterystycznych dla CLX zawiera modu CLXEditors, za edytorw specyficznych dla VCL modu VCLEditors. Rysunek 13.8 przedstawia efekt dziaania specjalizowanego edytora komponentu TRadioGroup TddgRadioGroupEditor. Pozwala on w wygodny sposb edytowa waciwo ItemIndex. Jego kod rdowy jest przedstawiony na wydruku 13.5.

Rysunek 13.8. Uatwiony wybr pozycji z listy komponentu TRadioGroup

580

Wydruk 13.5. QddgRgpEdt.pas kod rdowy edytora TddgRadioGroupEditor


{================================================================== QddgRgpEdt - Edytor TddgRadioGroupEditor Copyright 2001 by Ray Konopka ==================================================================} unit QddgRgpEdt; interface uses DesignIntf, DesignEditors, QExtCtrls, QDdgDsnEdt; type TddgRadioGroupEditor = class( TddgDefaultEditor ) protected function RadioGroup: TRadioGroup; virtual; public function GetVerbCount: Integer; override; function GetVerb( Index: Integer ) : string; override; procedure ExecuteVerb( Index: Integer ); override; end;

implementation uses QControls; {==================================} {== Metody TddgRadioGroupEditor ==} {==================================} function TddgRadioGroupEditor.RadioGroup: TRadioGroup; begin // pomocnicza funkcja zapewniajca wygodny dostp do edytowanego // komponentu; przy okazji daje gwarancj, i komponent ten // naley do klasy TRadioGroup lub pochodnej Result := Component as TRadioGroup; end; function TddgRadioGroupEditor.GetVerbCount: Integer; begin // zwraca liczb nowych opcji menu do wywietlenia Result := RadioGroup.Items.Count + 1; end;

function TddgRadioGroupEditor.GetVerb( Index: Integer ): string; begin // tekst opcji menu kontekstowego if Index = 0 then Result := 'Edit Items...' else Result := RadioGroup.Items[ Index - 1 ]; end;

procedure TddgRadioGroupEditor.ExecuteVerb( Index: Integer ); begin if Index = 0 then EditPropertyByName( 'Items' ) // zdefiniowane w QDdgDsnEdt.pas else begin if RadioGroup.ItemIndex <> Index - 1 then RadioGroup.ItemIndex := Index - 1 else RadioGroup.ItemIndex := -1; // usu zaznaczenie Designer.Modified; end; end; end.

581

Efektem dziaania specjalizowanego edytora TddgRadioGroupEditor jest wzbogacenie menu kontekstowego komponentu TRadioGroup w etykiety poszczeglnych jego elementw; wybranie ktrego elementu w menu kontekstowym powoduje jego zaznaczenie (w ramach komponentu). Oprcz tego do menu kontekstowego dodawana jest opcja Edit items poprzedzajca etykiety elementw i powodujca uruchomienie standardowego edytora pozycji uruchomienie nastpuje w wyniku wywoania metody EditPropertyByName() klasy TddgDefaultEditor. Metoda ta, otrzymujc nazw waciwoci edytowanego komponentu, wywouje aktualnie przypisany do niej edytor (zarejestrowany w rodowisku IDE). Klasa edytora TddgDefaultEditor zdefiniowana jest w module QddgDsnEdt.pas, ktrego tre przedstawia wydruk 13.6. Wydruk 13.6. QddgDsnEdt.pas kod rdowy edytora TddgDefaultEditor
{================================================================== QddgDsnEdt - Definicja klasy TddgDefaultEditor Copyright 2001 by Ray Konopka ==================================================================} unit QddgDsnEdt; interface uses Classes, DesignIntf, DesignEditors; type TddgDefaultEditor = class( TDefaultEditor ) private FPropName: string; FContinue: Boolean; FPropEditor: IProperty; procedure EnumPropertyEditors(const PropertyEditor: IProperty); procedure TestPropertyEditor( const PropertyEditor: IProperty; var Continue: Boolean ); protected procedure EditPropertyByName( const APropName: string ); end;

implementation uses SysUtils, TypInfo; {===============================} {== Metody TddgDefaultEditor ==} {===============================} procedure TddgDefaultEditor.EnumPropertyEditors( const PropertyEditor: IProperty ); begin if FContinue then TestPropertyEditor( PropertyEditor, FContinue ); end;

procedure TddgDefaultEditor.TestPropertyEditor( const PropertyEditor: IProperty; var Continue: Boolean ); begin if not Assigned( FPropEditor ) and ( CompareText( PropertyEditor.GetName, FPropName ) = 0 ) then begin Continue := False; FPropEditor := PropertyEditor; end; end;

procedure TddgDefaultEditor.EditPropertyByName( const

582

APropName: string ); var Components: IDesignerSelections; begin Components := TDesignerSelections.Create; FContinue := True; FPropName := APropName; Components.Add( Component ); FPropEditor := nil; try GetComponentProperties( Components, tkAny, Designer, EnumPropertyEditors ); if Assigned( FPropEditor ) then FPropEditor.Edit; finally FPropEditor := nil; end; end; end.

Pakiety
Nonikami komponentw CLX przeznaczonych do rejestracji w IDE Delphi 6 lub Kyliksa s pakiety, podobnie jak w przypadku komponentw VCL. Naley jednak wyranie zaznaczy, i pakiety skompilowane w Delphi 6 nie mog by instalowane w Kyliksie z powodu rnic w implementacji pakiety windowsowe maj posta specyficznych bibliotek DLL, podczas gdy w rodowisku Linuksa pakiety implementowane s jako tzw. obiekty wspdzielone (shared objects) w postaci plikw .so. Format i skadnia pliku rdowego pakietu s jednak takie same w obydwu rodowiskach. Zawarto pliku rdowego pakietu rni si nieco w obydwu rodowiskach, na przykad lista dyrektywy
requires zawiera w Linuksie odwoanie do pakietu baseclx, nieobecnego w Delphi 6. Na licie tej, podobnie

jak w VCL, powinny znale si wszystkie pakiety zawierajce instalowane komponenty CLX.

Konwencje nazewnicze
Wykorzystywane na uytek tego rozdziau komponenty zawarte s w pakietach wymienionych w tabelach 13.1 i 13.2. Obydwie tabele zawieraj nazwy pakietw (w postaci rdowej i skompilowanej) oraz nazwy innych pakietw wymaganych do instalacji odpowiednio dla Delphi 6 i Kyliksa.

Tabela 13.1. Przykadowe pakiety CLX dla Delphi 6 Plik rdowy


QddgSamples.dpk QddgSamples_Dsgn.dpk

Plik skompilowany
QddgSamples60.bpl QddgSamples_Dsgn60.bpl

Pakiety wymagane
visualclx visualclx designide QddgSamples

QddgDBSamples.dpk

QddgDBSamples60.bpl

visualclx dbrtl visualdbclx QddgSamples

QddgDBSamples_Dsgn.dpk

QddgDBSamples_Dsgn60.bpl

visualclx QddgSamples_Dsgn QddgSamples

583

Tabela 13.2. Przykadowe pakiety CLX dla Kyliksa Plik rdowy


QddgSamples.dpk

Plik skompilowany
bplQddgSamples.so.6

Pakiety wymagane
baseclx visualclx

QddgSamples_Dsgn.dpk

bplQddgSamples_Dsgn.so.6

baseclx visualclx designide QddgSamples

QddgDBSamples.dpk

bplQddgDBSamples.so.6

baseclx visualclx visualdbclx dataclx QddgSamples

QddgDBSamples_Dsgn.dpk

bplQddgDBSamples_Dsgn.so.6

baseclx visualclx QddgSamples_Dsgn QddgSamples

Jak wida, odpowiednio nazw pakietu rdowego i skompilowanego rzdzi si pewnymi (zwyczajowymi) reguami, rnymi dla Windows i Linuksa. W Delphi 6 do nazwy pliku rdowego dodawany jest przyrostek 60, podkrelajcy przynaleno pakietu do konkretnej wersji. Zauwamy, e w poprzednich wersjach Delphi nazwa pakietu skompilowanego bya tosama z jego nazw rdow; w Delphi 6, w celu zapewnienia przenonoci kodu, dodano kilka dyrektyw umoliwiajcych ksztatowanie nazwy wynikowej przez dodawanie przedrostkw i (lub) przyrostkw do nazwy rdowej. Na wydruku 13.7 nietrudno odnale dyrektyw $LIBSUFFIX ustalajc przyrostek nazwy w windowsowej wersji pakietu. Mimo i Borland nadaje niektrym pakietom nazwy rozpoczynajce si od dcl (by wskaza, i mamy do czynienia z pakietem rodowiskowym), staramy si tego unika w naszych przykadach, stosujc w zamian przyrostek _Dsgn. Wszystkie skompilowane pakiety windowsowe (rodowiskowe i wykonywalne) posiadaj rozszerzenie .bpl. W Linuksie t konwencj realizuje poprzedzenie nazwy pakietu przyrostkiem bpl decyduje o tym dyrektywa $SOPREFIX, ktr nietrudno odnale na wydruku 13.7; ponadto konkretna wersja (skompilowanego) pakietu znajduje odzwierciedlenie w ostatnim czonie nazwy jego pliku, zgodnie z dyrektyw $SOVERSION.

Pakiety wykonywalne
Wydruki 13.7 i 13.8 przedstawiaj kod rdowy pakietw zwizanych z przykadowymi komponentami wykorzystywanymi w niniejszym rozdziale. Zwr uwag na symbole kompilacji warunkowej MSWINDOWS i LINUX pierwszy z nich obowizujcy jest podczas kompilacji pakietu w Delphi 6, drugi podczas kompilacji w Kyliksie. Wydruk 13.7. QddgSamples.dpk plik rdowy pakietu wykonywalnego dla komponentw nie wsppracujcych z baz danych
package QddgSamples; {$R *.res} {$ALIGN 8} {$ASSERTIONS ON} {$BOOLEVAL OFF} {$DEBUGINFO ON}

584

{$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} {$LOCALSYMBOLS ON} {$LONGSTRINGS ON} {$OPENSTRINGS ON} {$OPTIMIZATION ON} {$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} {$REFERENCEINFO OFF} {$SAFEDIVIDE OFF} {$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST ON} {$MINENUMSIZE 1} {$IMAGEBASE $400000} {$DESCRIPTION 'DDG: CLX Components'} {$IFDEF MSWINDOWS} {$LIBSUFFIX '60'} {$ENDIF} {$IFDEF LINUX} {$SOPREFIX 'bpl'} {$SOVERSION '6'} {$ENDIF} {$RUNONLY} {$IMPLICITBUILD OFF} requires {$IFDEF LINUX} baseclx, {$ENDIF} visualclx; contains QddgSpin in 'QddgSpin.pas', QddgDsnSpn in 'QddgDsnSpn.pas', QddgILSpin in 'QddgILSpin.pas'; end.

Wskazwka

Uzaleniajc okrelone fragmenty kodu rdowego od konkretnej platformy, powinnimy posugiwa si odrbnymi konstrukcjami {$IFDEF}{$ENDIF} (jak na wydruku 13.7), a unika konstrukcji {$IFDEF}{$ELSE} w rodzaju {$IFDEF MSWINDOWS} // kod specyficzny dla Windows {$ELSE} // kod specyficzny dla Linuksa {$ENDIF} Dziki temu, jeeli w przyszoci Borland zaimplementuje w Delphi obsug take innych platform (poza Windows i Linuksem), kod przeznaczony dla Linuksa bdzie widoczny take dla kadej innej platformy niewindowsowej.

Wydruk 13.8. QddgDBSamples.dpk plik rdowy pakietu wykonywalnego dla komponentw bazodanowych
package QddgDBSamples; {$R *.res}

585

{$ALIGN 8} {$ASSERTIONS ON} {$BOOLEVAL OFF} {$DEBUGINFO ON} {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} {$LOCALSYMBOLS ON} {$LONGSTRINGS ON} {$OPENSTRINGS ON} {$OPTIMIZATION ON} {$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} {$REFERENCEINFO OFF} {$SAFEDIVIDE OFF} {$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST ON} {$MINENUMSIZE 1} {$IMAGEBASE $400000} {$DESCRIPTION 'DDG: CLX Components (Data-Aware)'} {$IFDEF MSWINDOWS} {$LIBSUFFIX '60'} {$ENDIF} {$IFDEF LINUX} {$SOPREFIX 'bpl'} {$SOVERSION '6'} {$ENDIF} {$RUNONLY} {$IMPLICITBUILD OFF} requires {$IFDEF MSWINDOWS} dbrtl, {$ENDIF} {$IFDEF LINUX} baseclx, dataclx, {$ENDIF} visualclx, visualdbclx, QddgSamples; contains QddgDBSpin in 'QddgDBSpin.pas'; end.

Pakiety rodowiskowe
Mimo i moliwe jest umieszczenie zaprojektowanych komponentw w pojedynczym pakiecie rodowiskowowykonywalnym, postpowanie takie nie jest zalecane. Jeeli bowiem pakiet taki zawiera edytor (komponentu lub waciwoci) zaprojektowany przez uytkownika, wymaga on do swego funkcjonowania pakietu designide ktry nie moe by rozpowszechniany wraz z gotow aplikacj. Naley wwczas stworzy dwa odrbne pakiety wykonywalny i rodowiskowy i powierzy pakietowi rodowiskowemu rejestracj komponentw wykorzystywanych przez pakiety wykonywalne. Wydruki 13.9 i 13.10 przedstawiaj tre plikw rdowych pakietw rodowiskowych, odpowiadajcych pakietom wykonywalnym prezentowanym na wydrukach 13.7 i 13.8. Wydruk 13.9. QddgSamples_Dsgn.dpk plik rdowy pakietu rodowiskowego dla komponentw nie wsppracujcych z baz danych
package QddgSamples_Dsgn;

586

{$R *.res} {$R 'QddgSamples_Reg.dcr'} {$ALIGN 8} {$ASSERTIONS OFF} {$BOOLEVAL OFF} {$DEBUGINFO OFF} {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} {$LOCALSYMBOLS OFF} {$LONGSTRINGS ON} {$OPENSTRINGS ON} {$OPTIMIZATION ON} {$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} {$REFERENCEINFO OFF} {$SAFEDIVIDE OFF} {$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST ON} {$MINENUMSIZE 1} {$IMAGEBASE $400000} {$DESCRIPTION 'DDG: CLX Components'} {$LIBSUFFIX '60'} {$LIBVERSION '6'} {$DESIGNONLY} {$IMPLICITBUILD OFF} requires {$IFDEF LINUX} baseclx, {$ENDIF} visualclx, designide, QddgSamples; contains QddgSamples_Reg in 'QddgSamples_Reg.pas', QddgDsnEdt in 'QddgDsnEdt.pas', QddgRgpEdt in 'QddgRgpEdt.pas'; end.

Ostrzeenie

Tworzc pojedynczy plik pakietu dla Delphi 6 i Kyliksa, musimy pamita o wraliwoci Linuksa na wielko liter w nazwach plikw. Nazwy odnonych pakietw (w dyrektywach requires i contains) powinny by zapisywane w swej wiernej postaci (a wic np. designide, nie DesignIDE), w przeciwnym razie kompilator Kyliksa nie bdzie mg zlokalizowa waciwego pliku pod Linuksem DesignIDE.dcp to nie to samo co designide.dcp.

Wydruk 13.10. QddgDBSamples_Dsgn.dpk plik rdowy pakietu rodowiskowego dla komponentw bazodanowych
package QddgDBSamples_Dsgn; {$R *.res} {$ALIGN 8} {$ASSERTIONS OFF} {$BOOLEVAL OFF} {$DEBUGINFO OFF} {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} {$LOCALSYMBOLS OFF} {$LONGSTRINGS ON} {$OPENSTRINGS ON} {$OPTIMIZATION ON}

587

{$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} {$REFERENCEINFO OFF} {$SAFEDIVIDE OFF} {$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST ON} {$MINENUMSIZE 1} {$IMAGEBASE $400000} {$DESCRIPTION 'DDG: CLX Components (Data-Aware)'} {$LIBSUFFIX '60'} {$LIBVERSION '6'} {$DESIGNONLY} {$IMPLICITBUILD OFF} requires {$IFDEF LINUX} baseclx, {$ENDIF} visualclx, QddgSamples_Dsgn, QddgDBSamples; contains QddgDBSamples_Reg in 'QddgDBSamples_Reg.pas'; end.

Moduy rejestracyjne
Podobnie jak komponenty VCL, take komponenty CLX zawarte w pakietach rodowiskowych wymagaj rejestracji. Rejestracj t wykonuje procedura Register() zawarta w jednym z moduw wymienionych w dyrektywie contains. Wydruk 13.11 prezentuje kod rdowy moduu rejestracyjnego QddgSamples_reg pakietu QddgSamples_Dsgn rejestracji podlegaj komponenty TddgSpinner, TddgDesignSpinner i TddgImgListSpinner oraz edytorTddgRadioGroupEditor.

Wydruk 13.11. Modu rejestracyjny pakietu QddgSamples_Dsgn


{================================================================== QddgSamples_Reg Modu rejestracyjny dla komponentw niebazodanowych Copyright 2001 by Ray Konopka ==================================================================} unit QddgSamples_Reg; interface procedure Register; implementation uses Classes, DesignIntf, DesignEditors, QExtCtrls, QddgSpin, QddgDsnSpn, QddgILSpin, QddgRgpEdt;

{=============================} {== procedura rejestracyjna ==} {=============================} procedure Register; begin {== rejestracja komponentw ==} RegisterComponents( 'DDG-CLX', [ TddgSpinner, TddgDesignSpinner,

588

TddgImgListSpinner ] ); {== rejestracja edytora komponentu ==} RegisterComponentEditor( TRadioGroup, TddgRadioGroupEditor ); end; end.

Ikony komponentw
Nowo stworzonym komponentom mona przyporzdkowa ikony identyfikujce je w palecie komponentw ikony te musz by 16-kolorowymi bitmapami o rozmiarze 2424 piksele. Zgodnie z sugestiami zawartymi w systemie pomocy Delphi i Kyliksa, naley utworzy odrbne zasoby bitmap dla kadego z komponentw. Tymczasem edytor pakietw dla kadego dodawanego do pakietu moduu .dcu poszukuje odpowiadajcego mu pliku .dcr nawet wtedy, gdy pakiet jest pakietem wykonywalnym; wspomniane bitmapy nie s w pakiecie wykonywalnym do niczego potrzebne i tylko bezproduktywnie zajmuj miejsce. Tak wic, zamiast tworzy osobne pliki .dcr dla kadego z komponentw, naley raczej utworzy pojedynczy plik z bitmapami dla wszystkich komponentw w pakiecie. Tak si szczliwie skada, e pliki zasobowe doczane do wykonywalnych plikw Linuksa maj format identyczny z plikami zasobowymi Win32 (mimo i same pliki wykonywalne rni si w obydwu tych rodowiskach). Mona wic, uywajc dowolnego edytora zasobw windowsowych, utworzy dany plik .res i zmieni jego rozszerzenie na .dcr. Edycj bitmapy dla jednego z opisywanych wczeniej komponentw przedstawia rysunek 13.9.

Rysunek 13.9. Edycja bitmapy zawartej w pliku .dcr Zwr uwag na to, i w obydwu naszych przykadowych pakietach rodowiskowych plik .dcr ma tak sam nazw jak odpowiedni modu rejestracyjny; umieszczajc wic ten ostatni w pakiecie, automatycznie powodujemy rwnie doczenie stosownej bitmapy. Dla pakietw wykonywalnych nie istniej moduy rejestracyjne, nie ma wic te niepotrzebnych bitmap.

589

Na zakoczenie jeszcze dobra rada: mimo i ikona reprezentujca komponent w palecie nie ma adnego wpywu na jego funkcjonowanie, nie mona nie docenia jej znaczenia. Jest ona wizytwk komponentu i ksztatuje pierwsze wyobraenie o nim; niedbaa wizytwka moe stwarza (by moe niesusznie) wraenie, i opatrzony ni komponent wykonany jest rwnie niedbale. Jak istotne jest to w przypadku komponentw wykonywanych dla celw komercyjnych, nie trzeba nikogo przekonywa

Podsumowanie
Niniejszy rozdzia powicilimy pewnemu rzec by mona: rozwojowemu aspektowi tworzenia aplikacji i komponentw w rodowisku typu RAD, mianowicie uwzgldnieniu przyszej ich migracji na platformy inne ni MS Windows. To wanie Delphi, jako pierwsze popularne narzdzie do byskawicznego tworzenia aplikacji, przekroczyo zaklt granic Windows, gdy pod postaci Kyliksa zaistniao w systemie Linux. Moliwo tworzenia aplikacji uniwersalnych, akceptowanych zarwno w Delphi, jak i w Kyliksie, pojawia si w Delphi 6 pod postaci biblioteki CLX, bdcej zestawem komponentw i zrealizowanej na podstawie midzyplatformowych mechanizmw biblioteki Qt. Tworzenie aplikacji midzyplatformowych wie si z pewnymi ograniczeniami w stosunku do aplikacji opartych na bibliotece VCL; ograniczenia te wynikaj po prostu z braku pewnych mechanizmw Windows w innych systemach operacyjnych, midzy innymi w Linuksie, i s naturaln cen pacon za uniwersalno. Nawet podczas tworzenia aplikacji przeznaczonych wycznie dla Windows warto zdawa sobie spraw z faktu, i (ewentualne) ich przystosowanie do wymogw CLX (w przyszoci) bdzie tym atwiejsze, w im wikszym stopniu respektowane bd owe ograniczenia. w respekt wyraa si powinien przede wszystkim w unikaniu (wszdzie, gdzie to tylko moliwe i akceptowalne) mechanizmw specyficznych dla Windows gwnie komunikatw, ktrych obsug naley zastpi metodami dyspozycyjnymi, oraz bezporednich odwoa do funkcji GDI, ktre powinny ustpi miejsca odpowiednim metodom ptna. Zaprezentowane w niniejszym rozdziale implementacje przykadowych komponentw obfituj w takie wanie eliminacje. Jednym z najistotniejszych przejaww uniwersalnoci aplikacji midzyplatformowej jest akceptowalno jej jedynego kodu rdowego na rnych platformach (na razie w Delphi 6 i Kyliksie). W sytuacji, gdy pewne rozwizania nie dadz si atwo zaprogramowa w sposb uniwersalny, moliwe jest wydzielenie fragmentw kodu dedykowanych tylko konkretnemu rodowisku; temu celowi su odpowiednie symbole kompilacji warunkowej (na razie MSWINDOWS i LINUX).

590

"Delphi 6. Vademecum profesjonalisty. Tom II" -- spis treci:


O Autorach (11) Przedmowa do wydania oryginalnego (13) Przedmowa do wydania polskiego (15) Cz V Zaawansowane wykorzystanie komponentw (17) Rozdzia 14. Pakiety (19)

Korzyci zwizane z uywaniem pakietw (19) o Redukcja kodu wynikowego (19) o Zmniejszenie rozmiaru dystrybuowanych plikw (20) o Pakiety jako zasobniki z komponentami (20) Kiedy nie opaca si uywa pakietw? (20) Typy pakietw (21) Pliki pakietu (21) Kompilacja aplikacji z podziaem na pakiety (21) Instalowanie pakietw w rodowisku IDE (22) Tworzenie i instalowanie wasnych pakietw (23) o Edytor pakietw (23) o Scenariusze projektowania pakietw (24) Wersjonowanie pakietw (27) Dyrektywy kompilacji zwizane z tworzeniem pakietw (28) o Sabe wizanie moduu w pakiecie (28) Konwencje nazewnictwa pakietw (29) Pakiety rozszerzajce funkcjonalno aplikacji (30) o Generowanie formularzy rozszerzajcych (30) Eksportowanie funkcji i procedur z pakietw (36) o Wywietlanie formularza zawartego w pakiecie (36) Uzyskiwanie informacji o pakiecie (39) Podsumowanie (42)

Rozdzia 15. OLE, COM i ActiveX (43)

Podstawy COM (43) o COM - model obiektu-komponentu (43) o COM kontra ActiveX kontra OLE (44) o Nieco terminologii (45) o C wspaniaego jest w ActiveX? (45) o OLE 1 kontra OLE 2 (45) o Pami strukturalna (46) o Jednolity transfer danych (46) o Modele wtkowe obiektu COM (46) o COM+ (47) Technologia COM a Object Pascal (47) o Interfejsy (47) o Szczegy korzystania z interfejsw COM w Delphi 6 (50) o Typ HRESULT (55) Klasy COM i obiekty-producenci (57) o Klasy TComObject i TComObjectFactory (57) o Wewntrzprocesowe serwery COM (58) o Zewntrzprocesowe serwery COM (61) o Agregacja obiektw COM (62) Rozproszona realizacja COM (DCOM) (63) Automatyzacja COM (64) o Interfejs IDispatch (64) o Informacja o typie obiektu automatyzacji (66) o Wczesne wizanie kontra pne wizanie (66)

601

Rejestracja (67) Tworzenie przykadowego serwera automatyzacji (67) Tworzenie aplikacji-kontrolerw automatyzacji (80) Zaawansowane techniki automatyzacji (87) o Zdarzenia automatyzacji (87) o Kolekcje automatyzacji (100) o Nowe typy interfejsw w bibliotece typu (108) o Wymiana danych binarnych (109) o Za kulisami, czyli elementy COM wbudowane w Object Pascal (111) TOleContainer (121) o Elementy podstawowe - prosta aplikacja demonstracyjna (122) o Mechanizmy zaawansowane - nieco wiksza aplikacja (123) Podsumowanie (132)

o o o

Rozdzia 16. Programowanie rozszerze powoki Windows (133)

Wsppraca aplikacji z zasobnikiem systemowym (133) o Funkcja Shell_NotifyIcon (133) o Zarzdzanie komunikatami (136) o Ikony i podpowiedzi (136) o Wspdziaanie myszy z zasobnikiem (137) o Ukrywanie i odkrywanie aplikacji (140) Paski narzdziowe aplikacji na pulpicie (147) o Formularz TAppBar - enkapsulacja paska aplikacji (148) o Przykad wykorzystania paska aplikacji (157) czniki powoki (shell links) (159) o Uzyskiwanie instancji interfejsu IShellLink (160) o Zastosowanie interfejsu IShellLink (160) o Przykadowa aplikacja (168) Serwery rozszerzajce powoki (shell extensions) (175) o Tworzenie obiektw COM serwerw rozszerzajcych (176) o Rozszerzenia typu Copy Hook (177) o Rozszerzenia typu Context Menu (182) o Rozszerzenia typu Icon (191) o Rozszerzenia typu Info Tip (199) Podsumowanie (205)

Rozdzia 17. Open Tools API (207)

Interfejsy Open Tools (207) Przykady zastosowa (210) o Prymitywny kreator ("Dumb Wizard") (210) o Kreator kreatorw (213) o DDG SEARCH (223) Kreatory formularzowe (233) Podsumowanie (240)

Cz VI Projektowanie aplikacji korporacyjnych (241) Rozdzia 18. Przetwarzanie transakcyjne - COM+/MTS (243)

Co to jest COM+? (243) Dlaczego COM? (243) Usugi (244) o Transakcje (244) o Bezpieczestwo (245) o Aktywacja natychmiastowa (250) o Komponenty kolejkowane (250) o Komasacja obiektw (257) o Zdarzenia (258) Mechanizmy wykonawcze (265) o Baza rejestracyjna (265)

602

Komponenty konfigurowane (265) Kontekst wykonawczy (266) Neutralno wtkowa (266) Tworzenie aplikacji COM+ (266) o Cel: skalowalno (266) o Kontekst wykonawczy (267) o Obiekty stanowe i bezstanowe (267) o Czas ycia obiektu a interfejsy (268) o Organizacja aplikacji COM+ (269) o Transakcje (269) o Zasoby (270) COM+ w Delphi (270) o Kreatory obiektw COM+ (270) o Szkielet aplikacji wykorzystujcej COM+ (271) o Przykadowa aplikacja (273) o ledzenie aplikacji COM+ (288) Podsumowanie (289)

o o o

Rozdzia 19. CORBA (291)

Moliwoci CORBA (291) Architektura CORBA (292) o OSAgent (293) o Interfejsy (294) IDL - jzyk opisu interfejsw (294) o Typy podstawowe (295) o Typy definiowane przez uytkownika (296) o Aliasy (296) o Wyliczenia (296) o Struktury (296) o Tablice (296) o Sekwencje (297) o Argumenty wywoania metod (297) o Moduy (297) Przykad: prosta aplikacja bankowa (298) Zoone typy danych (307) Delphi, CORBA i Enterprise Java Beans (EJBs) (313) o Troch teorii... (313) o EJB s specjalizowanymi komponentami (314) o EJB rezyduj wewntrz pojemnika (314) o EJB posiadaj predefiniowane API (314) o Interfejsy Home i Remote (314) o Rodzaje EJB (314) o Dostosowanie JBuildera 5 do tworzenia EJB (315) o Prosta aplikacja EJB (316) CORBA a usugi sieciowe (321) o Tworzenie usugi sieciowej (322) o Tworzenie aplikacji-klienta SOAP (323) o Umieszczenie klienta CORBA w serwerze WWW (325) Podsumowanie (328)

Rozdzia 20. BizSnap, SOAP i usugi sieciowe (329)

Czym s usugi sieciowe? (329) SOAP (330) Tworzenie usugi sieciowej (330) o Definiowanie interfejsu wywoywalnego (332) o Implementowanie interfejsu wywoywalnego (333) o Testowanie usugi sieciowej (334) Wywoywanie usugi sieciowej z aplikacji-klienta (336) o Generowanie moduu importowego dla zdalnego obiektu (337) o Konfigurowanie komponentu THTTPRIO (338) Podsumowanie (339)

603

Rozdzia 21. DataSnap vel MIDAS (341)

Zasady tworzenia aplikacji wielowarstwowych (341) Korzyci wynikajce z architektury wielowarstwowej (342) o Centralizacja logiki biznesowej (342) o Architektura "uproszczonego klienta" (343) o Automatyczne uzgadnianie bdw (343) o Model aktwki (343) o Odporno na bdy (343) o Rwnowaenie obcienia serwera (344) Typowa architektura DataSnap (344) o Serwer (344) o Klient (347) Tworzenie aplikacji DataSnap (349) o Tworzenie serwera (349) o Tworzenie klienta (351) Dodatkowe techniki optymalizowania aplikacji (357) o Techniki optymalizacji aplikacji-klienta (357) o Techniki optymalizacji serwera aplikacji (359) Przykadowe aplikacje (368) o Zczenia (368) Zaawansowane moliwoci komponentu TClientDataSet (378) o Aplikacje dwuwarstwowe (378) Klasyczne bdy (379) Udostpnianie i instalacja aplikacji DataSnap (380) o Licencjonowanie DataSnap (380) o Konfigurowanie DCOM (380) o Pliki wymagane przez aplikacj (381) o Kopot z Internetem - zapory (382) Podsumowanie (384)

Cz VII Tworzenie aplikacji internetowych (385) Rozdzia 22. Active Server Pages (387)

Active Server Objects (387) o Active Server Pages (387) Kreator obiektw ASO (389) o Edytor biblioteki typu (391) o Obiekt Response (394) o Pierwsze uruchomienie (395) o Obiekt Request (395) o Rekompilacja obiektw ASO (396) o Ponowne uruchomienie serwera ASP (397) Obiekty Session, Server i Application (398) Obiekty ASO i bazy danych (399) Obiekty ASO a NetCLX (402) ledzenie obiektw ASO (403) o ledzenie obiektw ASP za pomoc MTS (404) o Debugging ASO w Windows NT (405) o Debugging ASO w Windows 2000 (406) Podsumowanie (407)

Rozdzia 23. WebSnap (409)

Moliwoci WebSnap (409) o Wiele moduw danych (409) o Techniki skryptowe (409) o Komponenty-adaptery (410) o Wielokierunkowe zarzdzanie stronami (410) o Komponenty-producenci (410) o Zarzdzanie sesjami (410)

604

Usugi logowania (411) Zarzdzanie informacj o uytkownikach (411) Zarzdzanie zasobami rozproszonymi (411) Usugi adowania plikw (411) Tworzenie aplikacji WebSnap (411) o Projektowanie aplikacji (411) o Rozbudowa aplikacji (418) o Menu nawigacyjne (419) o Logowanie (422) o Zarzdzanie informacj o preferencjach uytkownikw (423) o Przechowywanie informacji pomidzy sesjami uytkownikw (427) o Przetwarzanie obrazw i obsuga grafiki (429) o Wywietlanie zawartoci bazy danych (430) o Konwertowanie aplikacji do postaci ISAPI DLL (434) Zagadnienia zaawansowane (435) o Komponent LocateFileService (435) o adowanie plikw (436) o Wykorzystanie specyficznych szablonw (438) o Wsppraca komponentu TAdapterPageProducer z komponentami tworzonymi przez uytkownikw (438) Podsumowanie (440)

o o o o

Rozdzia 24. Aplikacje dla telefonii bezprzewodowej (441)

Ewolucja oprogramowania - skd przychodzimy? (441) o Przed rokiem 1980: (442) o Pne lata 80.: biurkowe aplikacje bazodanowe (442) o Wczesne lata 90.: aplikacje klient-serwer (442) o Pne lata 90.: aplikacje wielowarstwowe i Internet (442) o Wiek XXI: "ruchoma" informatyka (443) Ruchome urzdzenia bezprzewodowe (443) o Telefony komrkowe (443) o Urzdzenia z systemem PalmOS (443) o PocketPC (444) o RIM BlackBerry (444) czno radiowa (444) o GSM, CDMA i TDMA (444) o CDPD (444) o 3G (445) o GPRS (445) o BlueTooth (445) o 802.11 (445) Serwerowe technologie bezprzewodowe (446) o SMS (446) o WAP (446) o I-mode (456) o PQA (456) Uytkowe aspekty aplikacji bezprzewodowych (459) o Komutacja obwodw kontra komutacja pakietw (459) o "Bezprzewodowy" nie znaczy "internetowy" (460) o Czynniki geometryczne (460) o Wprowadzanie danych i techniki nawigacji (460) o M-commerce (460) Podsumowanie (461)

Dodatki (463) Dodatek A Skrcony spis treci tomu 1 (465) Dodatek B Skorowidz tomu 1 (467) Skorowidz tomu 2 (485)

605

You might also like