artykuły

Delphi - Efekt fali

12:37
Tue, 9 November 2004
Popularny artykuł objaśniający problemy związane z projektowaniem animacji w Delphi z poziomu kodu programu. Opisuje różne techniki stosowane przy wyświetlaniu grafiki: DelphiX, płótno systemowe (GDI), bitmapy pamięciowe. Zawiera wyjaśnienie i opis rozwiązania problemu dotyczącego migotania animacji rysowanych za pomocą GDI. W trakcie artykułu czytelnik pisze program wyświetlający prostą animacje przypominającą efekt fal powstałych np. po wpadaniu kropel deszczu do wody.

Wstęp

DoDelphizostał napisany specjalny moduł, który pozwala wykorzystywać możliwościDirectX. Moduł ten nazywa sięDelphiXi możecie go znaleźć na płycie Eksperta, oraz oczywiście w sieci. W tym artykule, chciałbym pokrótce opisać modułDelphiXdo tego stopnia, aby każdy mógł się z nim oswoić. Nie dołączę go jednak do tego artykułu ponieważ zajmuje on nieco ponad 570 kB. Jeśli nie posiadaszDelphiX, to jeszcze świat się nie wali. Możesz przecież spróbować rysować naCanvasieformularza (my będziemy rysować i tu i tu, ale chciałbym zwrócić jednak szczególną uwagę naDXDraw). Przy takiej próbie musisz się nastawić, że efekt końcowy będzie nieco słabszy (przede wszystkim, na słabszych komputerach może dojść do tzw. (i tu sięgam do fachowego słownika) "skakania" ;). Wszystko przez to, że przy rysowaniu naCanvasiewykorzystujemy systemowe funkcje graficzne, natomiastDirectXzwraca się bezpośrednio do sprzętu. WykorzystującDelphiX, a co za tym idzieDirectXna takich komputerach przyspieszamy tym samym rysowanie kilkaset razy. ChoćDirectXteż jest praktycznie systemową funkcją, ponieważ każdy Windows ma ją zaimplementowaną, np. wWin98 SEdla przypomnienia był toDirectX 6. Po coś w końcu jest w systemie ;) Czas przejść do senda sprawy.

Co chcemy uzyskać?

Jak można wywnioskować z tytułu, będziemy starali się o uzyskanie efektu fali pojawiającej się na przykład gdy kropla wpada do wody. Metoda, którą przedstawię, nie będzie zaawansowana graficznie. Zależało mi na prostym rozwiązaniu. Takie właśnie ono będzie. W artykule przedstawie trzy różne sposoby rysowania na ekranie tej samej grafiki. Dowiesz się który sposób jest najbardziej wydajny, a który najprostszy.

Piszemy program dla DirectX (wykorzystujemy nakładkę DelphiX)

