20 aplicatii Delphi si Visual Basic Informatica Programare

Citation preview

Bogdan Pătruţ

20 APLICAŢII DELPHI ŞI VISUAL BASIC

EduSoft Bacău – 2005

Redactor: Tiberiu Socaciu

Copyright © 2005 Editura EduSoft Toate drepturile asupra prezentei ediţii sunt rezervate Editurii EduSoft. Reproducerea parţială sau integrală a conţinutului, prin orice mijloc, fără acordul scris al Editurii EduSoft este interzisă şi se va pedepsi conform legislaţiei în vigoare. Editura EduSoft 600065 Bacău, str. 9 Mai, nr. 82, sc. C, ap. 13 E-mail: [email protected], Web: www.edusoft.ro

ISBN 973-87655-3-6

2

CUPRINS INTRODUCERE PARTEA I. APLICAŢII ÎN DELPHI 5 1. Ciupercile - un joc de îndemânare şi perspicacitate 7 2. Harta - un program despre România turistică 26 3. Mărfuri - un joc de logică 38 4. Puzzle - un joc de perspicacitate 49 5. Supraf - reprezentarea grafică a suprafeţelor 54 6. Parser - analiza sintactică şi lexicală a unei fraze 61 7. AutoWeb - Creator de pagini web 71 8. Zodiac - ce ne este scris în stele? 84 9. Gastro - program pentru reţete culinare 102 PARTEA II. APLICAŢII ÎN VISUAL BASIC 113 10. Calculator de buzunar 115 11. Prinde muştele 122 12. Vânătoarea de berze 128 13. Tetris 135 14. Puzzle cu numere 148 15. Puzzle cu imagini 151 16. Bila 158 17. Test grilă 163 18. Test de circulaţie rutieră 171 19. Bioritm 182 20. Editor de hărţi 196 BIBLIOGRAFIE 211

3

Introducere Prin această carte dorim să venim în sprijinul tuturor celor care programează sau doresc să programeze în mediul Windows şi care vor să realizeze aplicaţii Windows uşor şi repede. Această carte este o colecţie de aplicaţii dezvoltate în mediile de programare vizuală Delphi şi Visual Basic, realizate şi comercializate de firmele Inprise (Borland), respectiv Microsoft. Mediul de programare Delphi permite realizarea de programe care să se execute în Windows, cu interfeţe de tip Windows, pe baza unui limbaj de programare de tip Pascal, iar mediul de programare Visual Basic se bazează pe limbajul foarte simplu de învăţat Basic. Interfeţele aplicaţiilor vizuale se implementează cu uşurinţă, dar proiectarea şi dezvoltarea unor aplicaţii inteligente şi puternice nu se poate realiza fără un efort de gândire din partea programatorului. Aceasta presupune proiectarea şi implementarea în limbajul Object Pascal, respectiv Visual Basic a unor algoritmi eficienţi de rezolvare a problemelor în cauză. Aşadar, cartea se adresează programatorilor serioşi, care au cunoştinţele suficiente de programare obiectuală şi vizuală în mediile de programare menţionate. Dar chiar şi programatorii obişnuiţi cu medii de programare mai simple pot învăţa uşor programarea vizuală în Delphi sau Visual Basic, pe baza unor exemple ca cele din această lucrare. Exemplele de aplicaţii prezentate în această lucrare sunt diferite prin temele pe care le tratează. Am încercat să acoperim un număr suficient de situaţii pe care orice programator le-ar întâlni atunci când ar dori să elaboreze un program mai complex în Delphi sau Visual Basic. Cititorii care doresc să intre în posesia surselor programelor prezentate în această carte şi a fişierelor cu date (imagini, sunete, text) sunt rugaţi să ne viziteze site-ul http://edusoft.inf.ro. Cu convingerea că oricine va studia cu atenţie aplicaţiile prezentate în această carte va face din programarea vizuală o pasiune, le doresc cititorilor lectură plăcută şi compilare fără erori! Autorul

4

PARTEA I APLICAŢII ÎN DELPHI

5

6

Aplicaţia 1

Ciupercile - un joc de îndemânare şi perspicacitate 1.1. Prezentare generală Ne propunem să realizăm un joc de îndemânare şi perspicacitate clasic, pe care l-am denumit Ciupercile şi în care, mânuind un omuleţ printr-un labirint de ziduri şi scări, trebuie să culegem cu el nişte ciuperci amplasate în diferite poziţii ale acestui labirint. De asemenea, trebuie să ne ferim de duşmani, care în cazul acestui joc sunt nişte caracatiţe. Fireşte, deşi scenariul pare imposibil (ciuperci, caracatiţe, omuleţi, ziduri şi scări), trebuie să ne gândim că totul este doar un joc! Iată cum va arăta jocul nostru în timpul execuţiei programului:

7

Ceea ce observaţi dumneavoastră în imaginea de mai sus este un moment din timpul desfăşurării jocului pe cazul unui labirint dat (vezi LAB3.LBR). De fapt, vom creea, separat, cu un editor de texte simplu (de pildă Notepad) mai multe fişiere cu labirinturi, ca cele de mai jos: LAB1.LBR LAB2.LBR @@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@ @ @ @ * & * @ @ & ** * @ @@@@@@@#@@@@@@@@@@@#@@ @@@@@@@#@@@@@@@ @@#@@ @ * * # * # @ @ # # @ @@@#@@@@@@ @@#@@@@@@ @ & # # @ @ # # @ @@@@@@@@@@@@@@@#@@@@@@ @@@#@@ @@@@@@@@@@#@@ @ # @ @ # $ * * # @ @* $ # @ @ #@@@#@@@@@@#@@ # @ @@@#@@@@@@@@@@@@@@@@@@ @ # * # # # @ @ # * @ @ @#@@@# * @@@# @@@@ @ # @@@@@#@@ @ @ # @@@ # @ @ # * # @ @ &# # ** @ @ @#@@@@ # @ @@@@@@#@@@@ @@@@@@@@@ @ # & # & @ @ * &# & * @ @@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@ LAB3.LBR LAB4.LBR @@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@ @ * ** @ @ @ @@@@@@@#@@@ @@@@@#@@ @ **& * * @ @ ** & # * # @ @@@@@@@#@@@@@@@ @@#@@ @@@#@@@@@@ @@#@@@#@@ @ * # ** & # @ @* # * ** # * # @ @@@@#@@@@@ @@@#@#@@@@ @@@#@@@ @@#@@@@@@@#@@ @ *# # # @ @ # # & ** # @ @@@@#@ @@@@# # @ @ # @#@@@@@@#@@ # @ @* # $ # # * @ @ *# $# * &# *# @ @@@#@@@@@@@@@@#@@#@@@@ @ @#@@@# * @@@# @@@@ @ # * *# # @ @ # @@@ # @ @ #@@@@ @@@@#@@# @ @ &# * # & ** @ @ # & * *# # @ @@@@@@ @@#@ @#@@@@@@@ @ @#@@@@ @#@@@ @ @ * & # # & * @ @ # ** & # * @ @@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@

