Professional Documents
Culture Documents
Pacheco Teixeira - Delphi 6 - Vademecum Profesjonalisty - Tom I (Helion-PL)
Pacheco Teixeira - Delphi 6 - Vademecum Profesjonalisty - Tom I (Helion-PL)
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
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.
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.
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.
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.
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.
25
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.
29
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.
30
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
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.
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).
33
interface
type TForm1 = class(TForm) private { Private declarations } public { Public declarations } end;
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;
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
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:
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.
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.
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.
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.
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.
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.
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.
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.
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
// // // // //
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.
Ostrzeenie
Komentarze tej samej postaci nie mog by zagniedane, gdy jest to sprzeczne z reguami skadniowymi jzyka. Na przykad w poniszym przykadzie
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
48
... 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);
49
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);
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; ...
//
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)
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
ProsteSortowanieMetodPrzesiewaniaZOgraniczeniami
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";
Kompilator, ustalajc typ staej, wybiera typy o moliwie najmniejszej liczebnoci. Typ staej cakowitoliczbowej ustalany jest w nastpujcy sposb:
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);
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;
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
54
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);
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
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() ++
56
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
nie istnieje
short
Short
nie istnieje
int
nie istnieje
Integer, Long
nie istnieje
long float
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
Currency Date Variant
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
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
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
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 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.
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;
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;
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
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.
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
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;
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.
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';
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] := '*';
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;
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
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.
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.
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;
Nie to jest jednak najwaniejsze; jak zobaczymy za chwil, bezporednie operowanie polami struktury
TVarData niesie ze sob niebezpieczestwo dezorganizacji zarzdzania pamici.
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.
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);
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
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;
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.
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.
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;
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
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);
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
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;
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.
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;
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;
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;
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;
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;
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
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
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 := [];
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?
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 + [' ', '+', ''];
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
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;
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
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;
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
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
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.
{ 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:
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
91
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
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.
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;
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.
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).
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 );
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 &).
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.
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
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
= = = = = = = = = = = =
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:
: 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
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.
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.
105
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).
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.
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
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.
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.
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;
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;
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
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;
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
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
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;
117
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
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.
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:
123
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.
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.
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
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.
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.
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
152
Nacinito lewy przycisk myszy. Przesunito kursor myszy. Okno docelowe powinno odwiey swj obszar klienta (client area).
WM_TIMER WM_QUIT
$0113 $0012
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
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.
154
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;
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.
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}
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.
Powyszy przykad ma jednak znaczenie raczej teoretyczne, poniewa kady komponent posiada waciwo
Color, co uwalnia programist od bezporedniej obsugi komunikatu WM_CTLCOLOR.
powinien
mie
nastpujc
struktur
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
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.
Function PostMessage(hWnd: HWND; Msg: UINT; wParam : WPARAM; lParam : LPARAM): BOOL;stdcall;
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 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
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
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.
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.
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;
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
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
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;
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
W celu ilustracji obsugi komunikatu na poszczeglnych etapach skonstruowalimy przykadowy projekt o nazwie CatchIt; jego formularz gwny jest przedstawiony na rysunku 3.3.
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
// 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.
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
177
Rozdzia 4.
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
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.
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.
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
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.
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
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:
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.
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.
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}
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.
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.
185
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.
(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.
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.
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.
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.
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.
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.
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.
191
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
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.
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;
lub
S := 'Ala ma kota'; .... S := Copy(S,1,3);
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}
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.
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.
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().
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
Tabela 4.2. Rnice w rozmiarze zmiennych standardowych typw Typ Delphi 1 Delphi 2 i nastpne
Integer Cardinal String
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.
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
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.
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;
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.
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;
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.
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
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()
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
SetViewportExtEx() SetViewportOrgEx() SetWindowExtEx() SetWindowOrgEx()
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).
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):
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
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.
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():
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).
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
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
{ TTestThread }
procedure TTestThread.Execute; var k: Integer; begin FreeOnTerminate := True; for k := 1 to 2000000 do begin if Terminated then Break;
219
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.
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.
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;
implementation
221
{$R *.DFM}
end.
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
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
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.
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
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:
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.
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.
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.
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.
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;
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;
230
end.
Wskazwka
W powyszym przykadzie pomoglimy nieco przypadkowi, zwikszajc prawdopodobiestwo ingerencji drugiego wtku w prac pierwszego. Zastosowana przez nas procedura Sleep()
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.
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;
implementation
{$R *.DFM}
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;
implementation
{$R *.DFM}
var
235
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).
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
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;
implementation
238
{$R *.DFM}
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.
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.
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;
implementation
{$R *.DFM}
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
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().
244
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;
implementation
{$R *.DFM}
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
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
procedure TMainForm.Font1Click(Sender: TObject); { Wybr czcionki } begin if FontDialog.Execute then lbFiles.Font := FontDialog.Font; end;
249
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.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
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.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.
interface
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
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;
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
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.
258
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;
implementation
259
{$R *.DFM}
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.FormClose(Sender: TObject; var Action: TCloseAction); begin Action := caHide; 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
Na zaczonym krku CD-ROM znajduje si projekt o nazwie BdeThrd.dpr, ilustrujcy wielowtkowe zapytania; jego formularz gwny jest przedstawiony na rysunku 5.8.
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
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;
implementation
{$R *.DFM}
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
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 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 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
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.
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 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
Pen.Color := FColor;
269
// odblokuj ptno
{ TMainForm }
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;
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.
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:
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():
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.
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
implementation
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;
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;
implementation
uses Fibers;
{$R *.dfm}
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
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;
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)));
// 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
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;
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.
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.
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).
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.
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.
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.
279
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;
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).
280
interface type
{ poniszy rekord przechowuje liczb monet kadego rodzaju } PCoinsRec = ^TCoinsRec; TCoinsRec = record Quarters, Dimes, Nickels, Pennies: word; end;
{$IFNDEF PENNIESLIB}
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:
Alternatyw jest import oparty na indeksach przypisanych procedurom (funkcjom) w dyrektywie exports biblioteki DLL:
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.
282
interface
uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, Grids, Calendar;
type
{ deklaracja eksportowanej funkcji } function ShowCalendar(AHandle: THandle; ACaption: String): TDateTime; StdCall;
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
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!
284
interface
uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, Grids, Calendar;
type
{ deklaracja eksportowanych funkcji } function ShowCalendar(AHandle: THandle; ACaption: String): Longint; stdCall; procedure CloseCalendar(AFormRef: Longint); stdcall;
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;
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.
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;
{$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.
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;
implementation
{$R *.DFM}
{ 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 }
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:
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:
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
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
procedure DLLEntryExitProc(dwReason: DWord); begin case dwReason of DLL_PROCESS_ATTACH: begin ShowMessage('Przyczenie do procesu'); end;
DLL_THREAD_ATTACH:
DLL_THREAD_DETACH:
end;
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;
implementation
{$R *.DFM}
293
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 }
294
procedure TMainForm.btnFreeThreadClick(Sender: TObject); { koczenie wtku; powinno to powodowa wywoanie procedury inicjujco-koczcej KADEJ zaadowanej biblioteki DLL z parametrem DLL_THREAD_DETACH }
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.
3.
Zwolnienie wtku
4.
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.).
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.
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():
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
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;
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 }
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 }
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.
301
{ 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;
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;
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);
{ 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 }
%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.
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.
Wydruk 6.12. Biblioteka DLL umoliwiajca wspdzielenie swego obszaru danych przez rne procesy
library ShareLib;
{$I DLLDATA.INC}
{ 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
{ dokonaj mapowania w obszar pamici i przypisz adres tego obszaru do zmiennej GlobalData }
{ 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
DLLEntryPoint(DLL_PROCESS_ATTACH); 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.).
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;
{ zaimportuj procedur udostpniajc dane globalne } procedure GetDLLData(var AGlobalData: PGlobalDLLData); StdCall External 'SHARELIB.DLL';
implementation
{$R *.DFM}
{ 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;
{ uaktualnij zawarto danych globalnych } if meGlobDataInt.Text = EmptyStr then meGlobDataInt.Text := '0'; GlobalData^.I := StrToInt(meGlobDataInt.Text); end;
309
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';
implementation
{$R *.DFM}
310
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.
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.
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;
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.
implementation
constructor TStringConvert.Create(APrepend, AAppend: String); begin inherited Create; FPrepend := APrepend; FAppend end; := AAppend;
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:
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.
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;
314
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
315
Rozdzia 7.
317
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.
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 .
319
Table1.Active := True;
Czynnoci wieczc wszystkie operacje na zbiorze danych jest jego zamknicie, dokonywane przez wywoanie metody Close()
Table1.Close;
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.
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, FMTBcd, DBXpress, IBDatabase, ADODB, DBTables, DB, SqlExpr, IBCustomDataSet, IBQuery, IBTable, StdCtrls;
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;
implementation
{$R *.dfm}
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin CloseDatasets; IBDatabase1.Connected := false;
SQLConnection1.Connected := false;
321
end;
// 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;
// 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;
322
Table1.Open; Query1.Open;
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).
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
Przykad nawigowania wrd rekordw rnych zbiorw danych ilustruje kolejny projekt (DatasetNav.dpr), ktrego modu gwny przedstawiamy na wydruku 7.2.
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;
implementation
{$R *.dfm}
SQLConnection1.Connected := True;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin CloseDatasets; IBDatabase1.Connected := false;
325
BDE
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.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
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.
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;
implementation
{$R *.dfm}
330
end;
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction); begin adodsCustomer.Close; ADOConnection1.Connected := False; 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
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.
jest
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
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
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:
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
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.
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
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.
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.
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
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
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).
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.
Po otwarciu obydwu zbiorw (Table1 i Table2) ujrzymy zawarto nowego pola CustName (rys. 7.8):
340
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
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.
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.
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:
343
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
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
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.
interface
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, DBCtrls, DB, DBTables, StdCtrls, Mask, Buttons, ComCtrls;
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;
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
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
347
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
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'];
// 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
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.
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
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:
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:
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.
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;
// 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
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.
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.
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
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;
end.
Notatka
358
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().
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
implementation
{$R *.DFM}
{ 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 }
360
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 }
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 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).
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;
implementation
{$R *.DFM}
procedure TFilterForm.cbFilteredClick(Sender: TObject); begin { wcz filtrowanie gdy zaznaczona jest opcja "wykonaj filtrowanie" }
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
if not DM.Table1.Locate(CBField.Text, EValue.Text, LO) then MessageDlg('Brak dopasowania', mtInformation, [mbOk], 0); 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
Zmiana waciwoci Bookmark, przez przypisanie jej wartoci wczeniej zapamitanej, spowoduje ustawienie zbioru danych na pozycji odpowiadajcej tej wartoci:
Table1.Bookmark := BM;
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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
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.
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
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.
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;
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
Ustawienie na True waciwoci AutoSave komponentu TSQLMonitor powoduje samoczynne zapisywanie listy komunikatw w pliku o nazwie okrelonej przez waciwo FileName.
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.
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
387
Rozdzia 10.
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
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
391
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.
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.
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;
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.
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.
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
instrukcje
L := X.MaxLength; ...... X.MaxLength := 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.
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.
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).
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;
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;
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.
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.
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.
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).
400
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
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
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.
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.
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.
403
Metody zwizane z aktywnoci okna, jego pozycjonowaniem i wyrwnywaniem (alignment) to m.in. CanFocus(), Focused(), AlignControls(), EnableAlign(), DisableAlign() i Realign().
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().
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.
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
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.
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:
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.
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.
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).
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;
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.
415
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
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().
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.
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.
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
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.
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
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.
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.
426
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
Funkcja przypisujca
SetOrdProp() SetEnumProp() SetObjectProp() SetStrProp() SetFloatProp() SetVariantProp() SetMethodProp() SetInt64Prop()
Funkcja odczytujca
GetOrdProp() GetEnumProp() GetObjectProp() GetStrProp() GetFloatProp() GetVariantProp() GetMethodProp() GetInt64Prop()
zmiennoprzecinkowy
variant
zdarzeniowy
Int64
427
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.
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).
489
} } } }
{ 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');
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. }
// pobierz wsprzdne prostokta // opisanego na okienku podpowiedzi // obetnij 1 piksel z lewej strony // wypenij region tem
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
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.
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
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.
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;
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
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;
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;
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.
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.
503
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
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.
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.
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:
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
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
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.
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
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;
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.
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;
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.
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.
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.
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.
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
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.
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;
518
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;
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.
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
THelpCategory
TWinControl.Hint, TWinControl.HelpContext
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
TScrollBox.Align, TScrollBox.Visible
524
TInputCategory
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.
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'] );
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;
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.
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.
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.
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
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.
534
{ 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
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
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
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.
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:
// 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:
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:
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:
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: //
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
// 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;
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!
564
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: //
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;
// // // // // //
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
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;
// 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.
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;
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
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
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';
// 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;
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.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
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
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:
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
// ponw wyjtek
(* // VCL->CLX:
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().
580
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;
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.
Plik skompilowany
QddgSamples60.bpl QddgSamples_Dsgn60.bpl
Pakiety wymagane
visualclx visualclx designide QddgSamples
QddgDBSamples.dpk
QddgDBSamples60.bpl
QddgDBSamples_Dsgn.dpk
QddgDBSamples_Dsgn60.bpl
583
Plik skompilowany
bplQddgSamples.so.6
Pakiety wymagane
baseclx visualclx
QddgSamples_Dsgn.dpk
bplQddgSamples_Dsgn.so.6
QddgDBSamples.dpk
bplQddgDBSamples.so.6
QddgDBSamples_Dsgn.dpk
bplQddgDBSamples_Dsgn.so.6
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.
{=============================} {== 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
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)
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
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)
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
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)
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
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)
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
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