Przede wszystkim, jak to już zostało wspomniane, musimy zaopatrzyć się w modułDelphiX. Gdy go zainstalujemy, na palecie komponentów pojawi się nowa zakładka "DelphiX". To właśnie na niej znajdują się poszczególne komponenty tego pakietu. Zaczynamy:

  1. Z palety "DelphiX" wybieramy komponent "DXDraw" i kładziemy go na formie (w tym celu klikamy na ikonę komponentu, a następnie klikamy na formularz).
  2. Z palety "DelphiX" wybieramy komponent "DXTimer" i umieszczamy go na formie.
  3. Zaznaczamy komponent "DXDraw1" i w "Inspektorze Obiektów" odnajdujemy właściwość "Align", którą ustawiamy na "alClient".
    Następnie odnajdujemy w "Inspektorze Obiektów" grupę "Options", którą rozwijamy (kliknięciem na znak "plus" po prawej stronie) i zmieniamy właściwość "doFullScreen" na "True" (obraz będzie wypełniał cały ekran).
    Odnajdujemy teraz właściwość "Name" i zmieniamy ją z "DXDraw1" na "DXD" (będzie mniej pisania ;).
    Jeszcze jedna właściwość pozostała do zmienienia - "Color" - ustawiamy ją na "clBlack" (co spowoduje zmianę tła komponentu na czarne.
  4. Wracamy do formy i zaznaczamy komponent "DXTimer1" i w "Inspektorze Obiektów" zmieniamy jego właściwość "Interval" na 10 (komponent będzie co 10 milisekund wywoływał proceduręOnTimer).
  5. Przechodzimy do zakładkiEvents(przy zaznaczonym komponencie "DXTimer1") i odszukujemy zdarzenie "OnActivate" (jest ono wywoływane, gdy tylko komponent "DXTimer1" zostanie aktywowany, czyli w naszym wypadku, zaraz po starcie programu.
  6. Przyduszając klawisz F12 przechodzimy do edytora kodu.
  7. Do listy "uses" (używane) dodajemy słówko "DXClass".
  8. Następnie znajdujemy deklaracje klasy "TForm1". Klasa ta nie będzie już dziedziczyła z "TForm", jak to było do tej pory, ale z "TDXForm". Wpisujemy więc, po prawej stronie ( tam w nawiasie ) zamiast "TForm", "TDXForm".
    Choć bez tej zmiany byłoby w naszym przypadku wszystko OK (sprawdzałem), to jak najbardziej powinniśmy wyrabiać sobie dobre nawyki, ponieważ autorzy przestrzegają, że w niektórych wypadkach, gdy tego nie uczynimy, możemy natrafić na różne dziwne błędy.
    Nie wiesz co to znaczy dziedziczenie klas? Jest to określenie, które często używane jest przy tworzeniu nowych klas. Otóż, nową klasę (czyli np. zbiór naszych zmiennych, procedur i funkcji) możemy utworzyć od nowa, bądź zbudować na bazie już istniejącej (tzw. dziedziczyć). W tym drugim wypadku będziemy mieli do czynienia ze skopiowaniem procedur i funkcji klasy matki (klasy z której dziedziczymy) i dodaniu ich do naszych procedur i funkcji.
    Dla mniej zaawansowanych mam analogię: mamy dwa kosze jabłek: "Kosz1" (pełny - jabłka sąsiada ;) i "Kosz2" (kilka własnych jabłek na dnie). W "Koszu2" mamy kilka jabłek, które przed chwilą zerwaliśmy, ale chcemy też mieć w nim wszystkie jabłka sąsiada, co robimy gdy nie wolno nam ukraść jabłek z kosza sąsiada? Kopiujemy jabłka! ;) (mniejsza o praktyczne wykonanie tej czynności ;))) I już mamy pełny kosz i dodatkowo są w nim jeszcze nasze jabłka! ;)))
    Poniższy rysunek objaśni gdzie i co wpisać:
    Co należy dodać do listy uses i co należy zmienić w nazwie klasy TForm
  9. Przed słówkiem "var" (sekcja zmiennych) wpisujemy słówko "const" (sekcja stałych), a pod nim dopisujemy taką linię: ilosc = 100; // Ilość baniek do zasymulowania
  10. Przechodzimy do sekcji "var" i dodajemy do niej takie zmienne (tablice): pozycja : array[1..ilosc] of TPoint; wielkosc : array[1..ilosc] of integer;
  11. Przechodzimy do formularza (F12), zaznaczamy komponent "DXTimer1", przechodzimy do "Inspektora Obiektów", przechodzimy do zakładki "Events" i podwójnie klikamy w pole po prawej stronie, obok zdarzenia "OnActivate". Zostaniemy przeniesieni do edytora kodu, a w nim zostanie wygenerowana nowa procedura, którą uzupełniamy następująco: procedure TForm1.DXTimer1Activate(Sender: TObject); var i : integer; begin Randomize(); // Losowanie początkowych pozycji i wielkości baniek (początkowe wypełnienie tablic) for i:=Low(pozycja) To High(pozycja) Do begin // Losowanie początkowej pozycji bańki pozycja[i] := Point(Random(DXD.SurfaceWidth),Random(DXD.SurfaceHeight)); // Losowanie początkowej wielkości bańki wielkosc[i] := Random(50); end; // Kolor wypełnienia czarny, kolor pióra biały (początkowo) DXD.Surface.Canvas.Brush.Color := clBlack; // DXD.Surface.Canvas.Brush.Style := bsClear; << a gdyby ta linia nie była komentarzem??? ;) DXD.Surface.Canvas.Pen.Color := clWhite; end;
  12. Przechodzimy do formularza (F12), zaznaczamy komponent "DXTimer1", przechodzimy do "Inspektora Obiektów", przechodzimy do zakładki "Events" i podwójnie klikamy w pole po prawej stronie, obok zdarzenia "OnTimer". Zostaniemy przeniesieni do edytora kodu, a w nim zostanie wygenerowana nowa procedura, którą uzupełniamy następująco: procedure TForm1.DXTimer1Timer(Sender: TObject; LagCount: Integer); var i : integer; promien : integer; begin // Zamalowywanie ekranu (żeby rysunki się nie nakładały) DXD.Surface.Canvas.Pen.Color := clBlack; DXD.Surface.Canvas.Brush.Color := clBlack; DXD.Surface.Canvas.Rectangle(0,0,DXD.SurfaceWidth,DXD.SurfaceHeight); // Pętla chodzi po wszystkich bańkach for i:=Low(pozycja) To High(pozycja) Do begin // Zmiana koloru obramowania, wraz ze wzrostem bańki DXD.Surface.Canvas.Pen.Color := RGB(255-wielkosc[i]*5,255-wielkosc[i]*5,255-wielkosc[i]*5); // Dla przyspieszenia obliczeń, przypiszemy do zmiennej wynik dzielenia promien := wielkosc[i] div 2; DXD.Surface.Canvas.Ellipse(pozycja[i].X-promien,pozycja[i].Y-promien,pozycja[i].X+promien,pozycja[i].Y+promien); // Powiększ o 1 wielkość aktualnie przerabianej bańki Inc(wielkosc[i]); // Jeśli wielkość jest ponad 50 to niech bańka zniknie i na jej miejscu powstanie nowa if wielkosc[i] > 50 Then begin // Jaka ma być pozycja nowej bańki pozycja[i] := Point(Random(DXD.SurfaceWidth),Random(DXD.SurfaceHeight)); // Jaki ma być rozmiar nowej bańki (można wstawić np. 1) wielkosc[i] := Random(10); end; end; // Dwie poniższe instrukcje są wymagane aby wyświetlić narysowany obraz !!! DXD.Surface.Canvas.Release; DXD.Flip; end;

