PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C#

Krzywe Lissajous Formularz aplikacji Zasoby Algorytm animacji Reprezentacja danych Kopiowanie i wyświetlanie obrazów Program w C# Poprzedni przykład Następny przykład Program w C++ Kontakt

Krzywe Lissajous

Dwa niezależne drgania harmoniczne punktu material­nego w kierun­kach wzajemnie prostopa­dłych można opisać równa­niami przedsta­wiającymi zależność jego przemie­szczenia w czasie t:

w których A1A2 są amplitu­dami (maksymal­nymi wychyle­niami), ω1ω2 częstotli­wościami, a φ1φ2 fazami początko­wymi tych drgań. W wyniku złożenia obu drgań uzyskuje się intere­sujące kształty toru wypadko­wego ruchu punktu zwane figurami lub krzywymi Lissajous. Ich obrazy można otrzymać na ekranie oscylo­skopu, a także mechani­cznie za pomocą specjal­nego wahadła zbudowa­nego z lejka napełnio­nego piaskiem, wykonu­jącego drgania jedno­cześnie w dwóch płaszczy­znach i tworzą­cego krzywą z piasku.

Naszym zadaniem jest opracowanie programu okienko­wego (aplikacji Windows Forms) w języku C# prezentują­cego genero­wanie krzywej Lissajous o zadanych parame­trach. Program ma korzystać z techniki animacji kompute­rowej opartej na manipu­lowaniu bitmapami i użyciu zegara zgłasza­jącego zdarzenia co określoną liczbę milisekund.

Formularz aplikacji

Formularz główny powinien zawierać powierz­chnię, po której będzie poruszał się punkt materialny (kulka, gwiazdka itp.) rysu­jący krzywą Lissajous, a także kontrolki ukazu­jące jej parametry. Rzecz jasna, stosowny interfejs graficzny (GUI) powinien umożliwiać użytko­wnikowi wygodną zmianę tych parame­trów. Przedsta­wione poniżej rozwią­zanie polega na podziale formu­larza na dwie części (rys.). Jego lewą część zajmuje panel sterujący o właści­wościach DockBackColor ustawio­nych, odpowiednio, na LeftControl. W prawej, większej części formu­larza o właści­wości BackColor równej Info (kolor jasny, żółto-kremowy) będzie prezento­wana animacja oparta na kompo­nencie niewizu­alnym Timer (zegar, czaso­mierz). Po wsta­wieniu go do formu­larza przypisu­jemy jego właści­wości Inverval (interwał czasowy) wartość 10, dzięki czemu do aplikacji będą przycho­dziły co 10 milise­kund komuni­katy zegarowe. Ich obsługa będzie polegała na wyliczeniu nowego położenia punktu material­nego, doryso­waniu przebytego przezeń odcinka krzywej i poka­zaniu obrazu tego punktu w nowym miejscu. Aby uniknąć problemów wynika­jących ze zmiany rozmiaru okna podczas animacji, właści­wość FormBorder­Style formu­larza usta­wiamy na Fixed­Single (pojedyncza stała ramka) i Maxi­mizeBox na False (zabloko­wana maksymali­zacja).

Nieco rozbudowany panel sterujący składa się z dwóch konte­nerów GroupBox i dwóch przycisków Button. Jeden i drugi kontener jest otoczony ramką z tekstem obejmu­jącą trzy opisane etykietami kontrolki umożli­wiające wprowa­dzenie parametrów drgania względem osi xy. Pierwsza etykieta jest niekom­pletna – górne ograni­czenie amplitudy drgania i wstępna jej wartość zostaną wyzna­czone na początku przebiegu programu. Przycisk Start ma urucha­miać animację. Gdy zostanie naciśnięty, jego etykieta zostanie zamie­niona na Stop. Ponowne naciśnięcie przycisku spowoduje zatrzy­manie animacji i zamianę jego etykiety na Kontynuuj. Użytkownik będzie mógł wtedy wznowić animację albo wyczyścić powierz­chnię rysowania i przejść do edycji parame­trów krzywej. Przycisk Wyczyść jest wstępnie zabloko­wany (właści­wość Enabled równa False) – uakty­wniany będzie za każdym razem, gdy animacja zostanie zatrzymana. Parametry krzywej będzie można zmieniać na początku przebiegu programu lub po wyczyszczeniu powierzcni rysowania, w trakcie animacji będą dostępne tylko do odczytu (właści­wość ReadOnly ustawiona na True).