8

Aceste fişiere text conţin, sub o formă codificată, toate informaţiile necesare reprezentării atât a labirintului în care se desfăşoară acţiunea din jocul curent, cât şi poziţiile ciupercilor, poziţiile iniţiale ale caracatiţelor şi a omuleţului. Am spus "iniţiale", deoarece, o dată cu trecerea timpului, caracatiţele vor avea mişcări aleatorii prin labirint, vor merge pe diferitele niveluri şi vor trece de la un nivel la altul folosindu-se de scări. Mişcările lor nu vor fi controlate de jucător, ci de un cronometru (Timer1), pe când omuleţul va fi deplasat de către jucător, prin intermediul tastelor de cursor. Codificarea labirintului respectă anumite reguli, de aceea, recomandăm cititorului ca, înainte de a-şi crea propriile fişiere cu labirinturi (LBR), să le realizeze pe cele date ca model. Fiecare simbol folosit în fişierul LBR are o anumită semnificaţie: @ = zid; # = scară; * = ciupercă; & = caracatiţă; $ = omuleţul. Iată restricţiile folosite în crearea labirinturilor: • • • • • •

un labirint este o matrice cu 22 coloane şi 16 rânduri; labirintul este bordat pe margini cu ziduri (@); simbolul omuleţului apare o singură dată ($); scările (#) pleacă de deasupra unui zid şi urcă până la un alt nivel, exact între două ziduri; ciupercile (*) şi caracatiţele (&) stau pe ziduri; se va folosi simbolul ' ' (spaţiu) pentru a marca spaţiile în cadrul labirintului.

O altă restricţie impusă de textul programului, aşa după cum se va vedea, este ca numărul de caracatiţe să nu depăşească 10, iar numărul de ciuperci să nu fie mai mare de 30. Dacă veţi crea un labirint greşit, atunci veţi avea probleme şi de acest lucru vă veţi da seama în timpul execuţiei programului. Îl veţi opri şi veţi corecta labirintul până nu vor mai apărea probleme. De asemenea, puteţi porni de la un labirint prezentat şi îl modificaţi după dorinţă, respectând restricţiile de mai sus.

9

1.2. Textul explicat al programului În continuare vom prezenta unit-ul ciupercile1.pas, folosit în proiectul Delphi ciupercile.dpr. Acest unit conţine toate declaraţiile de variabile şi toate procedurile şi alte elemente folosite în cadrul aplicaţiei. Vom comenta fiecare din aceste proceduri şi algortmii pe care ele îi implementează. unit ciupercile1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, StdCtrls, ExtDlgs; type TForm1 = class(TForm) Timer1: TTimer; Button1: TButton; Label1: TLabel; Label2: TLabel; procedure Timer1Timer(Sender: TObject); procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure FormPaint(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1;

Declaraţiile anterioare definesc o formă simplă conţinând doar un buton, două etichete şi un cronometru:

10

Toate celelalte lucruri care vor apărea în joc vor fi desenate direct pe formă, folosind metodele de desenare ce vor acţiona asupra proprietăţii Canvas a formei Form1. Fireşte, imediat după începutul jocului, butonul Buton1, conţinând acel text explicativ, va dispărea. implementation {$R *.DFM}

În cadrul programului vom fi nevoiţi să desenăm mai multe figurine (omuleţul, ciupercile etc.). De aceea, vom folosi un procedeu de desenare relativă, pixel cu pixel, a unor curbe, implementat de procedura de mai jos. Am încadrat în chenar această procedură, pentru că o considerăm de interes general pentru cititor şi la fel vom proceda şi cu alte proceduri de acest gen, în cadrul cărţii. procedure DeseneazaCurba(x0,y0,c: Integer; s: String); var i: Byte; x,y: Integer; begin x:=x0; y:=y0; for i:=1 to Length(s) do begin case s[i] of '1': Dec(y); '2': begin Inc(x); Dec(y) end; '3': Inc(x); '4': begin Inc(x); Inc(y) end; '5': Inc(y);

11

'6': begin Dec(x); Inc(y) end; '7': Dec(x); '8': begin Dec(x); Dec(y) end end; Form1.Canvas.Pixels[x,y]:=c end end;

Această procedură desenează o curbă, punct cu punct, care are culoarea c. Punctul de plecare al curbei are coordonatele x0, y0, iar fiecare din următoarele puncte va avea coordonate în funcţie de coordonatele punctului precedent. În acest sens se va folosi o codificare prin cifrele '1'..'8', reprezentând direcţiile de "deplasare" pentru desenarea punctului următor.

De pildă, pentru a desena curba din următoarea figură, vom apela procedura anterioară astfel: DeseneazaCurba(x0,y0,clBlack,'023354').

Ciupercile şi omuleţul se deplasează prin labirint. Deplasarea va presupune ştergerea figurinei din poziţia veche şi redesenarea ei în noua poziţie. Ştergerea se va face cu ajutorul procedurii de mai 12

jos, ai cărei parametri indică zona dreptunghiulară care urmează să fie ştearsă, prin colorarea tuturor punctelor sale în culoarea fondului (aici clBtnFace). procedure ClearView(x1,y1,x2,y2: Integer); var i,j: Integer; begin for i:=x1 to x2 do for j:=y1 to y2 do Form1.Canvas.Pixels[i,j]:=clBtnFace end;

Urmează declaraţiile de constante, variabile şi tipuri de date referitoare la toate personajele din scenariul nostru: const lat=24;

Această variabilă reprezintă mărimea laturii oricărui pătrăţel din labirint (fie că este zid, scară sau altceva). Labrintul propriu-zis va fi stocat sub forma unei matrice cu 16 linii şi 22 coloane: var L: array[1..16,1..22] of Char;

Prin vieti am notat numărul de vieţi ale omuleţului, care va fi iniţial 10 şi va scădea de fiecare dată când o caracatiţă îl va întâlni. Xom, Yom sunt coordonatele curente ale omuleţului, iar Xom_i şi Yom_i sunt coordonatele sale iniţiale. var vieti, Xom, Yom, Xom_i, Yom_i: Integer;

Numărul maxim de ciuperci şi numărul maxim de caracatiţe este precizat prin declaraţiile: const max_ciup=30; max_carac=10;

Urmează declaraţiile unor tipuri de date obiectuale, TCiuperca şi TCaracatita. O ciupercă este caracterizată de coordonatele sale în labirint şi ca metode avem Init pentru iniţializare, Display pentru afişare şi Clear, pentru ştergere, folosită atunci când omuleţul a cules ciuperca în cauză. type TCiuperca = object x,y: Integer; procedure Init(x0,y0: Integer); procedure Display;

13

procedure Clear; end;

O caracatiţă are, în plus, un atribut mut care reprezintă sensul deplasării caracatiţei la un moment dat, deplasarea propriu-zisă făcându-se cu ajutorul metodei Move. După definirea tipului de date TCaracatita urmează declararea vectorilor cu ciuperci şi caracatiţe. type TCaracatita = object x,y,mut: Integer; procedure Init(x0,y0: Integer); procedure Display; procedure Clear; procedure Move; end; var Ciup: array[0..max_ciup] of TCiuperca; Carac: array[0..max_carac] of TCaracatita;

Vom avea nevoie şi de următoarele trei variabile, reprezentând respectiv numărul de ciuperci rămase în labirint, numărul iniţial de ciuperci, numărul de caracatiţe: var ciuperci,ciuperci_initiale, caracatite: Integer;

Desenarea omuleţului în pătrăţelul de coordonate i, j din matricea labirintului se va face cu procedura de mai jos, care apelează atât procedura DeseneazaCurba, cât şi ale metode grafice (FloodFill, Rectangle) ce scriu direct în proprietatea Canvas a formei Form1. procedure Omulet(i,j: Integer); begin DeseneazaCurba(lat*j+3,lat*i+11,clBlack, '0555443433336666653332321244443331'+ '8888833322221776676777777777888'); Form1.Canvas.Brush.Style:=bsSolid; Form1.Canvas.Brush.Color:=clBlue; Form1.Canvas.FloodFill(lat*j+4,lat*i+14, clBlack,fsBorder); Form1.Canvas.Brush.Color:=clBlue; Form1.Canvas.FloodFill(lat*j+12,lat*i+16, clBlack,fsBorder); DeseneazaCurba(lat*j+8,lat*i+7,clYellow, '0565454433333222118187777777');

14

Form1.Canvas.Brush.Color:=clYellow; Form1.Canvas.FloodFill(lat*j+10,lat*i+9, clYellow,fsBorder); Form1.Canvas.Pen.Color:=clGreen; Form1.Canvas.Pen.Width:=1; Form1.Canvas.Brush.Color:=clGreen; Form1.Canvas.Rectangle(lat*j+7,lat*i+3, lat*j+18,lat*i+6); Form1.Canvas.Pen.Color:=clWhite; Form1.Canvas.MoveTo(lat*j+11,lat*i+2); Form1.Canvas.LineTo(lat*j+14,lat*i+2); DeseneazaCurba(lat*j+10,lat*i+8,clFuchsia,'0357'); DeseneazaCurba(lat*j+14,lat*i+8,clFuchsia,'0357'); DeseneazaCurba(lat*j+10,lat*i+11,clRed,'03533313') end;

Procedura Tipar realizează afişarea informaţiei corespunzătoare unei celule a matricei labirintului, în funcţie de conţinutul acesteia (dat de L[i,j], unde i şi j sunt linia, respectiv coloana acelei celule). procedure Tipar(i,j: Integer); begin case L[i,j] of ' ': ClearView(lat*j+1,lat*i+1, lat*j+lat,lat*i+lat-1); '*': begin Ciup[0].Init(i,j); Ciup[0].Display end; { '&': begin Carac[0].Init(i,j); Carac[0].Display end; } '#': begin Form1.Canvas.Pen.Color:=clmaroon; Form1.Canvas.Pen.Width:=3; Form1.Canvas.Moveto(lat*j+lat div 3,lat*i); Form1.Canvas.LineTo(lat*j+lat div 3, lat*(i+1)-1); Form1.Canvas.MoveTo(lat*(j+1)-lat div 3, lat*i); Form1.Canvas.LineTo(lat*(j+1)-lat div 3, lat*(i+1)-1); Form1.Canvas.MoveTo(lat*j+lat div 3, lat*i+lat div 3); Form1.Canvas.LineTo(lat*(j+1)-lat div 3, lat*i+lat div 3) end; '@': begin

15

Form1.Canvas.Pen.Width:=1; Form1.Canvas.Pen.Color:=clMaroon; Form1.Canvas.Brush.Color:=clRed; Form1.Canvas.Brush.Style:=bsDiagCross; Form1.Canvas.Rectangle(lat*j+1,lat*i, lat*(j+1)-1,lat*(i+1)) end; '$': Omulet(i,j) end end;

În continuare, sunt prezentate cele trei metode ale obiectelor cele trei metode ale obiectelor TCiuperca: procedure TCiuperca.Init; begin x:=x0; y:=y0 end; procedure TCiuperca.Display; var i,j: Byte; begin i:=x; j:=y; DeseneazaCurba(lat*j+3,lat*i+9,clYellow, '0112223232323334334344445577678777777777767777'); Form1.Canvas.Brush.Color:=clYellow; Form1.Canvas.Brush.Style:=bsSolid; Form1.Canvas.FloodFill(lat*j+4,lat*i+8, clYellow,fsBorder); DeseneazaCurba(lat*j+5,lat*i+7,clRed,'0313557'); DeseneazaCurba(lat*j+13,lat*i+7,clRed,'01353'); DeseneazaCurba(lat*j+13,lat*i+3,clRed,'0753'); DeseneazaCurba(lat*j+18,lat*i+6,clRed,'0135'); DeseneazaCurba(lat*j+11,lat*i+10,clLime, '05556565656544333332212181818111177'); Form1.Canvas.Brush.Color:=clLime; Form1.Canvas.Brush.Style:=bsSolid; Form1.Canvas.FloodFill(lat*j+11,lat*i+17,clLime,fsBorder) end; procedure TCiuperca.Clear; begin ClearView(lat*y+1,lat*x+1,lat*y+lat-1,lat*x+lat-1) end;

De asemenea, TCaracatita:

avem

şi 16

metodele

obiectelor

de

tip

procedure TCaracatita.Init; begin x:=x0; y:=y0; mut:=Random(2); end;

În metoda de iniţializare, proprietatea (atributul) mut ia una din valorile 0 sau 1, pentru o deplasare pe orizontală. procedure TCaracatita.Display; var i,j: Byte; begin i:=x; j:=y; DeseneazaCurba(lat*j+1,lat*i+15,clBlue, '01222211112123132333433'+ '44455555644344877877644444554881717171717755'+ '44535558181188166557555611211111'+ '767757557561121222177675773123'); Form1.Canvas.Brush.Color:=clAqua; Form1.Canvas.Brush.Style:=bsSolid; Form1.Canvas.FloodFill(lat*j+10,lat*i+4, clBlue,fsBorder); DeseneazaCurba(lat*j+7,lat*i+6, clGreen,'02334557778333168'); DeseneazaCurba(lat*j+13,lat*i+6, clGreen,'0233455777825331') end;

procedure TCaracatita.Clear; begin ClearView(lat*y+1,lat*x+1,lat*y+lat-1,lat*x+lat-1); Tipar(x,y) end;

De remarcat că metoda de Clear a metodei TCaracatita se termină prin apelul procedurii Tipar, pentru a restabili conţinutul "de sub" caracatiţă din celula de unde pleacă acea caracatiţă (care ar putea fi spaţiu, scară sau ciupercă). Urmează descrierea metodei TCaracatita.Move de deplasare a unei caracatiţe. Ea apelează la o procedură internă cu numele Muta, ce are ca argument sensul deplasării (m: Integer). procedure TCaracatita.Move; procedure Muta(m: Integer); begin

17

Clear; Tipar(x,y); case m of 0: y:=y-1; 1: y:=y+1; 2: x:=x+1; 3: x:=x-1 end; Display end; begin case L[x,y] of ' ','*': if L[x+1,y]='@' then case mut of 0: if y>2 then if L[x+1,y-1] in ['@','#'] then Muta(mut) else mut:=1 else mut:=1; 1: if ysau Astfel, fraza considerată anterior conţine doar cuvinte din vocabularul ales. Pe de altă parte, fraza "orice câine urăşte o pisică", deşi corectă sintactic în limba română, nu este acceptată, deoarece conţine cuvinte ce nu sunt trecute în vocabularul nostru. Fişierul conţine, pe fiecare rând, o regulă de forma: categorie gramaticală (sau parte de vorbire) -> cuvânt din limba română

Regulile de sintaxă (din fişierul 'GRAM.TXT') sunt o submulţime a regulilor de sintaxă ale limbii române: S->NP VP NP->Pron NP->N NP->Det N NP->NP AP AP->A AP->AP CP CP->C A VP->V VP VP->V NP Astfel, folosind notaţiile consacrate (S (sentence) = propoziţie, NP (noun phrase) = grup verbal, VP (verb phrase) = grup verbal, N (noun) = substantiv, Det (determiner) = determinator (de pildă articol nehotărât), AP = grup adjectival, A (adjective) = adjectiv, C = 62

conjuncţie, V = verb, CP = grup format dintr-o conjuncţie şi un adjectiv. Regulile de sintaxă sunt date, câte una pe linie, astfel: categorie gramaticală 1 -> categ. gram. 2 categ. gram. 3, cu sensul că prima categorie gramaticală se formează din concatenarea celorlalte două, din dreapta săgeţii. Conform regulilor de sintaxă folosite în aplicaţia noastră, fraza considerată la început este corectă, pe când în figura următoare este prezentat un caz considerat incorect.

De remarcat că algoritmul implementat de noi foloseşte doar gramatici scrise în forma normală Chomsky (CNF). O gramatică CNF este o gramatică liberă de context, în care producţiile sunt de forma: A -> B C, în care A este un neterminal, iar B şi C sunt neterminali sau preterminali. De asemenea, am considerat ca acceptabile şi reguli de forma A->B. Aşadar, în partea dreaptă a regulilor de producţie vor fi două sau doar un singur element. Detalii referitoare la subiectul tratat, pentru cititorul neavizat, pot fi studiate în lucrări cu un pronunţat caracter ştiinţific, ca cele menţionate la bibliografie.

63

În continuare, vom prezenta algoritmul de bază de analiză (numită şi parsare "chart-parsing") elaborat de Cocke, Kasami şi Younger şi numit algoritmul CKY de bază. Algoritmul foloseşte o matrice (o diagramă) chart ca cele din figurile anterioare. Mai întâi avem nevoie de nişte definiţii: Se defineşte operaţia: Star(X,Y)=ÎC|(A este în X) şi (B este în Y) şi C->AB este o regulă din gramaticăŞ. Aceasta reprezintă faptul că produsul a două celule din matrice este creat prin combinarea tuturor perechilor de itemi din cele două celule ce satisfac nişte reguli din gramatică. O altă operaţie ce se defineşte este: Closure(S) = ÎA|(A este în S) sau ((B este în Closure(S)) şi (A->B este o regulă din gramaticăŞ. Aceasta reprezintă faptul că închiderea unei celule S este formată din conţinutul lui S plus rezultatul adăugării oricărei categorii ce derivă dintr-un membru existent al închiderii lui S. De pildă, dacă N este în S atunci N aparţine lui Closure(S); apoi, dacă există o regulă NP->N, şi NP va fi adăugat în Closure(S); lucrurile continuă în acest mod cât timp se mai pot adăuga noi membri în Closure(S). În fine, există şi o funcţie Lookup, de forma: Lookup(k) = ÎA|A->cuvântul kŞ, adică ne dă lista de categorii gramaticale pe care le cuvântul al k-lea din fraza noastră (uneori un cuvânt poate avea mai multe categorii gramaticale, de pildă "duce" poate fi atât verb, cât şi substantiv). Cu definiţiile de mai înainte, algoritmul de bază CKY este: for k:=1 to n do begin chart[k-1,k]:=Closure(Lookup(k)); for i:=k-2 downto 0 do begin chart[i,k]:=∅; for j:=k-1 downto i+1 do chart[i,k]:=chart[i,k] ∪ Star(chart[i,j], chart[j,k]; chart[i,k]:=Closure(chart[i,k]); end end;

64

if S ∈ chart[0,n] then Accept else Reject

Algoritmul prezentat mai sus va fi implementat în programul care urmează, în care se vor realiza proceduri pentru cele trei funcţii definite, din cauza unor restricţii ale limbajului Pascal.

6.2. Textul explicat al programului

unit parser1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Grids, ExtCtrls; type TForm1 = class(TForm) StringGrid1: TStringGrid; Edit1: TEdit; Label1: TLabel; procedure Edit1KeyPress(Sender: TObject; var Key: Char);

65

procedure FormCreate(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} const max=20; maxreg=20; maxcuv=5; maxcuvinte=20;

Pentru a memora elementele care apar în stânga şi în dreapta unei reguli din gramatică, folosim tipul Cuvint. type Cuvint=String[30]; MultimeDeCuvinte=object nc: Integer; cuv: array[1..maxcuv] of Cuvint; procedure Adauga(c: Cuvint); end;

O regulă are două părţi, una în stânga şi una în dreapta, iar o gramatică este constituită din mai multe astfel de reguli. regula=record st,dr: Cuvint end; type Gramatica=object nr: Integer; reg: array[1..maxreg] of regula; procedure Citeste(nf: String); end;

Diagrama este o matrice în care fiecare celulă este o mulţime de cuvinte. G este gramatica cu regulile, iar D este, de fapt, dicţionarul de cuvinte folosit. var chart:array[0..max, 0..max] of MultimeDeCuvinte; G: Gramatica; D: Gramatica; Propoz: array[1..maxcuvinte] of Cuvint;

66

Desenarea diagramei se face conform conţinutului matricei chart, folosind controlul StringGrid1: procedure DeseneazaChart(i,j: Integer); var t: String; k,kk: Integer; begin if chart[i,j].nc>=1 then begin t:='{'; for k:=1 to chart[i,j].nc-1 do t:=t+chart[i,j].cuv[k]+','; t:=t+chart[i,j].cuv[chart[i,j].nc]+'}'; Form1.StringGrid1.Cells[j,i+1]:=t; Form1.StringGrid1.Refresh end end;

Funcţia următoare verifică dacă un cuvânt dat x se află sau nu într-o mulţime de cuvinte M. function EsteIn(x: Cuvint; M: MultimeDeCuvinte): Boolean; var este: Boolean; i: Integer; begin este:=False; for i:=1 to M.nc do if x=M.cuv[i] then este:=True; EsteIn:=este end;

Pentru obiectele de tip MultimeDeCuvinte am prevăzut o procedură de adăugare a unui nou cuvânt: procedure MultimeDeCuvinte.Adauga(c: Cuvint); begin nc:=nc+1; cuv[nc]:=c end;

Pe baza ultimelor două subprograme descrise, putem defini operaţiile Star, Closure şi Lookup, precum şi operaţia de citire a unei gramatici, a cărei reguli sunt scrise într-un fişier text dat. procedure Star(X,Y: MultimeDeCuvinte; var Z: MultimeDeCuvinte); var i,j,k: Integer; begin Z.nc:=0;

67

for i:=1 to X.nc do for j:=1 to Y.nc do for k:=1 to G.nr do if G.reg[k].dr=X.cuv[i]+' '+Y.cuv[j] then Z.Adauga(G.reg[k].st) end; procedure Closure(S: MultimeDeCuvinte; var C: MultimeDeCuvinte); var i: Integer; gata: Boolean; begin C:=S; repeat gata:=True; for i:=1 to G.nr do if EsteIn(G.reg[i].dr,C) then if not EsteIn(G.reg[i].st,C) then begin C.Adauga(G.reg[i].st); gata:=False end until gata end; procedure Gramatica.Citeste(nf: String); var f: TextFile; s: String; p: Byte; begin nr:=0; AssignFile(f,nf); Reset(f); while not eof(f) do begin nr:=nr+1; ReadLn(f,s); p:=Pos('->',s); reg[nr].st:=Copy(s,1,p-1); reg[nr].dr:=Copy(s,p+2,Length(s)-(p+1)) end; CloseFile(f) end; procedure Lookup(k: Integer; var L: MultimeDeCuvinte); var i: Integer; begin L.nc:=0; for i:=1 to D.nr do if D.reg[i].dr=Propoz[k] then begin L.nc:=L.nc+1; L.cuv[L.nc]:=D.reg[i].st

68

end end;

În fine, procedura următoare realizează analiza gramaticală a frazei date (ss), pe baza algoritmului descris teoretic în primul paragraf. procedure Parseaza(ss: String); var i,j,k,kk: Integer; n: Integer; p: Byte; L,C,S: MultimeDeCuvinte; t: String; begin ss:=ss+' '; n:=0; while ss'' do begin p:=Pos(' ',ss); n:=n+1; Propoz[n]:=Copy(ss,1,p-1); Form1.StringGrid1.Cells[n,0]:=Propoz[n]; Str(n-1,t); Form1.StringGrid1.Cells[0,n]:=t; Delete(ss,1,p) end; Form1.StringGrid1.RowCount:=1+n; Form1.StringGrid1.ColCount:=1+n; Form1.StringGrid1.Show; for k:=1 to n do begin Lookup(k,L); Closure(L,C); chart[k-1,k]:=C; for i:=k-2 downto 0 do begin chart[i,k].nc:=0; for j:=k-1 downto i+1 do begin Star(chart[i,j],chart[j,k],S); for kk:=1 to S.nc do if not EsteIn(S.cuv[kk], chart[i,k]) then chart[i,k].Adauga(S.cuv[kk]) end; Closure(chart[i,k],C); chart[i,k]:=C end end; for i:=0 to n do

69

for j:=0 to n do DeseneazaChart(i,j); if EsteIn('S',chart[0,n]) then Form1.Label1.Caption:= 'Fraza acceptata.' else Form1.Label1.Caption:= 'Fraza rejectata.' end; procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char); begin if Key=Chr(13) then Parseaza(Edit1.Text) end; procedure TForm1.FormCreate(Sender: TObject); begin Caption:='Basic CKY Parser'; StringGrid1.Hide; G.Citeste('gram.txt'); D.Citeste('lex.txt'); end; end.

Iată conţinuturile celor două fişiere (gramatica şi dicţionarul) folosite de noi în exemplele considerate: GRAM.TXT

LEX.TXT Det->orice Det->fiecare Det->o Det->un Pron->el N->barbat N->femeie V->iubeste V->uraste A->frumoasa A->desteapta C->si C->sau

S->NP VP NP->Pron NP->N NP->Det N NP->NP AP AP->A AP->AP CP CP->C A VP->V VP VP->V NP

Atenţie! Algoritmul funcţionează doar dacă în partea din dreapta a fiecărei reguli din gramatică sunt cel mult două simboluri, deci va trebui să aveţi grijă să rescrieţi gramaticile pe care le folosiţi astfel încât să îndeplinească această restricţie. 70

Aplicaţia 7

AutoWeb - Creator de pagini web 7.1. Prezentare generală Ne propunem, în continuare, să realizăm un program cu ajutorul căruia să realizăm pagini web personale, după un anumit format, redat în figura de mai jos:

După cum se poate observa în figură, o pagină web creată cu programul nostru, pe care l-am denumit AutoWeb, este compusă din trei cadre (frames) şi are următoarea structură: • un cadru în partea de sus, neredimensionabil, în care este scris numele persoanei şi deviza (motto-ul); • un cadru în partea din stânga, redimensionabil, ce cuprinde hiperlegături către diferite subiecte ce vor fi tratate în fişiere html separate şi vor fi afişate în dreapta; 71



un cadru în partea din dreapta, în care vor fi afişate fişierele html la care se face referinţă din cadrul cuprinsului din cadrul stâng. Structura fişierului principal index.html este următoarea:

Pagina mea web





Aceasta pagina are cadre, dar exploratorul dumneavoastra nu le accepta.



• • •

Aşadar, am definit două trei cadre: sus având conţinutul din fişierul sus.html; stg cu sursa luată din fişierul stanga.html; drp, care se încarcă, pentru început, cu fişierul sub1.html, referitor la subiectul 1. Conţinutul fişierului sus.html este simplu:

sus

Ioana Ionescu



Si miine e o zi!



72

Cu excepţia rândurilor subliniate, care reprezintă numele persoanei şi motto-ul său, toate celelalte linii ale fişierului vor fi generate prin program şi rămân neschimbate. Totuşi, trebuie ca să existe un fişier sus.html creat aprioric. Acesta va fi ca cel descris anterior, în care în locurile subliniate apar două texte sugestive: '(dati numele dumneavoastra aici)' şi '(dati motto-ul dumneavoastra aici)'. Numele persoanei şi motto-ul său vor fi introduse sau modificate în timpul execuţiei aplicaţiei AutoWeb, care va genera acest fişier. De asemenea, se presupune existenţa în directorul curent a unui fişier pentru realizarea fundalului, cu numele fond.jpg. Atenţie! Pagina web creată va putea fi afişată doar în Internet Explorer (din cauza modului de definire a cadrelor, ca şi din cauza faptului că se definesc cele două căsuţe de text defilant, cu ajutorul marcatorului marquee. Dacă doriţi să realizaţi o variantă care să funcţioneze şi în Netscape Communicator, propunem să renunţaţi de tot la cadrul sus, punând conţinutul acestuia în cadrul din stânga. Cadrul stg va conţine întotdeauna fişierul stg.html, care ar avea o structură de cuprins cu referiri la diferite fişiere html ce vor fi afişate în cadrul drp. De aceea va trebui să folosiţi atributul target în cadrul definirii hiperlegăturilor. Astfel, atunci când se alege un subiect din cuprinsul din stânga, datorită faptului că ţinta este cadrul drp, trimiterea se va face către un fişier ce va fi afişat în partea din dreapta. Dacă atributul target ar lipsi, noul fişier s-ar afişa tot în cadrul din stânga.

Meniul

Cuprins:

  • Despre mine

  • Pasiunile mele

  • Familia mea



73

Cu excepţia rândurilor subliniate, care vor fi introduse de utilizatorul aplicaţiei AutoWeb, toate celelalte rânduri ale fişierului stanga.html de mai sus vor fi generate prin program. Corespunzător celor trei subiecte la care se face referinţă în cadrul lui stanga.html, vor exista trei fişiere sub1.html, sub2.html şi sub3.html. Acestea vor conţine fraze simple, neformatate, dar e posibil să existe şi o imagine în interior. Dacă ea există, numele ei este precizat de către utilizatorul programului şi imaginea va fi afişată înaintea textului. De pildă, în exemplul nostru, am folosit fişierul sub1.html cu următorul conţinut, în care rândurile subliniate au fost introduse de la tastatură, iar restul au fost generate prin program:

Despre mine

Ma numesc Ioana Ionescu.

Sint eleva in clasa a X-a, la Colegiul National "Ferdinand I" din Bacau.

Sint nascuta in zodia Gemeni si am 17 ani.



Celelalte pagini (sub2.html şi sub3.html) vor fi create asemănător. După cum am spus, toate aceste fişiere sunt scrise de programul AutoWeb, aplicaţie pe care o vom prezenta mai jos. Excepţie face fişierul index.html care va fi realizat de dumneavoastră manual şi nu îl veţi modifica. Fişierele sub1.html, sub2.html şi sub3.html vor fi şi ele create anterior, dar vor fi modificate de către program. La început, ele vor arăta ca mai jos (păstraţi propoziţiile scrise între parenteze rotunde, pentru că ele trebuie să se regăsească în acea procedură din textul programului care se ocupă de salvarea întregii pagini web).

74

(scrieti aici denumirea subiectului)

(tratati aici pe larg acest subiect)



Pentru a le edita, puteţi folosi un editor de texte precum Notepad. În timpul execuţiei, aplicaţia AutoWeb arată ca în figura următoare. Aşadar, există o căsuţă de text în care se introduce numele persoanei, una în care aceasta îşi scrie motto-ul, precum şi trei tabulatori (controale de tip TTabControl), corespunzători celor trei subiecte. În cadrul fiecăruia se va înscrie numele subiectului corespunzător, conţinutul fişierului respectiv, adică textul, precum şi numele fişierului în care se află imaginea ce va apărea în acel fişier.

75

De pildă, în exemplul considerat în figuri, subiectul 1 va figura în meniul din cadrul stâng cu numele "Despre mine", va avea conţinutul (textul) "Mă numesc Ioana Ionescu....", iar fişierul cu imagine va fi "fata.jpg". Fireşte, se presupune că există pe disc aceste fişiere cu imagini, scanate. În figura următoare se observă cum arată forma aplicaţiei în timpul proiectării acesteia. Aşadar, pe lângă controalele descrise, există un buton pentru salvarea fişierelor HTML create, adică pentru înscrierea pe disc a paginii web realizate. Să remarcăm şi căsuţa de introducere a numelui fişierului cu imagini, în fiecare din cei trei tabulatori. Există şi un control de tip TOpenDialog, care este apelat de către un buton, pentru a putea alege un fişier cu o imagine, în mod interactiv, prin intermediul unei ferestre de dialog de tip "deschidere de fişier".

76

7.2. Textul explicat al programului În continuare, să vedem cum funcţionează toate controalele din forma aplicaţiei AutoWeb. unit web1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls; type TForm1 = class(TForm) Label1: TLabel; Edit1: TEdit; Label2: TLabel; Edit2: TEdit; TabControl1: TTabControl; Memo1: TMemo; Button1: TButton; Edit3: TEdit; Label3: TLabel; Button2: TButton; Edit4: TEdit; OpenDialog1: TOpenDialog; procedure TabControl1Change(Sender: TObject); procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM}

77

var nr_sub_tratat: Integer; subiect: array[1..3] of String;

Procedura următoare realizează trecerea de la un tabulator la altul. Astfel, schimbându-se subiectul curent, salvăm datele din subiectul curent şi apoi le încărcăm pe cele ale subiectului următor. Urmăriţi cu atenţie comentariile din cadrul textului procedurii! procedure TForm1.TabControl1Change(Sender: TObject); var s: String; i: Integer; f: TextFile; begin { salvam ce este scris in subiectul curent } Str(nr_sub_tratat,s); subiect[nr_sub_tratat]:=Edit4.Text; AssignFile(f,'sub'+s+'.html'); Rewrite(f); WriteLn(f,''); WriteLn(f,Edit4.Text); WriteLn(f,''); WriteLn(f,''); if (Edit3.Text'(dati numele fisierului)') and (Edit3.Text'') then begin WriteLn(f,'') end; if Memo1.Lines.Count=0 then WriteLn(f,'

(tratati aici subiectul'+ ' pe larg)

') else for i:=0 to Memo1.Lines.Count-1 do WriteLn(f,'

'+Memo1.Lines[i]+'

'); WriteLn(f,''); WriteLn(f,''); CloseFile(f); { incarcam ce era scris in subiectul nou tratat } Memo1.Clear; Edit3.Clear; Edit4.Clear; nr_sub_tratat:=TabControl1.TabIndex+1; Str(nr_sub_tratat,s); AssignFile(f,'sub'+s+'.html'); Reset(f); ReadLn(f,s); {}

78

ReadLn(f,s); Edit4.Text:=s; subiect[nr_sub_tratat]:=Edit4.Text; ReadLn(f,s); {} ReadLn(f,s);{f,'} ReadLn(f,s); if Copy(s,1,4)='") fis_ob = Left(s, p - 1) + ".WMF" nume_ob = Right(s, Len(s) - p - 1) Load obiectiv(i) obiectiv(i).BorderStyle = 1 obiectiv(i).Stretch = True obiectiv(i).Left = lat * (j - 1) obiectiv(i).Top = has * (k - 1) obiectiv(i).Width = lat obiectiv(i).Height = has obiectiv(i).Tag = nume_ob obiectiv(i).Picture = LoadPicture(fis_ob) obiectiv(i).DragMode = 1 obiectiv(i).Visible = True Next k Next j Close #1 End Sub

Dacă utilizatorul programului vrea să înţeleagă ce reprezintă fiecare din cele 45 de pictograme din legenda din stânga lui Form1, atunci el va acţiona butonul din dreapta al mouse-ului, în dreptul obiectivului corespunzător. Acest lucru va avea ca efect apelarea subrutinei: Sub obiectiv_Click (Index As Integer) MsgBox obiectiv(Index).Tag, 64, "Ce reprezinta asta?" End Sub

Figura de mai jos prezintă modul de afişare a semnificaţiei pictogramei cu pachetul:

204

X_min

pictograma cu pachetul, care reprezintå un magazin

Trasarea străzilor se face cu subrutina următoare, care ţine cont de faptul că variabila desen s-a iniţializat cu False, în Form_Load: Sub Form_MouseDown (Button As Integer, Shift As Integer, x As Single, y As Single) If desen Then Select Case Button Case 1: DrawWidth = 3 ForeColor = RGB(150, 200, 200) Case 2: DrawWidth = 8 ForeColor = RGB(200, 200, 50) End Select

205

If x < x_min Then x = x_min Line (first_x, first_y)-(x, y) ns = ns + 1 ReDim Preserve Strada(ns) Strada(ns).X1 = first_x: Strada(ns).X2 = x Strada(ns).Y1 = first_y: Strada(ns).Y2 = y Strada(ns).tip = Button desen = False MousePointer = 0 Else desen = True first_x = x: first_y = y MousePointer = 2 End If End Sub

Çtergerea tututor străzilor se realizează apelând din meniu, comanda corespunzătoare: Sub mnuClear_Click () ns = 0: ReDim Strada(0) Form_Paint End Sub

Iată şi cum se realizează amplasarea obiectelor pe hartă, prin “drag and drop”: Sub Form_DragDrop (source As Control, x As Single, y As Single) If x - source.Width \ 2 < x_min Then MsgBox "Nu se poate aseza aici !", 16, "Atentie!" Exit Sub End If n = n + 1 Load ob(n) ob(n).Width = source.Width ob(n).Height = source.Height ob(n).Stretch = True ob(n).Tag = source.Tag ' se va adauga si denumirea ! ob(n).Left = x - (ob(n).Width / 2) ob(n).Top = y - (ob(n).Height / 2) ob(n).BorderStyle = 0 ob(n).Picture = source.Picture ob(n).Visible = True ob(n).MousePointer = 10 Form1.Enabled = False 'Form1.Hide ' pentru a evita problemele

206

Form2.Caption = "Denumire " + ob(n).Tag Form2.Text1.SelStart = 0 Form2.Text1.SelLength = Len(Form2.Text1.Text) Form2.Show ' preia denumirea lui ob(n) Do DoEvents ' asteapta pana se termina lucrul in Form2 Loop Until Form1.Enabled = True ob(n).Tag = ob(n).Tag + " " + Form2.Text1.Text End Sub

Observaţi apelui celei de a doua forme (Form2) care preia denumirea obiectului amplasat. Ciclul: Do DoEvents ' asteapta pana se termina lucrul in Form2 Loop Until Form1.Enabled = True

are rolul de a bloca temporar execuţia programului, la nivelul lui Form2. Çtergerea unui obiect, sau aflarea de informaţii despre el se realizează cu subrutina următoare: Sub ob_MouseDown (Index As Integer, Button As Integer, Shift As Integer, x As Single, y As Single) Select Case Button Case 2 ' se sterge afisajul si obiectul CurrentX = ob(n).Left CurrentY = ob(n).Top - 200 ForeColor = QBColor(15) Print ob(n).Tag x = ob(n).Left: y = ob(n).Top ob(Index).Visible = False ob(Index) = ob(n) ob(Index).Left = x: ob(Index).Top = y ob(Index).Visible = True Unload ob(n) n = n - 1 Case 1 ' afisez informatii MsgBox ob(Index).Tag, 64, "Informatii" End Select End Sub

207

Iată şi subrutina care redesenează, la nevoie, conţinutul formei Form1. Ea redesenează (la noile proporţii) legenda cu obiective din stânga formei, apoi străzile şi obiectele de pe hartă. Sub Form_Paint () Cls ' reafisez meniul cu obiective din stanga has = ScaleHeight \ 15 lat = has x_min_vechi = x_min 'pentru calcule ulterioare x_min = 3 * has + 10 For i = 1 To 45 obiectiv(i).Visible = False Next i For j = 1 To 3 For k = 1 To 15 i = 15 * (j - 1) + k obiectiv(i).Left = lat * (j - 1) obiectiv(i).Top = has * (k - 1) obiectiv(i).Width = lat obiectiv(i).Height = has obiectiv(i).Visible = True Next k Next j ' redesenez strazile rap_x=(ScaleWidth-x_min)/x_0: rap_y=ScaleHeight/y_0 x_0 = ScaleWidth - x_min: y_0 = ScaleHeight For i = 1 To ns Select Case Strada(i).tip Case 1: DrawWidth = 3 ForeColor = RGB(150, 200, 200) Case 2: DrawWidth = 8 ForeColor = RGB(200, 200, 50) End Select Strada(i).X1=x_min+rap_x*(Strada(i).X1-x_min_vechi) Strada(i).Y1 = rap_y * Strada(i).Y1 Strada(i).X2=x_min+rap_x*(Strada(i).X2-x_min_vechi) Strada(i).Y2 = rap_y * Strada(i).Y2 Line (Strada(i).X1, Strada(i).Y1)(Strada(i).X2, Strada(i).Y2) Next i ' sterg ob-urile, pentru a le reafisa la noile dimensiuni For i = 1 To n ob(i).Visible = False Next i For i = 1 To n ' calculez noile coordonate

208

ob(i).Left=(ob(i).Left-x_min_vechi)*rap_x+x_min ob(i).Top = ob(i).Top * rap_y ' calculez noile dimensiuni in functie de un obiectiv ob(i).Width = obiectiv(1).Width ob(i).Height = obiectiv(1).Height 'afisez din nou ob(i).Visible = True Next i End Sub

Form_Resize apelează, la rându-i, subrutina Form_Paint descrisă anterior: Sub Form_Resize () Form_Paint End Sub

Restul subrutinelor prezentate mai jos:

(din

fişierul

“HARTA.FRM”)

Sub Form_Unload (Cancel As Integer) Unload Form2 ' ca sa se opreasca ! End Sub Sub mnuDespre_Click () s1 = "Acesta este un editor de harti!" NL = Chr$(13) + Chr$(10) s2 = "(C) 1998 B.P., tel. 092-738341" MsgBox s1 + NL + s2, 64, "Despre program" End Sub Sub mnuExit_Click () End End Sub Sub mnuHartaNoua_Click () For i = n To 1 Step -1 Unload ob(i) Next i n = 0 mnuClear_Click End Sub

209

sunt

• Fişierul “HARTA_D.FRM” Acest fişier conţine doar o singură subrutină, uşor de înţeles, care acţionează la finele introducererii unui text în caseta Text1:

Sub Text1_KeyPress (KeyAscii As Integer) If KeyAscii = 13 Then Form1.Enabled = True Form2.Hide End If End Sub

210

Bibliografie 1. Bogdan Pătruţ - Aplicaţii în Delphi, Editura Teora, Bucureşti, 1998. 2. Bogdan Pătruţ - Aplicaţii în Visual Basic, Editura Teora, Bucureşti, 1998

* *

*

Observaţie. Sursele aplicaţiilor din această carte au fost realizate, compilate şi testate în mediile de programare Delphi 3 şi Visual Basic 3. Adaptarea acestor aplicaţii la versiuni ulterioare ale celor două medii de programare rămâne în seama cititorului. Orice adaptare la Delphi 6 sau Visual Basic 6 pe care o realizaţi o puteţi trimite prin e-mail la [email protected] sau [email protected].

211