Piszemy program korzystający jedynie z Canvas'u formularza

Napisanie identycznego programu, który wyświetlałby efekt swojego działania na typowymCanvasie(Canvasod ang. płótno) nie jest rzeczą łatwiejszą niż pisanie tego samego programu podDirectXwDelphiX. Zanim zaczniemy mam dla Was typową już dla mnie przestrogę programistyczną:

"Jeśli rysujesz jakikolwiek obraz np. program ma zademonstrować wykres w postaci graficznej, rysuj na bitmapie pamięciowej! Takie rozwiązanie będzie szybsze, nie będą się pojawiać błędy typowe dla rysowania bezpośredniego (jak migające tło) itp.
Gdy zdecydujesz się na takie rozwiązanie, pierwsze co musisz zrobić to zadeklarowanie i utworzenie nowej bitmapy typuTBitmap. Chcąc narysować np. sto kółek, rysujesz je na bitmapie pamięciowej, a następnie wyświetlasz bitmapę na formie przy pomocy funkcjiDraw(), np: form1.Canvas.Draw(0, 0, bitmapa1);"

Póki co, zajmiemy się najpierw rysowaniem bezpośrednim, potem pokażę na czym polegają różnicę przy wykorzystaniu bitmapy pamięciowej. Po zapoznaniu się z tymi dwoma przykładami, będziecie wiedzieć dlaczego większość programistów stosuje bitmapy pamięciowe.
Zaczynamy!

  1. Kładziemy na formie jeden komponent "Timer" z palety "System".
  2. Przechodzimy do "Inspektora Obiektów" i ustawiamy właściwość "Interval" na "10".
  3. Przechodzimy do zakładkiEventsi dwukrotnie klikamy na puste pole znajdujące się po prawej stronie od zdarzenia "OnTimer".
  4. W ten sposób przeszliśmy do edytora kodu, aDelphiwygenerował nową procedurę, którą uzupełniamy następująco: procedure TForm1.Timer1Timer(Sender: TObject); var i : integer; promien : integer; begin // Zamalowywanie ekranu (żeby rysunki się nie nakładały) form1.Canvas.Pen.Color := clBlack; form1.Canvas.Brush.Color := clBlack; form1.Canvas.Rectangle(0,0,form1.ClientWidth,form1.ClientHeight); // Pętla chodzi po wszystkich bańkach for i:=Low(pozycja) To High(pozycja) Do begin // Zmiana koloru obramowania, wraz ze wzrostem bańki form1.Canvas.Pen.Color := RGB(255-wielkosc[i]*5,255-wielkosc[i]*5,255-wielkosc[i]*5); promien := wielkosc[i] div 2; form1.Canvas.Ellipse(pozycja[i].X-promien,pozycja[i].Y-promien,pozycja[i].X+promien,pozycja[i].Y+promien); // Powiększ o 1 wielkość aktualnie przerabianej bańki Inc(wielkosc[i]); // Jeśli wielkość jest ponad 50 to niech bańka zniknie i na jej miejscu powstanie nowa if wielkosc[i] > 50 Then begin // Jaka ma być pozycja nowej bańki pozycja[i] := Point(Random(form1.ClientWidth),Random(form1.ClientHeight)); // Jaki ma być rozmiar nowej bańki (można wstawić np. 1) wielkosc[i] := Random(10); end; end; end;
  5. Przechodzimy do formularza (tzw. widok projektu) naciskając klawisz F12.
  6. Klikamy nań dwukrotnie, przechodząc w ten sposób powtórnie do edytora kodu, który czeka na nas z nowo-wygenerowaną procedurą "OnCreate", którą uzupełniamy następująco: procedure TForm1.FormCreate(Sender: TObject); var i : integer; begin Randomize; // Losowanie początkowych pozycji i wielkości baniek (początkowe wypełnienie tablic) for i:=Low(pozycja) To High(pozycja) Do begin // Losowanie początkowej pozycji bańki pozycja[i] := Point(Random(Form1.ClientWidth),Random(Form1.ClientHeight)); // Losowanie początkowej wielkości bańki wielkosc[i] := Random(50); end; // Kolor wypełnienia czarny, kolor pióra biały początkowo) form1.Canvas.Brush.Color := clBlack; form1.Canvas.Pen.Color := clWhite; end;
  7. Pozostaje jeszcze dopisać do globalnych zmiennych, znajdujących się przed sekcją "implementation" dwie zmienne: var Form1: TForm1; pozycja : array[1..ilosc] of TPoint; wielkosc : array[1..ilosc] of integer;
  8. Przed sekcją "var" wstawiamy sekcjęconsti dodajemy do niej nową stałą: ilosc = 100;
  9. To już wszystko! Program powinien się uruchomić.