Zasoby

Niewielka bitmapa (mapa bitowa) jest często jednym z elementów zasobów programu (ang. program resources). Zasoby (teksty, obrazy, dźwięki itp.) są zazwyczaj włączane w trakcie kompilacji do pliku wykonaw­czego, mogą też być przecho­wywane w osobnym pliku binarnym. W rozpatry­wanym przykładzie zasobem będzie obrazek gwiazdki (rys.) wyobraża­jącej punkt materialny przemieszcza­jący się po ekranie i gene­rujący krzywą Lissajous. Obrazek został utworzony w popu­larnym programie GIMP. Jest zapisany w pliku grafi­cznym znaczek.png, ma rozmiar 23x23 piksele i warstwę przezro­czystości, dzięki której tło podłoża wokół gwiazdki nie będzie zasłaniane.

Bitmapę dodajemy do zasobów programu, rozpoczy­nając do polecenia Projekt Właściwości... . Następnie po zazna­czeniu opcji Zasoby w lewym panelu okna i rozwi­nięciu listy Dodaj zasób wybieramy polecenie Dodaj istniejący plik... (rys.). Na koniec w typowym oknie otwierania pliku, które się pojawi, znajdujemy potrzebny plik i potwier­dzamy jego wybór, naciskając przycisk Otwórz. Plik zostanie skopio­wany do folderu Resources aplikacji, a jego ikona z nazwą zasobu zostanie umieszczona w oknie zasobów.

Algorytm animacji

Animacja ma przedstawiać gwiazdkę (punkt materialny) poruszającą się wzdłuż krzywej opisanej równaniami parame­trycznymi podanymi na początku niniejszej witryny. Tło, po którym ma przesuwać się gwiazdka, będzie się zmieniać, gdyż ma ona jedno­cześnie rysować krzywą, po której się porusza. Do wyzna­czania położenia gwiazdki używać będziemy zmiennych rzeczy­wistych (pól formu­larza) t i dt oznacza­jących czas i stały odstęp pomiędzy dwoma kolejnymi momentami czasowymi, zmiennych całko­witych x i y określa­jących bieżącą pozycję gwiazdki oraz metody ObliczXY oblicza­jącej ich wartości w momencie t. Na początku, gdy naciśnięty zostanie przycisk Start, pobie­rzemy parametry krzywej, obli­czymy położenie gwiazdki w momencie początkowym zero, narysujemy jej obrazek i uaktywnimy zegar:

private void button1_Click(object sender, EventArgs e)
{
    ...                               // Pobierz parametry krzywej
    ObliczXY(t = 0, out x, out y);
    ...                               // Rysuj gwiazdkę w punkcie (x,y)
    timer1.Enabled = true;
}

Od tej chwili zegar będzie generował zdarzenia ze stałą częstotli­wością określoną przez właści­wość Interval (przyjęto 10 milisekund). Ich obsługa będzie polegać na przejściu do kolejnego momentu czasowego przesu­niętego o przyrost dt względem momentu poprze­dniego, wyli­czeniu nowego położenia gwiazdki, odtwo­rzenia zamazanego przez nią tła, naryso­waniu fragmentu toru przeby­tego przez gwiazdkę w ostatnim odcinku czasowym i naryso­waniu jej w nowym miejscu:

private void timer1_Tick(object sender, EventArgs e)
{
    int x1, y1;
    ObliczXY(t += dt, out x1, out y1);
    ...                               // Odtwórz tło ekranu pod gwiazdką
    ...                               // Rysuj linię od punktu (x,y) do (x1,y1)
    ...                               // Rysuj gwiazdkę w punkcie (x1,y1)
    x = x1;
    y = y1;
}

Ten schemat animacji jest bardzo uproszczony. Po pierwsze, metoda button1_­Click uwzględnia tylko funkcję urucho­mienia animacji za pomocą przycisku button1, który będzie również służył do jej zatrzymania (Stop) i wznowienia (Kontynuuj). Po drugie, metoda timer1_­Tick obsługi zdarzeń zegarowych odpowie­dzialna za animację nie powinna ze względu na odświe­żanie ekranu rysować bezpośrednio na formu­larzu, gdyż prowadzi­łoby to do zmniej­szenia szybkości animacji i efektu migotania ekranu. Przetwa­rzanie obrazu powinno się odbywać na obiektach (bitmapach) znajdu­jących się w pamięci opera­cyjnej komputera, a na koniec tylko jedna operacja powinna narysować gotowy obraz lub tylko jego zmieniony fragment na powierz­chni formularza.

