Rozdział 17
Przykłady zastosowań
.
.
.
Przykład W tym przykładzie pokażemy, w jaki sposób można zaprojektować znaną grę o nazwie piętnastka . Polega ona na uporządkowaniu w zadany sposób liczb w tablicy, która zawiera piętnaście liczb 1,...,15 oraz jedno puste miejsce. Porządkowanie wykonuje się, przestawiając liczby na puste miejsce z tym, że można to uczynić zmieniając pozycję wyłącznie liczby sąsiadującej z pustym miejscem w wierszu lub kolumnie.
Gra w piętnastkę jest często realizowana przy pomocy pudełka
z kwadracikami, na których są umieszczone liczby. Kwadraciki można
przesuwać na puste miejsce aż do uzyskania uporządkowania liczb
według żądanej kolejności.
Warto się teraz zastanowić, w jaki sposób zrealizować omówioną
grę na komputerze, wykorzystując system C++ Builder. Koncepcja,
która się nasuwa, zastosowanie komponentu - siatki, nie jest dobra,
ponieważ pojawiają się problemy z przesuwaniem liczb. W aplikacji
Pietnast.mak (katalog ROZ_17) gra została zrealizowana przy pomocy
16 przycisków. Przyciski te nie są jednak tworzone w oparciu o klasę
TButton, lecz przy pomocy klasy SpecPrzycisk będącej podklasą klasy
TButton (klasa ta była już wykorzystywana w rozdziale 11). Jak pa-
miętamy, w klasie SpecPrzycisk definiujemy dwie własności x oraz y,
w których można zapamiętać dowolne liczby całkowite. W naszym przy-
padku podczas tworzenia przycisku (w sposób programowy) we włas-
nościach x oraz y zapamiętamy numer wiersza i kolumny, w których
jest umieszczony przycisk.
Przerwijmy teraz na chwilę te rozważania, aby się pobawić grą. Na
rys. 17.3 jest podane ustawienie początkowe liczb (za każdym razem
inne - generowane losowo), natomiast ustawienie końcowe może być
zadane w sposób dowolny na przykład tak, aby liczby były uporząd-
kowane w kolejności wiersz po wierszu.
Liczby przeciąga się, wykorzystując mechanizm drag and drop
(ciągnij i upuść), czyli należy nacisnąć klawisz myszki na liczbie sąsia-
dującej z pustą kratką w wierszu lub kolumnie i przesunąć kursor myszki na puste pole, po czym zwolnić klawisz. Warto zaznaczyć, że próby przeciągania odległych liczb nie przynoszą rezultatów. I właśnie do tego celu są potrzebne własności x oraz y. Przy ich pomocy można bowiem sprawdzić, czy dana kratka sąsiaduje z pustą. Formularz można obejrzeć uruchamiając aplikację Rys. 17.3. Ustawienie początkowe liczb
Wróćmy teraz do omawiania sposobu zaprojektowania gry. Przede wszystkim przypomnimy metodę tworzenia klasy SpecPrzycisk (defi-
nicja tej klasy jest umieszczona w pliku klasaSpec.h).
class SpecPrzycisk: public TButton
{
public:
int x,y;
__fastcall SpecPrzycisk(TComponent*AOwner):
TButton(AOwner) {};
// void __fastcall ScaleBy(int M, int D) {};
};
Zauważmy, że deklaracje zmiennych x oraz y są umieszczone w
części publicznej definicji klasy. Ponadto klasa SpecPrzycisk ma dostęp
do wszystkich własności, metod oraz zdarzeń zdefiniowanych w klasie
TButton.
Na wstępie podamy deklaracje zmiennych wykorzystywanych w
metodach klasy TForm1. Jak zwykle deklaracje te umieszczamy w
części private definicji klasy TForm1 (plik p_pietnastka.h).
private: // User declarations
time_t start;
SpecPrzycisk *p[MAX_PRZYC];
int licznik;
String liczba[MAX_PRZYC];
// p - tablica wskazań na obiekty klasy SpecPrzycisk
// start - czas rozpoczęcia gry
// licznik - licznik posunięć
// liczba - tablica zawierająca rozmieszczenie liczb
Zwróćmy uwagę na deklarację tablicy p, która zawiera MAX_-
PRZYC wskazań na przyciski klasy SpecPrzycisk.
W pliku p_pietnastka.h jest jeszcze podana dyrektywa preprocesora
#include "p_klasaSpec.h"
umożliwiająca wykorzystanie klasy SpecPrzycisk oraz dyrektywa definiującą stałą MAX_PRZYC: #define MAX_PRZYC 16
Na marginesie przypomnijmy, że nie należy zapomnieć o dołączeniu
do aplikacji pliku p_klasaSpec.cpp (opcja Project/Add to Project).
Pokażemy teraz, w jaki sposób programowo utworzyć przycisk klasy
SpecPrzycisk. Otóż do utworzenia przycisku klasy SpecPrzycisk wy-
starczy napisać p[k] = new SpecPrzycisk (this);
gdzie tablica p musi być wcześniej odpowiednio zadeklarowana.
Mając już utworzony przycisk, należy ustawić niezbędne własności
(oczywiście w programie, ponieważ nie mamy dostępu do okienka
własności). Dla k-tego elementu tablicy p należy ustawić własność
Parent następująco:
p[k]->Parent = this;
Następnie należy podać miejsce umieszczenia przycisku, wykorzys-
tując własności Left oraz Top. Można to zrobić w elegancki sposób przy pomocy zmiennych i oraz j określających numer wiersza i kolumny, w których jest umieszczony dany przycisk. p[k]->Left = 100 + (i-1)*30; p[k]->Top = 100 + (j-1)*30;
Szerokość przycisku określamy przy pomocy własności Width (wysokość pozostawiamy bez zmian). p[k]->Width := 30;
Aby przycisk był widoczny na formularzu, jest konieczne usta-
wienie własności Visible. p[k]->Visible := True;
Ponieważ zdarzenia są traktowane analogicznie jak własności, metody wykonywane po zajściu zdarzeń OnDragDrop oraz OnDragOver okreś-
lamy w ten sposób, że jako wartości tych własności podajemy nazwy metod wykonywanych po zajściu danego zdarzenia. W naszym przypad-
ku metoda wykonywana po zajściu zdarzenia OnDragDrop to Ciagnij, a metoda Zaakceptuj jest realizowana po zajściu zdarzenia OnDragOver. Metody Ciagnij oraz Zaakceptuj są napisane w sposób ogólny i dlatego mogą być zastosowane dla każdego przycisku. p[k]->OnDragDrop := Ciagnij; p[k]->OnDragOver := Zaakceptuj;
Aby mechanizm drag and drop był wykonywany w sposób auto-
matyczny, należy ustawić własność DragMode. p[k]->DragMode := dMAutomatic;
I na koniec do zapamiętania numeru wiersza i kolumny umieszcze-
nia danego przycisku należy wykorzystać własności zaprojektowane w klasie SpecPrzycisk. p[k]->x := i; p[k]->y := j;
Konieczne jest jeszcze nadanie wartości własności Caption, ale dokonuje się tego w metodzie NoweClick, której wywołanie umieściliś-
my na końcu metody FormCreate.
Powyższe operacje wykonujemy w pętli dla każdego wiersza i
każdej kolumny, co ilustruje metoda FormCreate wykonywana przy
tworzeniu formularza.
void __fastcall TForm1::FormCreate(TObject *Sender)
{
int i,j,k;
// wyzerowanie licznika posunięć licznik = 0;
k = -1;
// utworzenie 16 przycisków
for (i=1; i<=4; i++)
for (j=1; j<=4; j++) {
k++;
// utworzenie przycisku
p[k] = new SpecPrzycisk (this);
p[k]->Parent = this;
// ustalenie miejsca umieszczenia przycisku
p[k]->Left = 100 + (i-1)*30;
p[k]->Top = 100 + (j-1)*30;
// ustalenie szerokości przycisku
p[k]->Width = 30;
// ustalenie wysokości przycisku
p[k]->Height = 30;
// przycisk będzie widoczny
p[k]->Visible = true;
// określenie zdarzeń wykonywanych po zajściu
// zdarzeń OnDragDrop oraz OnDragOver
p[k]->OnDragDrop = Ciagnij;
p[k]->OnDragOver = Zaakceptuj;
// ustawienie, aby mechanizm drag and drop był
// wykonywany w sposób automatyczny
p[k]->DragMode = dmAutomatic;
// zapamiętanie numeru wiersza i kolumny,
// w których jest umieszczony przycisk
p[k]->x = i;
p[k]->y = j;
};
// ustawienie nowej konfiguracji liczb
NoweClick(Sender);
}
Pokażemy teraz, w jaki sposób utworzyć nową konfigurację liczb. Tworzy ją metoda NoweClick wykonywana po naciśnięciu klawisza myszki na przycisku Nowe (metoda ta jest również wywoływana w treści metody FormCreate). Algorytm zastosowany w metodzie Nowe-
Click jest następujący. Najpierw do tablicy liczba wpisujemy w natu-
ralnej kolejności piętnaście liczb 1,...,15. Następnie wykorzystując generator liczb losowych, wypełniamy tablicę pomocniczą kol. Szansa, że zostaną wylosowane dwie takie same liczby jest znikoma, ale nawet gdyby tak się stało, to i tak nie przeszkadzałoby to w ustaleniu kolej-
ności liczb w tablicy liczba. I ostatni krok to uporządkowanie w kolejności niemalejącej liczb umieszczonych w tablicy kol z ustawieniem w tym samym porządku liczb w tablicy liczba. Podany niżej algorytm nie jest najbardziej efek-
tywnym algorytmem losowego porządkowania liczb w tablicy, ale za to
jednym z najprostszych.
A oto treść metody NoweClick.
void __fastcall TForm1::NoweClick(TObject *Sender)
{
int k,pom,nakt;
String pomliczba;
int kol[15];
bool zamiana;
// k - zmienna pomocnicza
// pom - zmienna do zmiany elementów w tablicy kol
// nakt - długość badanej części tablicy
// pomliczba - zmienna do zamiany elementów w tablicy
// liczba
// kol - tablica decydująca o kolejności
start = time(NULL); licznik = 0; Ruch->Caption = IntToStr(licznik);
// wpisanie do tablicy z liczbami kolejnych liczb
for (k=0; k<15; k++)
liczba[k] = IntToStr(k+1);
// wpisanie do tablicy kol 15 liczb przypadkowych z
// dużego zakresu
randomize;
for (k=0; k < 15; k++)
kol[k] = random(10000);
// uporządkowanie liczb zapisanych w tablicy liczba // zgodnie z kolejnością liczb stojących w tablicy kol zamiana = true; nakt = 15;
while (zamiana) {
zamiana = false;
for (k=0; k<nakt-1; k++)
if (kol[k] > kol[k+1]) {
// zamiana liczb w tablicy kol
pom = kol[k];
kol[k] = kol[k+1];
kol[k+1] = pom;
// zamiana liczb w tablicy liczba
pomliczba = liczba[k];
liczba[k] = liczba[k+1];
liczba[k+1] = pomliczba;
zamiana = true;
};
nakt--;
}
// wpisanie liczb z tablicy liczba do własności Caption
/ utworzonych przycisków
for (k=0; k<15; k++)
p[k]->Caption = liczba[k];
p[15]->Caption = "";
}
Metoda wykonywana po zajściu zdarzenia OnDragDrop to Ciagnij. Przypomnijmy, że zdarzenie to powstaje przy przeciąganiu własności obiektów. W naszym przypadku przeciągana jest własność Caption, co stwarza wrażenie przesuwania liczb na planszy do gry. Bez względu na to, który przycisk jest źródłowy (parametr Source), a który jest przycis-
kiem przeznaczenia (parametr Sender), zawsze jest realizowana metoda Ciagnij. Należy zatem sprawdzić warunki, których spełnienie jest konieczne, by można było zmienić własność Caption przycisku źródłowego i przeznaczenia. Po pierwsze należy się upewnić, że przycis-
ki te sąsiadują ze sobą w wierszu lub kolumnie. Po drugie muszą to być dwa rożne przyciski. Po trzecie przycisk źródłowy musi mieć przy-
pisaną jakąś liczbę, a przycisk przeznaczenia nie powinien mieć przy-
porządkowanej żadnej liczby. Wszystkie te warunki są sprawdzane w
dość skomplikowanym wyrażeniu logicznym, co ilustruje poniższa
metoda.
void __fastcall TForm1::Ciagnij(TObject *Sender,
TObject *Source, int X, int Y)
{
int i1,j1,i2,j2;
// i1,j1 - parametry przycisku źródłowego
// i2,j2 - parametry przycisku przeznaczenia
// ustalenie numeru wiersza i kolumny przycisku // źródłowego zawierającego liczbę i1 = ((SpecPrzycisk *) Source)->x; j1 = ((SpecPrzycisk *) Source)->y;
// ustalenie numeru wiersza i kolumny przycisku // przeznaczenia (pustego i2 = ((SpecPrzycisk *) Sender)->x; j2 = ((SpecPrzycisk *) Sender)->y;
// definicja warunków, przy których może dojść do
// przeciągnięcia liczby
if ( ((i1==i2) && (j1==j2-1)) ||
((i1==i2) && (j1==j2+1)) ||
((j1==j2) && (i1==i2-1)) ||
((j1==j2) && (i1==i2+1)) )
if ( (((SpecPrzycisk *) Sender) !=
((SpecPrzycisk *) Source ))
&& (((SpecPrzycisk *) Source)->Caption !="") &&
(((SpecPrzycisk *) Sender) ->Caption == "")) {
licznik++;
Ruch->Caption = IntToStr(licznik);
// przepisanie liczby
((SpecPrzycisk *) Sender)->Caption =
((SpecPrzycisk *) Source)->Caption;
((SpecPrzycisk *) Source)->Caption = "";
}
}
Metoda Zaakceptuj jest wykonywana po zajściu zdarzenia OnDrag-
Over polegającego na upuszczeniu ciągniętego obiektu i na ogół służy
do zaakceptowania tego obiektu. Ponieważ wszystkie warunki zostały
sprawdzone w metodzie Ciagnij, to metoda Zaakceptuj ma następującą
treść.
void __fastcall TForm1::Zaakceptuj(TObject *Sender,
TObject *Source, int X, int Y, TDragState State,
bool &Accept)
{
Accept = true;
}
Po wykonaniu kilku ruchów poprzednie ustawienie konfiguracji
liczb łatwo uzyskać przy pomocy poniższej metody realizowanej po
naciśnięciu przycisku Poprzednie.
void __fastcall TForm1::PoprzednieClick(TObject *Sender)
{
licznik = 0;
Ruch->Caption = IntToStr(licznik);
// wpisanie poprzedniego ustawienia liczb
for (int k=0; k<16; k++)
p[k]->Caption = liczba[k];
}
Metoda KoniecClick tym razem jest nieco bardziej skomplikowana
niż zazwyczaj, ponieważ trzeba usunąć utworzone przyciski.
void __fastcall TForm1::KoniecClick(TObject *Sender)
{
// usunięcie przycisków
for (int k=0; k<16; k++)
delete p[k];
Close(); }
Ostatnia metoda to Timer1Timer wykonywana po zajściu zdarzenia OnTimer dla komponentu Timer umożliwiającego wykonywanie jakiejś akcji co pewien ściśle określony czas. Metoda Timer1Timer jest wyko-
nywana po upłynięciu odcinka czasu określonego we własności Inter-
val. Ustawienie wartości tej własności na 1000 powoduje, że zdarzenie
OnTimer będzie zachodzić co jedną sekundę.
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
// wyświetlenie aktualnego czasu
Czas->Caption = IntToStr(time(NULL)-start);
}
Na zakończenie omawiania projektu Pietnast.mak zwróćmy jeszcze uwagę, że w projekcie jest wykorzystywana linia statusu, w której jest wyświetlany czas, jaki upłynął od rozpoczęcia gry oraz liczba wykona-
nych ruchów. Jak wiadomo, do zaprojektowania linii statusu wystarczy zastosować komponent - Panel, na którym są umieszczane inne kom- ponenty.
.
.
.