Piszemy program rysujący na bitmapie pamięciowej

I co? Lepszy efekt był gdy używaliśmyDirectX? To nie podlega wątpliwości, zawsze będzie lepszy i nie ma nawet co porównywać. Musi się dać jednak coś zrobić by tak nie migało! Hmm... co można zrobić? Narysować na bitmapie pamięciowej, a następnie wyświetlić! Zaraz zobaczycie, że takie rozwiązanie całkowicie spełni położone w nim nadzieje.
Zaczynamy:

Co musimy zmienić w stosunku do poprzedniego programu?

  1. Przedewszystkim należy globalnie zadeklarować i utworzyć nową zmienną typu TBitmap np.:

    (deklaracja bitmapy pamięciowej) var Form1: TForm1; pozycja : array[1..ilosc] of TPoint; wielkosc : array[1..ilosc] of integer; bitmapa : TBitmap; (utworzenie bitmapy pamięciowej) procedure TForm1.FormCreate(Sender: TObject); var i : integer; begin bitmapa := TBitmap.Create; // Bitmapa będzie miała rozmiary naszego formularza bitmapa.Width := form1.ClientWidth; bitmapa.Height := form1.ClientHeight; Randomize; ........
  2. następnie należy zmienić wszystkie odwołania do klasy Canvas formularza na odwołania do klasy Canvas bitmapy. Oto przykład takiej zmiany: form1.Canvas.Brush.Color := clBlack; na bitmapa.Canvas.Brush.Color := clBlack;
  3. na końcu procedury obsługującej zdarzenie "OnTimer" musimy dopisać funkcję, która będzie wyświetlała naszą bitmapę na formularzu: form1.Canvas.Draw(0, 0, bitmapa);
  4. To wszystko! Jeśli coś nie działa, spróbuj zapoznać się z kodem źródłowym, który został umieszczony tutaj: kody źródłowe wszystkich przykładów.