Reprezentacja danych

Rozbudowę wygenerowanego przez środo­wisko kodu źródło­wego klasy Form1 rozpoczy­namy od zadekla­rowania szeregu pól przyda­tnych w animacji przedsta­wiającej powsta­wanie krzywej Lissajous:

private double A1, A2;           // Amplitudy drgań względem osi x i y
private double Omega1, Omega2;   // Częstotliwości drgań
private double Fi1, Fi2;         // Fazy początkowe drgań
private double t, dt;            // Bieżący czas i przyrost czasowy
private int x, y;                // Bieżące położenie punktu (gwiazdki)

private Bitmap Krzywa = null;    // Obraz krzywej (bez gwiazdki)
private Bitmap Pkt = null;       // Obraz punktu materialnego (gwiazdki)
private Bitmap Tmp = null;       // Kopia robocza ekranu
private int DX, DY;              // Współrzędne środka obrazu krzywej
private int PX, PY;              // Współrzędne środka obrazu punktu (gwiazdki)

private int A1Max, A2Max;        // Maksymalne amplitudy drgań
private bool Animacja = false;   // Animacja rozpoczęta - true, nie - false

Sześć pól o sugestywnych nazwach pierwszej grupy repre­zentuje parametry krzywej, cztery kolejne są przezna­czone do pamię­tania aktualnego stanu animacji (zob. omówiony wyżej algorytm): bieżącego czasu, przyrostu czasowego i pozycji przesuwa­jącej się gwiazdki (punktu material­nego). Następna grupa pól dotyczy trzech obrazów (bitmap) przechowy­wanych i przetwa­rzanych w pamięci opera­cyjnej celem zminimali­zowania efektu migotania ekranu. Trzy pola tej grupy będą zawierać refe­rencje do tych obrazów, zaś cztery – współ­rzędne całkowite ich środków. Dwa pola trzeciej grupy posłużą do uaktual­nienia etykiet określa­jących maksymalne amplitudy drgań i kontroli wprowa­dzanych danych. Ostatnie pole ma wskazywać, czy odbywa się animacja. Jego wstępną wartością jest false (animacja jeszcze się nie rozpoczęła). Wartości części tych pól są wyzna­czane w metodzie obsługi zdarzenia Load formu­larza:

private void Form1_Load(object sender, EventArgs e)
{
    Krzywa = new Bitmap(ClientSize.Width - panel1.Width, ClientSize.Height);
    A1Max = ((Krzywa.Width - 20) / 20) * 10;
    A2Max = ((Krzywa.Height - 20) / 20) * 10;
    label1.Text = String.Format("Amplituda (0 - {0}): ", A1Max);
    label4.Text = String.Format("Amplituda (0 - {0}): ", A2Max);
    Amplituda1.Text = A1Max.ToString();
    Amplituda2.Text = A2Max.ToString();
    Pkt = new Bitmap(Properties.Resources.znaczek);
    Tmp = new Bitmap(Krzywa.Width, Krzywa.Height);
    DX = Krzywa.Width / 2;
    DY = Krzywa.Height / 2;
    PX = Pkt.Width / 2;
    PY = Pkt.Height / 2;
    using (Graphics g = Graphics.FromImage(Krzywa))
        g.Clear(BackColor);
    using (Graphics g = Graphics.FromImage(Tmp))
        g.Clear(BackColor);
}