Zakończenie

To już wszystko! Wszystko starałem się tłumaczyć używając komentarzy. Kod podałem w pełni, bez rozbicia na mniejsze części. Taki sposób wydaje się bardziej przystępny dla czytelnika, gdyż nie musi on składać procedury ze szczątków kodu porozrzucanych po artykule.
Pamiętajcie! Zawsze rysujcie na bitmapach pamięciowych, następnie je wyświetlajcie. Taki sposób po prostu ułatwi wam pracę, bardzo przyspieszy rysowanie i zaoszczędzi wielu potyczek z błędami graficznymi.
Dzięki wszystkim za uwagę!

12345
Delphi - Efekt fali Autor opinii: Czytelnicy, data przesłania: 4

Podobne artykuly:

Skomentuj

Aby zamieścić komentarz, proszę włączyć JavaScript - niestety roboty spamujące dają mi niezmiernie popalić.






Komentarze czytelników

    • lupolan
    • Wed, 24 November 2010, 12:18
    • Problem rozwiązany




      Odp: Miło to słyszeć. Pozdrawiam!
    • lupolan
    • Tue, 23 November 2010, 9:07
    • Witam.Zrobiłem tak jak pisałeś. Rysowanie na bitmapie pamięciowej rzeczywiście załatwia wszystko. Jednak, jak zawsze, pojawił się problem. Program ma przenieść na wykres wszystkie punkty pomiarowe (X-czas,Y-waga).Punkty pomiarowe na razie umieściłem w komponencie ListView (docelowe będą zapisywane do pliku i z niego odczytywane). W pętli odczytującej kolejne wiersze komponentu ListView są przeliczane kolejne wartości wagi i czasu na piksele i wstawiane do bitmapa.canvas.ellipse(X,Y,X+10,Y+10).Po odczytaniu w pętli wszystkich wierszy ListView próbowałem wyświetlić bitmape na formularzu. Program jednak nie działa. Moje pytanie brzmi czy jest możliwe narysowanie w pętli wszystkich punktów pomiarowych (elips) na bitmapie a później wyświetlenie bitmapy na formie?
    • lupolan
    • Mon, 25 October 2010, 18:28
    • Witam!
      Piszę program odczytujący wartości z 4 wag. Program na zapisać dla poszczególnych wag informacje o wadze artykułu wraz z czasem ważenia.
      Docelowym zadaniem tego programu jest wykres (a właściwie to 4 wykresy), wagi w funkcji czasu.
      Początkowo wykresy rysowałem przy użyciu komponentu tchart, jednak nie można (tego nie wiem) minimum bottom.axis ustawić na 06:00:00 a maximum na 05:59:59. Jest to jednak warunek sposobu wyświetlania osi X tych wykresów, więc czy rysowanie na bitmapie pamięciowej jest tym co powinienem tutaj zastosować? Dodam może jeszcze, że program musi zapisywać o 05:59:59 wykresy dla 4 wag. Następnie umieścić "czysty" formularz z osiami, siatką.




      Odp: Nie orientuje się dokładnie w tym co napisałeś. Jednak ogólnie rzecz biorąc, na bitmapie pamięciowej narysujesz wszystko (nawet najdziwniejszy wykres). Więc jeśli specjalistyczny komponent nie daje rady - można spróbować samemu narysować wykres.
Dexter