Najpierw tworzona jest bitmapa Krzywa o roz­miarze prawej części obszaru roboczego formu­larza. Na niej będzie rysowana w trakcie animacji krzywa Lissajous. Następnie obliczane są maksy­malne amplitudy drgań w kierunku jednej i drugiej osi zmodyfi­kowane ze względów estety­cznych tak, by ostatnim znakiem w ich zapisie dziesię­tnym była cyfra 0 oraz by krzywa o tych amplitu­dach mieściła się z małym margi­nesem na bitmapie. Obli­czone wartości są dalej używane do uaktual­nienia etykiet opisu­jących pola edycyjne amplitud i nadania tym polom wartości początko­wych. Kolejną operacją jest utwo­rzenie bitmapy Pktzasobu znaczek reprezen­tującego gwiazdkę i bitmapy Tmp, na której będą wykony­wane przekształ­cenia obrazu zamiast na powierz­chni formu­larza. Na koniec, po wyzna­czeniu współrzę­dnych środków obrazów krzywej i gwiazdki, bitmapy KrzywaTmp są czyszczone i wypeł­niane kolorem tła formu­larza. Użycie instrukcji using daje gwarancję, że po wykorzy­staniu obiektu klasy Graphics utworzo­nego dla bitmapy za pomocą metody FromImage zasoby z nim związane zostaną zwolnione. Pamięć przydzie­lona trzem bitmapom zostanie odzyskana w metodzie obsługi zdarzenia FormClosed okna wyzwala­nego po jego zamknięciu:

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
    if (Tmp != null) Tmp.Dispose();
    if (Pkt != null) Pkt.Dispose();
    if (Krzywa != null) Krzywa.Dispose();
}

Powróćmy do sześciu pól określa­jących parametry krzywej. Ich wartości należy pobrać przed rozpo­częciem animacji z kontrolek formularza usytuo­wanych na panelu steru­jącym. Cztery (amplitudy i częstotli­wości drgań) wymagają kontroli poprawności, ponieważ mają reprezen­tować liczby nieujemne, a do tego dwa z nich (amplitudy) nie powinny przekra­czać wartości maksy­malnej wynikającej z rozmiaru powierz­chni rysowania. Podobnie jak w przypadku pól formularza podrzę­dnego w programie kreślenia epicy­kloidy i hipocy­kloidy posłużymy się pomocniczą metodą, która przekształca tekst kontrolki edycy­jnej klasy TextBox na liczbę rzeczy­wistą:

private double Liczba(TextBox tb, int max)
{
    tb.Focus();
    double x = Convert.ToDouble(tb.Text);
    if (x < 0 || max > 0 && x > max)
        throw new Exception("Wartość przekracza dozwolony zakres.");
    return x;
}

Pierwszym argumentem metody jest pole, którego tekst ma być zwracany jako liczba rzeczy­wista, drugim maksymalna dopuszczalna jej wartość (gdy jest równa zero, kontrola górnego ograni­czenia liczby jest pomijana). Metoda Focus ustawia kursor wewnątrz kontrolki, dzięki czemu będzie on w przypadku wyjątku wskazywał miejsce wystą­pienia błędu. Pobranie wszystkich parametrów krzywej można teraz zaprogra­mować nastę­pująco:

private bool PobierzDane()
{
    try
    {
        A1 = Liczba(Amplituda1, A1Max);
        Omega1 = Liczba(Częstotliwość1, 0);
        A2 = Liczba(Amplituda2, A2Max);
        Omega2 = Liczba(Częstotliwość2, 0);
        Fi1 = (int)Faza1.Value * Math.PI / 180;
        Fi2 = (int)Faza2.Value * Math.PI / 180;
        return true;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Błąd", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }
}

Fazy początkowe drgań zawarte w kontrolkach klasy NumericUpDown są wyrażone w stopniach, dlatego ze względu na miarę kąta funkcji cosinus w równa­niach parametry­cznych krzywej są konwerto­wane na radiany. Wszystkie kontrolki edycyjne powinny w trakcie animacji uniemożli­wiać edycję danych. Najprościej zadanie to można rozwiązać, przypi­sując właści­wości Enabled (inter­akcja z użytko­wnikiem) dwóch kontenerów GroupBox, na których te kontrolki leżą, wartość logiczną False (blokada edycji) lub True (edycja dozwolona). Zablo­kowane kontrolki wydają się jednak zbyt wyblakłe, toteż posłużymy się ich właści­wością ReadOnly (tylko do odczytu). Wprawdzie kontrolki Faza1Faza2 sprawiają niespo­dziankę, gdyż ustawione jako tylko do odczytu umożli­wiają zmianę wartości Value za pomocą strzałek, lecz łatwo temu zaradzić, nadając ich właści­wości Increment wartość 0 lub 1 w zależ­ności od tego, czy edycja danych ma być zabloko­wana, czy nie:

private void BlokujDane(bool blokuj)
{
    Amplituda1.ReadOnly = blokuj;
    Częstotliwość1.ReadOnly = blokuj;
    Faza1.ReadOnly = blokuj;
    Faza1.Increment = blokuj ? 0 : 1;
    Amplituda2.ReadOnly = blokuj;
    Częstotliwość2.ReadOnly = blokuj;
    Faza2.ReadOnly = blokuj;
    Faza2.Increment = blokuj ? 0 : 1;
}

Kopiowanie i wyświetlanie obrazów

Przejdźmy wreszcie do sprecyzowania metod zapowie­dzianych w omówionym wyżej algorytmie animacji. Zaczy­namy od metody ObliczXY, która ma wyznaczać współ­rzędne punktu material­nego wykonu­jącego drgania harmo­niczne w dwóch wzajemnie prosto­padłych kierun­kach. Korzy­stając z podanych na początku niniejszej witryny równań parametry­cznych opisu­jących przemie­szczenie punktu w czasie, możemy tę metodę sformu­łować nastę­pująco:

private void ObliczXY(double t, out int x, out int y)
{
    x = (int)(A1 * Math.Cos(Omega1 * t + Fi1)) + DX;
    y = DY - (int)(A2 * Math.Cos(Omega2 * t + Fi2));
}

W obliczeniach współrzędnych punktu uwzglę­dniono środek bitmapy Tmp i odwró­cony kierunek jej osi y, gdyż będzie on na tej bitmapie rysowany. Metodę wywołuje się po raz pierwszy dla momentu czasowego zero, gdy wartością zmiennej Animacja jest false i użytko­wnik naciśnie przycisk Start, by pobrać z kontrolek okna dane reprezen­tujące para­metry krzywej i rozpocząć animację:

private void button1_Click(object sender, EventArgs e)
{
    if (!Animacja)
    {
        if (!PobierzDane()) return;
        ObliczXY(t = 0, out x, out y);
        dt = WSP / (Math.Max(Omega1 + Omega2, 1.0));
        using (Graphics g = Graphics.FromImage(Tmp))
            g.DrawImage(Pkt, x - PX, y - PY);
        using (Graphics g = CreateGraphics())
            g.DrawImage(Pkt, x + panel1.Width - PX, y - PY);
        Animacja = true;
        BlokujDane(true);
    }
    timer1.Enabled = !timer1.Enabled;
    button1.Text = timer1.Enabled ? "&Stop" : "&Kontynuuj";
    button2.Enabled = !timer1.Enabled;
    button1.Focus();
}

Oprócz określenia parametrów krzywej i wyzna­czenia początkowej pozycji punktu we wstępnych przygoto­waniach do animacji podejmo­wana jest decyzja odnośnie wielkości odstępu dt pomiędzy dwoma kolejnymi momentami czasowymi. Jego wartość jest wyzna­czana jako odwrotnie proporcjo­nalna do sumy częstotli­wości drgań w obu kierun­kach przy współ­czynniku WSP dobranym empiry­cznie dla rozmiaru powierz­chni ryso­wania (stała 0,04). Ustalenia te zapewniają do pewnego stopnia jednakową szybkość animacji dla różnych parametrów drgań.

Gdy znane jest początkowe położenie punktu, można na czystej bitmapie Tmp i powierz­chni okna aplikacji narysować bitmapę Pkt reprezen­tująca gwiazdkę. Obie operacje rysowania wymagają dostępu do obiektu klasy Graphics, który jest tworzony w zasięgu instrukcji using za pomocą metody FromBitmap klasy Graphics dla bitmapy oraz metody Create­Graphics klasy Control dla formu­larza. Po naryso­waniu gwiazdki zmiennej Animacja przypisana zostaje wartość true (animacja rozpo­częta), blokowana jest edycja parame­trów krzywej i urucha­miany jest zegar generu­jący zdarzenia sterujące animacją. Dodatkowo etykieta pierwszego przycisku zostaje zmie­niona na Stop (drugi przycisk pozostaje nadal zablokowany).

Działanie metody button1_Click jest inne w przypadku naciśnięcia przycisku w sytuacji, gdy zmienna Animacja ma wartość true. Operacje przygoto­wawcze do rozpo­częcia animacji są wówczas pomijane, a animacja zależnie od stanu zegara (aktywny lub nie) zostaje zatrzy­mana lub jest wznawiana po uprzednim zatrzy­maniu. Status animacji jest wskazy­wany poprzez etykietę przycisku. Mianowicie gdy trwa animacja (zegar włączony), na przycisku widnieje etykieta Stop, a gdy jest wstrzymana (zegar wyłączony), etykieta Kontynuuj. Drugi przycisk o etykiecie Wyczyść jest aktywny jedynie wtedy, gdy animacja jest zatrzymana. Naciśnięcie go powoduje powrót do stanu początko­wego:

private void button2_Click(object sender, EventArgs e)
{
    using (Graphics g = Graphics.FromImage(Krzywa))
        g.Clear(BackColor);
    using (Graphics g = Graphics.FromImage(Tmp))
        g.Clear(BackColor);
    using (Graphics g = CreateGraphics())
        g.Clear(BackColor);
    Animacja = false;
    button2.Enabled = false;
    BlokujDane(false);
    button1.Text = "&Start";
    Amplituda1.Focus();
}

Cechą charakterystyczną animacji w języku C# opartych na kompo­nencie Timer jest obsługa jego jedy­nego zdarzenia Tick genero­wanego cyklicznie z częstotli­wością określoną przez właści­wość Interval. Metoda obsługi tego zdarzenia powinna przetwa­rzać obrazy przecho­wywane w pamięci opera­cyjnej komputera (bitmapy) i na koniec przery­sować na powierz­chni formu­larza fragment obrazu, który uległ zmianie od poprze­dniego zdarzenia zegaro­wego. Liczba operacji rysowania bezpo­średnio na ekranie zostanie w ten sposób zredu­kowana do jednej, dzięki czemu efekt migotania ekranu zostanie zminima­lizowany. W rozpatry­wanym programie metoda używa bitmapy Krzywa do rysowania krzywej i bitmapy Tmp do rysowania gwiazdki na tle krzywej:

private void timer1_Tick(object sender, EventArgs e)
{
    int x2, y2;
    ObliczXY(t += dt, out x2, out y2);
    using (Graphics g = Graphics.FromImage(Krzywa))
        g.DrawLine(Pens.Blue, x, y, x2, y2);
    Rectangle r1 = new Rectangle(x - PX, y - PY, Pkt.Width, Pkt.Height);
    Rectangle r2 = new Rectangle(x2 - PX, y2 - PY, Pkt.Width, Pkt.Height);
    Rectangle ru = Rectangle.Union(r1, r2);
    using (Graphics g = Graphics.FromImage(Tmp))
    {
        g.DrawImage(Krzywa, ru.Left, ru.Top, ru, GraphicsUnit.Pixel);
        g.DrawImage(Pkt, r2.Left, r2.Top);
    }
    using (Graphics g = CreateGraphics())
        g.DrawImage(Tmp, ru.Left + panel1.Width, ru.Top, ru, GraphicsUnit.Pixel);
    x = x2;
    y = y2;
}

Na początku obliczane są współrzędne nowej pozycji gwiazdki, po czym na bitmapie Krzywa rysowany jest odcinek wyobra­żający fragment krzywej, który gwiazdka pokonuje w ostatnim interwale czasowym. Następnie tworzone są trzy prosto­kąty (struktury typu Rectangle):

Kolejną operacją jest skopiowanie fragmentu obrazu określonego przez prostokąt ru z bitmapy Krzywa (uaktual­nionego tła) na odpowie­dnie miejsce bitmapy Tmp. Efektem tej operacji jest wymazanie gwiazdki w dotych­czasowym miejscu i pojawienie się odcinka symbolizu­jącego przebyty przez nią fragment drogi. Z kolei skopio­wanie bitmapy Pkt na miejsce określone przez prostokąt r2 na bitmapie Tmp oznacza naryso­wanie gwiazdki w nowym miejscu. Ponieważ prostokąt ru określa fragment obrazu na bitmapie Tmp, który uległ zmianie od poprze­dniego zdarzenia zegaro­wego, wystarczy go skopiować na powierz­chnię formu­larza, by użytkownik odniósł wrażenie ruchu gwiazdki. Na koniec metoda uaktualnia współ­rzędne określa­jące bieżącą pozycję gwiazdki.

Uwaga. Wartość Pixel typu wyliczeniowego GraphicsUnit występu­jąca w roli ostatniego argumentu wywołania metody DrawImage kopiującej prosto­kątny wycinek ru bitmapy określa jednostkę miary dla tego prostokąta.

Poniższy rysunek przedstawia okno programu po kilku­nastu sekundach od urucho­mienia animacji dla krzywej Lissajous o ampli­tudach A1=270 i A2=230, częstotli­wościach ω1=7 i ω2=5 oraz fazach początko­wych φ1=φ2=0o. Jak widać, drgający punkt materialny ozna­czony gwiazdką zaczął drugi raz przebiegać po krzywej (nieco pogru­biony fragment toru drugiego obiegu).

A oto inny przykład okna utworzonego przez program dla krzywej Lissajous o ampli­tudach A1=250 i A2=230, częstotli­wościach ω1=13 i ω2=11 oraz fazach początko­wych φ1=30o i φ2=45o. Tym razem animacja została zatrzy­mana, gdy punkt materialny rozpoczął drugą wędrówkę po tym samym torze.

Program w C#

Nadszedł moment, by zaprezentować pełny kod źródłowy zawarty pliku Form1.cs opraco­wanego programu. Niestety, program ma banalną usterkę, która ujawnia się wtedy, gdy po zwinięciu okna zostanie ono przywrócone. Przykła­dowy wynik jego niepopra­wnego działania przedstawia rysunek:

Przyczyną niepowodzenia jest brak metody obsługi zdarzenia Paint formu­larza. Zważywszy na oszczę­dność pamięci, Windows nie zachowuje rysunku utworzo­nego na powierzchni okna, lecz każdora­zowo, gdy obszar roboczy okna jest uniewa­żniony (wymaga przemalo­wania), czyści go za pomocą pędzla koloru określo­nego we właści­wości BackColor formu­larza i zgłasza zdarzenie Paint, które program powinien obsłużyć. Na szczęście nie ma problemu z odtwa­rzaniem utraco­nego rysunku wygene­rowanego w trakcie animacji, gdyż jego kopia jest przecho­wywana na bitmapie Tmp:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.DrawImage(Tmp, panel1.Width, 0);
}

Pełny kod źródłowy C# w pliku Form1.cs programu prezentu­jącego powsta­wanie krzywej Lissajous za pomocą animacji opartej na przekształ­caniu obrazów i użyciu komponentu zegaro­wego jest przedsta­wiony na poniższym listingu.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Lissa
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private double A1, A2;           // Amplitudy drgań względem osi x i y
        private double Omega1, Omega2;   // Częstotliwości drgań
        private double Fi1, Fi2;         // Fazy początkowe drgań
        private double t, dt;            // Bieżący czas i przyrost czasowy
        private int x, y;                // Bieżące położenie punktu (gwiazdki)

        private Bitmap Krzywa = null;    // Obraz krzywej (bez gwiazdki)
        private Bitmap Pkt = null;       // Obraz punktu materialnego (gwiazdki)
        private Bitmap Tmp = null;       // Kopia robocza ekranu
        private int DX, DY;              // Współrzędne środka obrazu krzywej
        private int PX, PY;              // Współrzędne środka obrazu punktu (gwiazdki)

        private int A1Max, A2Max;        // Maksymalne amplitudy drgań
        private bool Animacja = false;   // Animacja rozpoczęta - true, nie - false
        private const double WSP = 0.04; // Współczynnik skalowania przy obliczaniu dt

        private void Form1_Load(object sender, EventArgs e)
        {
            Krzywa = new Bitmap(ClientSize.Width - panel1.Width, ClientSize.Height);
            A1Max = ((Krzywa.Width - 20) / 20) * 10;
            A2Max = ((Krzywa.Height - 20) / 20) * 10;
            label1.Text = String.Format("Amplituda (0 - {0}): ", A1Max);
            label4.Text = String.Format("Amplituda (0 - {0}): ", A2Max);
            Amplituda1.Text = A1Max.ToString();
            Amplituda2.Text = A2Max.ToString();
            Pkt = new Bitmap(Properties.Resources.znaczek);
            Tmp = new Bitmap(Krzywa.Width, Krzywa.Height);
            DX = Krzywa.Width / 2;
            DY = Krzywa.Height / 2;
            PX = Pkt.Width / 2;
            PY = Pkt.Height / 2;
            using (Graphics g = Graphics.FromImage(Krzywa))
                g.Clear(BackColor);
            using (Graphics g = Graphics.FromImage(Tmp))
                g.Clear(BackColor);
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (Tmp != null) Tmp.Dispose();
            if (Pkt != null) Pkt.Dispose();
            if (Krzywa != null) Krzywa.Dispose();
        }

        private double Liczba(TextBox tb, int max)
        {
            tb.Focus();
            double x = Convert.ToDouble(tb.Text);
            if (x < 0 || max > 0 && x > max)
                throw new Exception("Wartość przekracza dozwolony zakres.");
            return x;
        }

        private bool PobierzDane()
        {
            try
            {
                A1 = Liczba(Amplituda1, A1Max);
                Omega1 = Liczba(Częstotliwość1, 0);
                A2 = Liczba(Amplituda2, A2Max);
                Omega2 = Liczba(Częstotliwość2, 0);
                Fi1 = (int)Faza1.Value * Math.PI / 180;
                Fi2 = (int)Faza2.Value * Math.PI / 180;
                return true;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Błąd", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }
        }

        private void BlokujDane(bool blokuj)
        {
            Amplituda1.ReadOnly = blokuj;
            Częstotliwość1.ReadOnly = blokuj;
            Faza1.ReadOnly = blokuj;
            Faza1.Increment = blokuj ? 0 : 1;
            Amplituda2.ReadOnly = blokuj;
            Częstotliwość2.ReadOnly = blokuj;
            Faza2.ReadOnly = blokuj;
            Faza2.Increment = blokuj ? 0 : 1;
        }

        private void ObliczXY(double t, out int x, out int y)
        {
            x = (int)(A1 * Math.Cos(Omega1 * t + Fi1)) + DX;
            y = DY - (int)(A2 * Math.Cos(Omega2 * t + Fi2));
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (!Animacja)
            {
                if (!PobierzDane()) return;
                ObliczXY(t = 0, out x, out y);
                dt = WSP / (Math.Max(Omega1 + Omega2, 1.0));
                using (Graphics g = Graphics.FromImage(Tmp))
                    g.DrawImage(Pkt, x - PX, y - PY);
                using (Graphics g = CreateGraphics())
                    g.DrawImage(Pkt, x + panel1.Width - PX, y - PY);
                Animacja = true;
                BlokujDane(true);
            }
            timer1.Enabled = !timer1.Enabled;
            button1.Text = timer1.Enabled ? "&Stop" : "&Kontynuuj";
            button2.Enabled = !timer1.Enabled;
            button1.Focus();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            using (Graphics g = Graphics.FromImage(Krzywa))
                g.Clear(BackColor);
            using (Graphics g = Graphics.FromImage(Tmp))
                g.Clear(BackColor);
            using (Graphics g = CreateGraphics())
                g.Clear(BackColor);
            Animacja = false;
            button2.Enabled = false;
            BlokujDane(false);
            button1.Text = "&Start";
            Amplituda1.Focus();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            int x2, y2;
            ObliczXY(t += dt, out x2, out y2);
            using (Graphics g = Graphics.FromImage(Krzywa))
                g.DrawLine(Pens.Blue, x, y, x2, y2);
            Rectangle r1 = new Rectangle(x - PX, y - PY, Pkt.Width, Pkt.Height);
            Rectangle r2 = new Rectangle(x2 - PX, y2 - PY, Pkt.Width, Pkt.Height);
            Rectangle ru = Rectangle.Union(r1, r2);
            using (Graphics g = Graphics.FromImage(Tmp))
            {
                g.DrawImage(Krzywa, ru.Left, ru.Top, ru, GraphicsUnit.Pixel);
                g.DrawImage(Pkt, r2.Left, r2.Top);
            }
            using (Graphics g = CreateGraphics())
                g.DrawImage(Tmp, ru.Left + panel1.Width, ru.Top, ru, GraphicsUnit.Pixel);
            x = x2;
            y = y2;
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(Tmp, panel1.Width, 0);
        }
    }
}

Opracowanie przykładu: czerwiec/lipiec 2019