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

Przykład C#

Od punktu do fontanny Rysowanie punktu i klasa bazowa w C# Klasy pochodne w C# i program testowy Implementacja kropli wody fontanny w C# Wątki w C# Program "Fontanna" Poprzedni przykład Następny przykład Program w C++ Kontakt

Od punktu do fontanny

Język C# jest w pełni obiektowy, budowanie w nim programów wymaga więc umieję­tności programo­wania obiekto­wego, które polega na definio­waniu klas i two­rzeniu według nich obiektów komuniku­jących się pomiędzy sobą. Nawet najpro­stsza apli­kacja konsolowa zawiera defi­nicję klasy Program (por. kod programu Ile cyfr ma liczba?), a okienkowa również defi­nicję klasy Form1 wywodzącej się od biblio­tecznej klasy Form (por. kod programu Gra w chaos), na podstawie której tworzone jest okno apli­kacji (obiekt formu­larza). Programowanie obiektowe, a dokładniej programo­wanie zoriento­wane obiektowo (ang. Object Oriented Programming, OOP) charakte­ryzuje się następu­jącymi cechami:

Klasa nie zajmuje żadnego miejsca w pamięci, jest swoistym wzorcem, według którego tworzy się w pamięci obiekty. Każdy obiekt jest egzempla­rzem (instancją) pewnej klasy, ma własny zestaw pól określa­jących jego cechy, ma również sprecy­zowane przez metody klasy zacho­wanie. Hermety­zacja pozwala na kontrolo­wanie dostępu do pól i metod obiektu dla pozosta­łych części programu. Z kolei dziedzi­czenie umożliwia tworzenie nowych klas na podstawie klas istnie­jących. Klasy pochodne dziedziczą pola i metody klas bazowych, a ponadto mogą zawierać nowe pola i metody rozszerza­jące możliwości odziedzi­czone po klasach bazowych. Natomiast polimor­fizm stano­wiący uzupeł­nienie dziedzi­czności pozwala na określenie innego kodu metod w klasach pochodnych. Oznacza to, że ta sama metoda może w odnie­sieniu do obiektu pochodnego wywołać inne działanie niż w przypadku obiektu bazowego.

Przypomnijmy, że własne rozwiązania obiektowe znalazły zastoso­wanie w kilku progra­mach już prezento­wanych na niniejszej witrynie: funkcja Bessela, obli­czenia kalenda­rzowe, wyzna­czanie dni świątecznych, liniowa aproksy­macja średniokwa­dratowa, wieże Hanoi, układ planetarny oraz tworzenie epicy­kloidy i hipocy­kloidy. Używane w nich obiekty stanowią odzwiercie­dlenie obiektów świata rzeczywi­stego (wieże układane z kolo­rowych krążków, ciała hipotety­cznego układu planetar­nego) i matematy­cznego (pozostałe).

Ważną umiejętnością w programowaniu obiektowym jest tworzenie klas na bazie klas uprzednio zdefinio­wanych. Dzięki mechani­zmowi dziedzi­czenia można utworzyć nową klasę bez modyfiko­wania już gotowego kodu klasy bazowej poprzez uzupeł­nienie o nowe składniki. W dalszych rozważa­niach sformu­łujemy trzy klasy reprezen­tujące punkty, okręgi i gwiazdki oraz program testowy umożliwia­jący ich rysowanie na ekranie i przesu­wanie, a na koniec klasę wyobra­żającą krople wody i program imitujący fontannę. Klasa reprezen­tująca punkt będzie bazową dla klasy przedsta­wiającej okrąg, a ta z kolei bazową dla klas wyobraża­jących gwiazdkę i kroplę wody.

Rysowanie punktu i klasa bazowa w C#

Podstawową zasadą, którą należy kierować się w programo­waniu obiektowym, jest trakto­wanie klasy jako odpowie­dnika pewnej grupy obiektów rzeczy­wistych, których dotyczy program. W defi­nicji klasy należy wziąć pod uwagę tylko istotne, wspólne cechy i zacho­wania tych obiektów. Jeśli np. zadaniem programu jest kreślenie na ekranie prostych figur geometry­cznych i ich przesu­wanie, to przy określaniu pól i metod najpro­stszej klasy reprezen­tującej punkty, bazowej dla pozosta­łych klas uosabia­jących inne figury, wypada uwzglę­dnić współ­rzędne i kolor punktu oraz jego pokazy­wanie, ukrywanie i przesu­wanie.

Rzecz jasna ryso­wanie punktu różni się od ryso­wania innych figur geometry­cznych. W obszernym zestawie narzędzi ofero­wanych przez klasę Graphics obejmu­jącym m.in. ryso­wanie linii, prosto­kątów, elips, łuków, wielo­kątów i krzywych Beziera nie ma jednak metody ryso­wania punktu. Oczywistym rozwią­zaniem problemu jest utwo­rzenie mapy bitowej o rozmiarze jednego piksela i ryso­wanie jej za pomocą metody DrawImage. W rozpatry­wanym przypadku warto tego uniknąć, gdyż klasy pochodne (okrąg, gwiazdka i kropla) wywodzące się od klasy reprezen­tującej punkt nie wymagają bitmap do ryso­wania określonych przez nie figur. Wydaje się, że można łatwo sobie z tym poradzić za pomocą metody DrawLine rysu­jącej odcinek łaczący dwa punkty, które w przy­padku powierz­chni ekranowej są utożsa­miane z pikselami o współ­rzędnych całko­witych. Jednak gdy argu­menty metody określają ten sam piksel jako punkt początkowy i końcowy odcinka, nie rysuje ona niczego, zaś gdy określają dwa sąsiednie piksele, rysuje odcinek złożony z obydwu tych pikseli, nie z jednego.

To zadziwiające zachowanie metody DrawLine przywodzi na myśl, aby zamiast współrzę­dnych całko­witych posłużyć się współ­rzędnymi rzeczy­wistymi pojedyn­czej precyzji określa­jącymi dwa bliskie punkty, które obiekt klasy Graphics przekon­wertuje na współ­rzędne ekranowe jednego piksela. Oto przykładowy program testowy rysujący w kwadracie po lewej stronie 10 losowych odcinków, których końcami są dwa sąsiednie punkty o współ­rzędnych typu int, a w kwadracie po prawej stronie 10 analogi­cznych odcinków reduku­jących się do jednego piksela, których końcami są dwa punkty o współ­rzędnych typu float przesu­nięte względem siebie o 1/4 piksela:

using System;
using System.Drawing;
using System.Windows.Forms;

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

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            Random gen = new Random();
            int x1 = label1.Left;
            int y1 = label1.Top + 20;
            int x2 = label2.Left;
            e.Graphics.DrawRectangle(Pens.Red, x1, y1, 150, 150);
            e.Graphics.DrawRectangle(Pens.Red, x2, y1, 150, 150);
            x1 += 15; x2 += 15; y1 += 15;
            for (int k = 0; k < 10; k++)
            {
                int dx = gen.Next(121);
                int dy = gen.Next(121);
                int x = x1 + dx;
                int y = y1 + dy;
                e.Graphics.DrawLine(Pens.Navy, x, y, x + 1, y);
                x = x2 + dx;
                e.Graphics.DrawLine(Pens.Navy, x, y, x + 0.25F, y);
            }
        }
    }
}

Trzy argumenty typu int w drugim wywołaniu metody DrawLine są ze względu na czwarty argument konwerto­wane na typ float (napis 0.25F oznacza stałą tego typu). W programie używane są więc dwie różne metody o tej samej nazwie – pierwsza dla współ­rzędnych całko­witych, druga dla rzeczy­wistych pojedynczej precyzji (zob. technika przecią­żania metod). Efekt wykonania programu przedstawia poniższy rysunek.

Ze względu na małe rozmiary wyświetlanych obiektów łatwiej jest sprawdzić wynik działania programu na powię­kszonym cztero­krotnie obrazie (rys.). Wydaje się, że propono­wane rozwią­zanie jest poprawne. Gdy jednak przeka­zane do metody DrawLine w argu­mentach typu float współ­rzędne punktu początko­wego nie są całko­wite, uzyskany efekt jej wywołania nie zawsze jest zgodny z oczeki­waniami. Zdarza się, że metoda nie rysuje wtedy niczego, a przy zwiększonym odstępie między punktami wynoszącym 1/2 piksela rysuje dwa piksele zamiast jednego.

W programie imitującym fontannę nie będzie istotne, jak rysowane są punkty, gdyż klasa je reprezen­tująca ma być jedynie bazową dla innych klas, których obiekty będą rysowane inaczej. Najwygo­dniej jest zdefi­niować ją z poziomu aplikacji. Tworzymy zatem projekt nowej aplikacji okien­kowej, nadając jej nazwę figuryTest, a nastę­pnie dodajemy do niego klasę, którą zapisujemy w pliku Punkty.cs. W wygene­rowanym przez środo­wisko Visual Studio kodzie nowej klasy zastę­pujemy cały ciąg dyrektyw włącza­jących zbędne w niej przestrzenie nazw dwoma dyrekty­wami włącza­jącymi przestrzenie System.­DrawingSystem.Win­dows.Forms, które będą w niej używane. Zmieniamy również nazwę deklaro­wanej przestrzeni figuryTest na Punkty i nazwę klasy na Punkt.

Rozbudowę pustego szkieletu klasy Punkt rozpoczy­namy od zadekla­rowania pięciu pól reprezen­tujących współ­rzędne punktu, jego kolor i wido­czność oraz refe­rencję do okna umożli­wiającą dostęp do koloru tła powierz­chni ryso­wania potrze­bnego przy ukry­waniu punktu, do obiektu Graphics pominię­tego w argumen­tach metod pokazy­wania, ukrywania i przesu­wania punktu, a być może także do innych parame­trów okna wymaga­nych w klasach pocho­dnych. Wszystkie pola dekla­rujemy przy użyciu modyfi­katora dostępu protected oznacza­jącego, że są one chronione – dostępne nie tylko w klasie, w której są zdefinio­wane, lecz także w klasach pocho­dnych. Chronioną i zarazem wirtualną, zadekla­rowaną z wykorzy­staniem słowa kluczo­wego virtual, będzie metoda ryso­wania punktu wywoły­wana bezpo­średnio w metodach pokazywania i ukrywania go, redefi­niowana (przesła­niana) w klasach pocho­dnych. Oczywiście publiczny powinien być konstru­ktor klasy inicjali­zujący pola obiektu i realizu­jący pewne operacje związane z jego tworzeniem. Publicznymi też będą wszystkie pozostałe metody klasy, a także właści­wości udostę­pniające i ustawia­jące wartości pól obiektu. Pełna defi­nicja klasy może mieć postać następu­jącą:

using System.Drawing;
using System.Windows.Forms;

namespace Punkty
{
    class Punkt
    {
        protected int x, y;         // Współrzędne punktu
        protected Color kolor;      // Kolor punktu
        protected bool wid;         // true - widoczny, false - nie
        protected Form okno;        // Formularz (okno)

        public Punkt(int xPoc, int yPoc, Color kol, Form form)
        {
            x = xPoc;
            y = yPoc;
            kolor = kol;
            wid = false;
            okno = form;
        }

        protected virtual void Rysuj(Graphics g)
        {
            using (Pen pen = new Pen(kolor))
                g.DrawLine(pen, x, y, x + 0.25F, y);
        }

        public void Pokaż(Graphics g)
        {
            wid = true;
            Rysuj(g);
        }

        public void Pokaż()
        {
            using (Graphics g = okno.CreateGraphics())
                Pokaż(g);
        }

        public void Ukryj(Graphics g)
        {
            Color kol = kolor;
            kolor = okno.BackColor;
            wid = false;
            Rysuj(g);
            kolor = kol;
        }

        public void Ukryj()
        {
            using (Graphics g = okno.CreateGraphics())
                Ukryj(g);
        }

        public void Przesuń_do(Graphics g, int xNowe, int yNowe)
        {
            bool jest_widoczny = wid;
            if (jest_widoczny) Ukryj(g);
            x = xNowe;
            y = yNowe;
            if (jest_widoczny) Pokaż(g);
        }

        public void Przesuń_do(int xNowe, int yNowe)
        {
            using (Graphics g = okno.CreateGraphics())
                Przesuń_do(g, xNowe, yNowe);
        }

        public void Przesuń_o(Graphics g, int x_Delta, int y_Delta)
        {
            Przesuń_do(g, x + x_Delta, y + y_Delta);
        }

        public void Przesuń_o(int x_Delta, int y_Delta)
        {
            using (Graphics g = okno.CreateGraphics())
                Przesuń_o(g, x_Delta, y_Delta);
        }

        public int X            // Współrzędna x (właściwość)
        {
            get
            {
                return x;
            }
            set
            {
                Przesuń_do(value, y);
            }
        }

        public int Y            // Współrzędna y (właściwość)
        {
            get
            {
                return y;
            }
            set
            {
                Przesuń_do(x, value);
            }
        }

        public Color Kolor      // Kolor punktu (właściwość)
        {
            get
            {
                return kolor;
            }
            set
            {
                bool jest_widoczny = wid;
                using (Graphics g = okno.CreateGraphics())
                {
                    if (jest_widoczny) Ukryj(g);
                    kolor = value;
                    if (jest_widoczny && (value != okno.BackColor)) Pokaż(g);
                }
            }
        }

        public bool Widoczny    // Widoczność punktu (własciwość)
        {
            get
            {
                return wid;
            }
            set
            {
                if (value) Pokaż(); else Ukryj();
            }
        }
    }
}

Podwójne, różniące się liczbą argumentów wersje metod Pokaż (pokazanie punktu – naryso­wanie go we właściwym mu kolorze), Ukryj (ukrycie punktu – naryso­wanie go w kolorze tła), Przesuń_do (przesu­nięcie punktu do wskaza­nego miejsca) i Przesuń_o (przesu­nięcie o dany wektor) reali­zują określone operacje zarówno przy sprecyzo­wanym, jak i pomi­niętym obiekcie klasy Graphics. Wersje uproszczone tworzą na własne potrzeby obiekt graficzny, toteż nie powinny być używane w trakcie wykony­wania metody obsługi zdarzenia Paint, która taki obiekt dostarcza. Ponadto gdy poza metodą obsługi zdarzenia Paint dwie lub większa liczba z powyższych metod klasy Punkt ma być wyko­nana pod rząd, bardziej korzystne ze względu na mniejszy nakład pracy procesora jest umieszczenie wywołań ich wersji ze wskazanym obiektem grafi­cznym w zakresie instru­kcji using, która na początku taki obiekt tworzy, a na końcu zwalnia zasoby z nim związane.

Właściwości umożliwiają dostęp do pól klasy, które zgodnie z zasadą hermety­zacji nie powinny być publiczne, aby uniknąć przypad­kowej zmiany ich wartości spoza metod klasy i klas pocho­dnych. Sposób pobie­rania wartości pola jest w defi­nicji właści­wości określany w bloku get (pobierz), zaś przypi­sywania polu nowej wartości w bloku set (ustaw), w którym wartość ta jest reprezen­towana przez słowo kluczowe value. W obydwu blokach oprócz prostego zwracania i przypi­sywania wartości pól dopuszczalne są inne operacje, np. kontrola popra­wności przypisy­wanej wartości lub konwersja przy jej pobie­raniu. Zmiany ustawień właści­wości klasy Punkt realizo­wane są przy użyciu prostszych wersji metod tej klasy. Poniższy fragment kodu pokazuje, w jaki sposób za pomocą właści­wości zmienić kolor wyświe­tlanego punktu z niebie­skiego na czerwony, a nastę­pnie przesunąć ten punkt w prawo o 50 pikseli:

Punkt pkt = new Punkt(100, 75, Color.Blue, this);
pkt.Pokaż();
...
pkt.Kolor = Color.Red;    // Niebieski -> Czerwony
pkt.X = pkt.X + 50;       // pkt.Przesuń_o(50, 0);

Uwaga. W analogicznej klasie w języku C++ istnieje destru­ktor ~Punkt, który jest wykony­wany automaty­cznie w trakcie niszczenia obiektu. Jego zadaniem jest ukrycie punktu, gdy jest widoczny. Ze względu na mechanizm odśmie­cania pamięci (ang. garbage collector, GC) definio­wanie takiego destru­ktora w C# nie ma sensu, gdyż nie wiadomo, kiedy GC usunie obiekt. Progra­mista powinien zadbać o to, by ukryć punkt, gdy reprezen­tujący go obiekt przestanie być potrzebny i powierz­chnia graficzna, na której jest naryso­wany, będzie dalej w programie używana.

Klasy pochodne w C# i program testowy

Klasę pochodną Okrag umożliwiającą tworzenie obiektów reprezen­tujących okręgi zdefi­niujemy także z poziomu aplikacji figuryTest. Nową klasę umieścimy w odrębnym pliku Okregi.cs, w którym zmodyfi­kowany jak poprzednio ciąg dyrektyw using rozsze­rzamy o dyrektywę włącza­jącą przestrzeń Punkty, a zapropo­nowaną przez Visual Studio nazwę przestrzeni zamie­niamy na Okregi i nazwę definio­wanej klasy na Okrag. Ponadto w nagłówku tej klasy dopisujemy po dwukropku nazwę Punkt, wyrażając w ten sposób, że wywodzi się ona z klasy Punkt.

Odziedziczone pola xy określają w klasie Okrag współ­rzędne środka okręgu, a nowe pole, któremu nadajemy nazwę r, jego promień. Oprócz pól klasa ta dziedziczy metody i właści­wości klasy Punkt, które tym razem służą do pokazy­wania, ukrywania i przesu­wania okręgu oraz udostę­pniania i zmiany współ­rzędnych jego środka, koloru i wido­czności. Rzecz jasna zdefi­niować należy konstru­ktor klasy Okrag, który będzie inicjali­zował wszystkie pola tworzo­nych według niej obiektów reprezen­tujących okręgi, a także zredefi­niować (przesłonić) metodę rysowania, gdyż okrąg wygląda inaczej niż punkt. Gotowa definicja klasy, w której dodatkowo uwzglę­dniono jedynie właści­wość udostę­pniającą promień okręgu, może wyglądać nastę­pująco:

using System.Drawing;
using System.Windows.Forms;
using Punkty;

namespace Okregi
{
    class Okrag : Punkt
    {
        protected int r;            // Promień okręgu

        public Okrag(int xSr, int ySr, int rOkr, Color kol, Form form)
              : base(xSr, ySr, kol, form)
        {
            r = (rOkr > 0) ? rOkr : 1;
        }

        protected override void Rysuj(Graphics g)
        {
            using (Pen pen = new Pen(kolor))
                g.DrawEllipse(pen, x - r, y - r, 2 * r, 2 * r);
        }

        public int R            // Promień okręgu (właściwość)
        {
            get { return r; }
        }
    }
}

Jak widać, bezpośrednio po nagłówku konstruktora klasy Okrag występuje dwukropek i konstru­kcja przypomi­nająca wywołanie metody o nazwie base. W istocie nie jest to nazwa metody, lecz słowo kluczowe, które udostę­pnia składnik klasy bazowej z poziomu klasy pochodnej, a w konstru­ktorze klasy pochodnej jest odwoła­niem do konstru­ktora klasy bazowej. Takie odwołanie jest konieczne, ponieważ podczas tworzenia obiektu klasy pochodnej wszystkie odziedzi­czone jego pola powinny zostać zainicjali­zowane przez konstru­ktor klasy bazowej, co wymaga przeka­zania mu stoso­wnych argumentów. Dopiero po tej operacji mogą być inicjali­zowane pozostałe pola klasy pochodnej. Z kolei słowo kluczowe override w nagłówku metody rysowania w klasie pochodnej określa, że przesłania ona wirtualną metodę rysowania zdefinio­waną w klasie bazowej.

Uwaga. Słowa kluczowe this i base są odwoła­niami do klasy. Różnica polega na tym, że this wskazuje na klasę bieżącą, a base na klasę bazową, jeśli taka istnieje. Gdyby np. metoda Rysuj klasy Okrag miała postać

protected override void Rysuj(Graphics g)
{
    base.Rysuj(g);
    using (Pen pen = new Pen(kolor))
        g.DrawEllipse(pen, x - r, y - r, 2 * r, 2 * r);
}

obrazem obiektu tej klasy byłby okrąg z pikselem pośrodku (metoda wywoły­wałaby swój odpowie­dnik w klasie bazowej). Gdyby natomiast w nowej metodzie zastąpić słowo kluczowe base słowem this lub pominąć go wraz z kropką, program zała­małby się z powodu nieskoń­czonej reku­rencji (metoda wywoły­wałaby samą siebie).

Zanim przejdziemy do rozbudowy aplikacji figuryTest, definiujemy jeszcze przez analogię klasę Gwiazdka wywodzącą się z klasy Okrag. Umieszczamy ją w pliku Gwiazdki.cs. Kod metody Rysuj tej klasy można łatwo otrzymać, modyfi­kując dowolny z dwóch algory­tmów kreślenia gwiazdki. Wykorzy­stanie drugiego z nich prowadzi do następu­jącej definicji nowej klasy:

using System;
using System.Drawing;
using System.Windows.Forms;
using Okregi;

namespace Gwiazdki
{
    class Gwiazdka : Okrag
    {
        private int n;          // Liczba ramion gwiazdki

        public Gwiazdka(int xSr, int ySr, int rGw, int nRam, Color kol, Form form)
                 : base(xSr, ySr, rGw, kol, form)
        {
            n = (nRam > 1) ? nRam : 2;
        }

        protected override void Rysuj(Graphics g)
        {
            double r2 = 0.38 * r, alpha2 = Math.PI / n, fi, ro;
            Point[] P = new Point[2 * n];
            for (int k = 0; k < P.Length; k++)
            {
                fi = k * alpha2 + Math.PI / 2;
                ro = (k % 2 == 0) ? r : r2;
                P[k].X = (int)(ro * Math.Cos(fi)) + x;
                P[k].Y = y - (int)(ro * Math.Sin(fi));
            }
            using (Pen pen = new Pen(kolor))
                g.DrawPolygon(pen, P);
        }

        public int N            // Liczba ramion (właściwość)
        {
            get { return n; }
        }
    }
}

Aplikacja figuryTest ma umożliwić użytkownikowi wybór dowolnej z trzech figur geometry­cznych – punktu, okręgu lub gwiazdki, a po wyświe­tleniu jej przesu­wanie za pomocą poleceń menu i klawia­tury. Po zaprojekto­waniu formu­larza (rys.) rozbu­dowę jego kodu zaczy­namy od dopi­sania trzech dyrektyw using włącza­jących do użycia w jego klasie przestrzenie Punkty, OkregiGwiazdki, a następnie zadeklaro­wania w niej pola p typu Punkt.

Zgodnie z tytułem formularza zakładamy, że na początku aplikacja ma utworzyć obiekt klasy Punkt i wyświetlić go. Przyjmiemy, że jego pozycją będzie środek obszaru robo­czego okna z uwzglę­dnieniem paska menu. Zadanie to można łatwo rozwiązać za pomocą metod obsługi zdarzeń LoadPaint formu­larza:

private void Form1_Load(object sender, EventArgs e)
{
    p = new Punkt(ClientSize.Width / 2, (ClientSize.Height + menuStrip1.Height) / 2,
                  Color.Red, this);
}

private void Form1_Paint(object sender, PaintEventArgs e)
{
    p.Pokaż(e.Graphics);
}

Gdy skompilujemy i uruchomimy aplikację w tej postaci, na środku okna pojawi się czerwony punkcik. Aby umożliwić wybór dowolnej z trzech figur, należy zdefi­niować metody obsługi trzech poleceń menu rozwi­janego Figura. W przy­padku pole­cenia Gwiazdka metoda ta może wyglądać następu­jąco:

private void gwiazdkaToolStripMenuItem_Click(object sender, EventArgs e)
{
    p = new Gwiazdka(p.X, p.Y, 70, 5, Color.Blue, this);
    Text = "Przesuwanie gwiazdki";
    Invalidate();
}

Metoda tworzy nowy obiekt klasy Gwiazdka o pozycji takiej samej jak poprzedni, zapamię­tując refe­rencję do niego w polu p, uaktualnia tytuł okna i wymusza przemalo­wanie jego obszaru roboczego (efekt jej wykonania jest pokazany na poniższym rysunku). Mechanizm odśmie­cania pamięci może już dotychcza­sowy obiekt zniszczyć, ponieważ refe­rencja do niego została utracona po utwo­rzeniu nowego obiektu. Nie wiadomo jednak, kiedy to nastąpi.

Przesuwanie wyświetlonej w oknie figury geometry­cznej ma być realizo­wane za pomocą poleceń menu i klawia­tury. Przyjmiemy, że program ma reagować na klawisze strzałek przesu­nięciem figury zgodnie z ich oznacze­niami, a na klawisz Home umieszczeniem figury na środku obszaru robo­czego okna. Zaczniemy od zdefinio­wania w klasie Form1 typu wylicze­niowego Klawisz, którego elementy okre­ślają symboli­cznie, jaki klawisz został naciśnięty:

enum Klawisz { Lewo, Prawo, Góra, Dół, Home, Inny };

Naszym zamierzeniem jest teraz zdefinio­wanie metody Ruch, która zależnie od określo­nego argu­mentu typu Klawisz przesuwa figurę reprezen­towaną przez obiekt wskazy­wany przez referen­cję p. Wyboru odpowie­dniego przesu­nięcia można dokonać za pomocą szeregu konstru­kcji if–else, lepiej jednak użyć instru­kcji switch:

private void Ruch(Klawisz klawisz)
{
    switch (klawisz)
    {
        case Klawisz.Lewo:
            p.Przesuń_o(-2, 0);
            break;
        case Klawisz.Prawo:
            p.Przesuń_o(2, 0);
            break;
        case Klawisz.Góra:
            p.Przesuń_o(0, -2);
            break;
        case Klawisz.Dół:
            p.Przesuń_o(0, 2);
            break;
        case Klawisz.Home:
            p.Przesuń_do(ClientSize.Width / 2, (ClientSize.Height + menuStrip1.Height) / 2);
            break;
    }
}

Instrukcja sprawdza, czy wartość określonego w nawia­sach wyra­żenia pasuje do jednej ze stałych wyszczegól­nionych po słowie kluczowym case (przypadek). Jeżeli tak, dalsze wykonanie programu rozpo­czyna się od miejsca oznaczo­nego tą stałą. Instru­kcja break jest ważna, gdyż powoduje natychmia­stowe wyjście z instru­kcji switch. Jej brak spowodo­wałby błąd kompilacji, ale można ją zastąpić instru­kcją return (wyjście z metody) lub goto (przejście do innego przypadku). Gdy żaden z wymie­nionych warunków nie zostanie spełniony, instru­kcja switch zostaje zakończona. Można zarea­gować w takiej sytuacji, wprowa­dzając przypadek oznaczony słowem kluczowym default.

Moment naciśnięcia klawisza można wychwycić jako zdarzenie KeyDown formu­larza. Metoda obsługi tego zdarzenia udostępnia argument klasy KeyEventArgs, której właści­wość KeyCode określa kod naciśnię­tego klawisza jako wartość typu wylicze­niowego Keys. Zatem reakcję programu na naciśnięcie klawisza możemy zaprogra­mować następu­jąco:

private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    Ruch((e.KeyCode == Keys.Left) ? Klawisz.Lewo :
         (e.KeyCode == Keys.Right) ? Klawisz.Prawo :
         (e.KeyCode == Keys.Up) ? Klawisz.Góra :
         (e.KeyCode == Keys.Down) ? Klawisz.Dół :
         (e.KeyCode == Keys.Home) ? Klawisz.Home : Klawisz.Inny);
}

Przesuwanie figury za pomocą poleceń menu sprowadza się również do wywołania metody Ruch. Ostate­czna wersja programu figuryTest, w której zamiast metod przesu­wania obiektu reprezen­tującego figurę (metoda Ruch) użyto jego właści­wości, może wyglądać następu­jąco:

using System;
using System.Drawing;
using System.Windows.Forms;
using Punkty;
using Okregi;
using Gwiazdki;

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

        private Punkt p;

        enum Klawisz { Lewo, Prawo, Góra, Dół, Home, Inny };

        private void Form1_Load(object sender, EventArgs e)
        {
            p = new Punkt(ClientSize.Width / 2,
                          (ClientSize.Height + menuStrip1.Height) / 2,
                          Color.Red, this);
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            p.Pokaż(e.Graphics);
        }

        private void punktToolStripMenuItem_Click(object sender, EventArgs e)
        {
            p = new Punkt(p.X, p.Y, Color.Red, this);
            Text = "Przesuwanie punktu";
            Invalidate();
        }

        private void okrągToolStripMenuItem_Click(object sender, EventArgs e)
        {
            p = new Okrag(p.X, p.Y, 50, Color.Green, this);
            Text = "Przesuwanie okręgu";
            Invalidate();
        }

        private void gwiazdkaToolStripMenuItem_Click(object sender, EventArgs e)
        {
            p = new Gwiazdka(p.X, p.Y, 70, 5, Color.Blue, this);
            Text = "Przesuwanie gwiazdki";
            Invalidate();
        }

        private void Ruch(Klawisz klawisz)
        {
            switch (klawisz)
            {
                case Klawisz.Lewo:
                    p.X = p.X - 2;          // p.Przesuń_o(-2, 0);
                    break;
                case Klawisz.Prawo:
                    p.X = p.X + 2;          // p.Przesuń_o(2, 0);
                    break;
                case Klawisz.Góra:
                    p.Y = p.Y - 2;          // p.Przesuń_o(0, -2);
                    break;
                case Klawisz.Dół:
                    p.Y = p.Y + 2;          // p.Przesuń_o(0, 2);
                    break;
                case Klawisz.Home:
                    p.Przesuń_do(ClientSize.Width / 2,
                                 (ClientSize.Height + menuStrip1.Height) / 2);
                    break;
            }
        }

        private void lewoToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Ruch(Klawisz.Lewo);
        }

        private void prawoToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Ruch(Klawisz.Prawo);
        }

        private void góraToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Ruch(Klawisz.Góra);
        }

        private void dółToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Ruch(Klawisz.Dół);
        }

        private void centrumToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Ruch(Klawisz.Home);
        }

        private void koniecToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            Ruch((e.KeyCode == Keys.Left) ? Klawisz.Lewo :
                 (e.KeyCode == Keys.Right) ? Klawisz.Prawo :
                 (e.KeyCode == Keys.Up) ? Klawisz.Góra :
                 (e.KeyCode == Keys.Down) ? Klawisz.Dół :
                 (e.KeyCode == Keys.Home) ? Klawisz.Home : Klawisz.Inny);
        }
    }
}

Powróćmy na moment do metody Ruch działającej formalnie na obiektach klasy Punkt poprzez refe­rencję p. Mechanizm dziedzi­czenia umożliwia, aby refe­rencja ta odwoły­wała się również do obiektu klasy pocho­dnej wywodzącej się bezpo­średnio lub pośrednio z klasy Punkt (tu OkragGwiazdka). Nic nie stoi na przeszko­dzie, aby utworzyć nową klasę wywodzącą się z klasy Punkt lub z jej klasy pochodnej reprezen­tującą inną figurę geometry­czną i zlecić metodzie Ruch jej przesu­wanie. Utworzenie klasy pochodnej nie tylko nie zmienia zachowań przewi­dzianych w klasie bazowej, ale i nie wymaga żadnych modyfi­kacji jej kodu. Dzięki temu raz napisany i spraw­dzony kod nadaje się do dalszego wykorzy­stania i rozsze­rzania, co jest wielką zaletą progra­mowania obiekto­wego.

Implementacja kropli wody fontanny w C#

Zajmiemy się obecnie zdefiniowaniem klasy Kropla, która posłuży do tworzenia obiektów imitu­jących krople wody wyrzucane w górę z dyszy fontanny w nocnej wielo­barwnej ilumi­nacji. Narzuca­jącym się rozwią­zaniem jest wywie­dzenie nowej klasy z klasy Okrag. Odziedzi­czone pola x i y będą określać pozycję kropli na ekranie, r jej wielkość, a nowe pola vxvy składowe wektora prędkości. Potrzebna jest też nowa metoda przesu­wania kropli i zmiany jej prędkości. Aby wywołać wrażenie tryska­jącego z fontanny strumienia wody, wprowa­dzimy element chaosu przy ustalaniu prędkości początkowej kropel, posługując się genera­torem liczb losowych.

Naturalnym opisem ruchu wyrzuconej kropli wody jest rzut ukośny w polu grawita­cyjnym z prędkością początkową o kierunku pionowym lub niemal pionowym w górę (rys. powyżej, α ≈ 90o). Rzut ten można po pominięciu oporu powietrza rozłożyć na dwa ruchy – ruch jedno­stajny prostoli­niowy poziomy ze stałą prędko­ścią vx w prawo lub w lewo i ruch jedno­stajny opóźniony prostoli­niowy ku górze z prędko­ścią począ­tkową vy. Gdy rzut nastąpi z punktu x = y = 0, to po czasie t kropla znajdzie się w punkcie o współ­rzędnych

gdzie g jest przyśpie­szeniem ziemskim równym 9,81 m·s-2. Równania te przedsta­wiają parabolę skiero­waną ku dołowi (w skrajnym przypadku odcinek). Nietrudno sprawdzić, że maksy­malna wysokość, na jaką wzniesie się kropla zanim zacznie spadać, wynosi

Z równości tej wynika, że jeśli kropla będzie wyrzucana w górę z dolnej krawędzi obszaru roboczego okna z prędkością początkową wzdłuż osi y spełnia­jącą warunek

w którym ymax oznacza wysokość tego obszaru, to nie przekroczy jego górnej krawędzi. Wydaje się oczywiste, aby pozycją początkową kropli był środek dolnej krawędzi. Zważywszy na pożądaną losowość, pozycja ta będzie nieco zaburzana w poziomie (do 10 pikseli w lewo lub w prawo).

W celu zobrazo­wania ruchu kropli posłużymy się prostym schematem oblicze­niowym wynika­jącym z przedsta­wionych na początku wzorów określa­jących zmienia­jące się w czasie współ­rzędne. Jeśli miano­wicie dt jest niewielkim odstępem czasowym, to po jego upływie przy dowolnie ustalonej pozycji początkowej nową pozycję kropli i składową pionową jej prędkości można wyznaczyć w sposób przybli­żony nastę­pująco:

Po tych wstępnych przygotowaniach związanych z ustale­niem wygodnej reprezen­tacji ruchu kropli wody możemy przejść do sformuło­wania zapowie­dzianej klasy Kropla. Podobnie jak przy poprze­dnich klasach, nową klasę defi­niujemy w pliku Krople.cs aplikacji okien­kowej Fontanna, której projekt zawiera pliki Punkty.csOkregi.cs. Jej formularz ma proste menu z zabloko­wanym poleceniem Stop i czarne tło (rys.). Aby uprościć kod apli­kacji, blokujemy zmianę rozmiaru i maksyma­lizację okna (właści­wość FormBorder­Style usta­wiamy na Fixed­SingleMaxi­mizeBox na False).

Klasa Kropla dziedziczy pola i metody klasy Okrag, ma także nowe pola – stałą określa­jącą przyśpie­szenie ziemskie i zmienne reprezen­tujące składowe prędkości kropli, ma również nowe metody – nadanie wartości początko­wych składowym prędkości i przesu­wanie kropli o odcinek przebyty przez nią w ostatnim interwale czasowym. Generator liczb losowych klasy Random ma być używany zarówno przez każdy obiekt klasy Kropla, jak i okno apli­kacji. Utworzymy go w klasie Form1, a wtedy obiekty reprezen­tujące krople będą miały do niego dostęp poprzez pole okno. Definicja klasy Kropla może mieć postać następu­jącą:

using System;
using System.Drawing;
using System.Windows.Forms;
using Okregi;

namespace Fontanna
{
    class Kropla : Okrag
    {
        private const double G = 9.81;  // Przyśpieszenie ziemskie
        private double vx, vy;          // Składowe prędkości

        public Kropla(int x, int y, int r, Color kol, Form form)
               : base(x, y, r, kol, form)
        {
            Prędkość_Pocz();
        }

        public void Przesuń(Graphics g, double dt)
        {
            if (okno.ClientSize.Height <= okno.MainMenuStrip.Height) return;
            if (y <= okno.ClientSize.Height)
            {
                Przesuń_o(g, (int)(vx * dt), -(int)(vy * dt));
                vy -= G * dt;
            }
            else
            {
                Przesuń_do(g,
                           okno.ClientSize.Width / 2 + (okno as Form1).gen.Next(-10, 11),
                           okno.ClientSize.Height);
                Prędkość_Pocz();
            }
        }

        private void Prędkość_Pocz()
        {
            double vyMax = Math.Sqrt(2 * (okno.ClientSize.Height -
                                          okno.MainMenuStrip.Height) * G);
            vx = ((okno as Form1).gen.NextDouble() * 0.09 + 0.03) * vyMax;
            if ((okno as Form1).gen.Next(2) > 0) vx = -vx;
            vy = ((okno as Form1).gen.NextDouble() * 0.24 + 0.75) * vyMax;
        }
    }
}

Konstruktor Kropla po wywo­łaniu konstru­ktora klasy bazowej inicjali­zuje pola vxvy tworzo­nego obiektu, korzy­stając z prywatnej metody Prędkość_Pocz. Metoda ta wyznacza najpierw wartość maksy­malną prędkości pionowej, przy której kropla nie przekroczy górnej krawędzi obszaru robo­czego okna z uwzglę­dnieniem paska menu, a potem obie składowe wektora prędkości początkowej jako liczby losowe z zakresu 3÷12%75÷99% obliczonej uprzednio wartości maksy­malnej (dla składowej poziomej losowany jest również jej znak). Zauważmy, że w metodzie tej odwołania do utworzo­nego w oknie klasy Form1 obiektu gen klasy Random generu­jącego liczby losowe mają postać

(okno as Form1).gen

Operator as służy tu do jawnego rzuto­wania refe­rencji okno klasy Form na refe­rencję klasy Form1, która zawiera pole gen. Takie rzuto­wanie występuje również w metodzie Przesuń, której zadaniem jest przesu­nięcie kropli do nowej pozycji i uaktual­nienie jej prędkości pionowej zgodnie z przedsta­wionym powyżej schematem oblicze­niowym wyraża­jącym zmianę parametrów rzutu ukośnego w odstępie czasu, jaki upłynął od osta­tniego do bieżącego momentu czasowego (zmiana znaku trzeciego argu­mentu w wywo­łaniu metody Przesuń_o jest konse­kwencją odwro­tnego kierunku osi y na ekranie). Obli­czenia te mają sens tylko wtedy, gdy kropla jest widoczna na ekranie. Gdy opadła poniżej dolnej krawędzi obszaru roboczego okna, jest przesu­wana do środka tej krawędzi (z niewielkim zabu­rzeniem w lewo lub w prawo), nadawana jest jej też prędkość ku górze za pomocą metody Prędkość_Pocz. W ten sposób krople, które wydostają się poza obszar roboczy okna, są z powrotem kierowane do stru­mienia tryska­jącego z fontanny.

Wątki w C#

Ruch obiektów klasy Kropla wyobraża­jących krople wody stru­mienia fontanny jest operacją czaso­chłonną, dlatego powinien być wykony­wany w oddzielnym wątku (ang. thread), nieza­leżnie od głównego wątku aplikacji. W przeci­wnym razie aplikacja zajęta animacją nie reago­wałaby na działania użytko­wnika, sprawiając wrażenie zawie­szenia się (zabloko­wania) grafi­cznego inter­fejsu użytko­wnika. Tematyka wątków jest bardzo obszerna. W apli­kacji Fontanna skorzy­stamy z klasy wątku Thread. W tym celu w klasie formu­larza Form1 dekla­rujemy użycie przestrzeni System.­Threading, definiujemy też następu­jące pola:

private const double DT = 0.25;     // Odstęp czasowy
public Random gen;                  // Generator liczb losowych
private Kropla[] kropla;            // Krople wody
private Thread wątek;               // Wątek ruchu kropel

Nie precyzując na razie, w jaki sposób utworzyć tablicę dynamiczną obiektów klasy Kropla reprezen­tujących krople wody, możemy ich nieza­leżne od siebie funkcjo­nowanie zaprogra­mować w postaci metody:

private void Animacja()
{
    using (Graphics g = CreateGraphics())
        for (int k = 0; ; k = (++k % kropla.Length))
            kropla[k].Przesuń(g, DT);
}

Jak widać, pętla for jest nieskończona, nie ma bowiem wyra­żenia warun­kowego określa­jącego jej zakoń­czenie. Pętla przebiega wszystkie elementy tablicy kropla, przesu­wając każdą wyimagi­nowaną przez nie kroplę wody do nowego miejsca. Po uwzglę­dnieniu osta­tniego elementu tablicy przechodzi na jej początek, rozpoczy­nając ponowny przebieg po wszystkich elementach. Jest więc oczywiste, że ta metoda zablo­kuje każdy program jednową­tkowy.

Argumentem jednego z konstruktorów klasy Thread jest bezargu­mentowa metoda, która ma być w utwo­rzonym wątku wykony­wana, a ważną właści­wością wątku jest IsBackground. Gdy ustawimy ją na true, wątek zostanie zamknięty, gdy użytko­wnik zamknie aplikację. Gdyby pozo­stawić wartość domyślną false, po zamknięciu aplikacji wątek nadal by działał. Utworzenie wątku nie oznacza jego urucho­mienia, służy do tego jego metoda Start. Nowy wątek, w którym będzie reali­zowana powyższa animacja, tworzymy i urucha­miamy w metodzie obsługi polecenia Start menu:

private void startToolStripMenuItem_Click(object sender, EventArgs e)
{
    startToolStripMenuItem.Enabled = false;
    stopToolStripMenuItem.Enabled = true;
    wątek = new Thread(Animacja);
    wątek.IsBackground = true;
    wątek.Start();
}

Klasa Thread zawiera szereg innych właści­wości i metod pozwala­jących na zarzą­dzanie wątkiem, m.in. metodę Abort (przer­wanie działania wątku i jego zakoń­czenie), Suspend (wstrzy­manie wątku) i Resume (wzno­wienie wątku po jego wstrzy­maniu). W metodzie obsługi polecenia Stop menu użyjemy metody Abort:

private void stopToolStripMenuItem_Click(object sender, EventArgs e)
{
    startToolStripMenuItem.Enabled = true;
    stopToolStripMenuItem.Enabled = false;
    wątek.Abort();
}

Wielu informatyków odradza użycie metody Abort. Ten pogląd nie wydaje się w pełni słuszny, przynaj­mniej w rozpatry­wanym programie. Inne rozwią­zanie stero­wania animacją polega na chwilowym wstrzy­mywaniu i wznawianiu wątku za pomocą metod SuspendResume, są one jednak oznaczone jako przesta­rzałe, chociaż są wygodne i nadal można z nich korzystać.

Program "Fontanna"

Kod źródłowy aplikacji okienkowej Fontanna imitu­jącej fontannę jest przedsta­wiony na poniższym listingu. Na początku w metodzie obsługi zdarzenia Load formu­larza ustalany jest rozmiar obszaru robo­czego okna, tworzony jest obiekt genera­tora liczb losowych i dyna­miczna tablica pustych referencji klasy Kropla, która jest wypeł­niana referen­cjami do genero­wanych obiektów tej klasy reprezen­tujących różnoko­lorowe krople wody ustawione pośrodku dolnego brzegu obszaru robo­czego okna. Animację rozpo­czyna pole­cenie Start menu, a zatrzy­muje pole­cenie Stop lub zamknięcie okna aplikacji kończące jej wykonanie.

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;
using System.Threading;

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

        private const double DT = 0.25;     // Odstęp czasowy
        public Random gen;                  // Generator liczb losowych
        private Kropla[] kropla;            // Krople wody
        private Thread wątek;               // Wątek ruchu kropel

        private void Form1_Load(object sender, EventArgs e)
        {
            ClientSize = new Size(800, 600);
            gen = new Random();
            kropla = new Kropla[gen.Next(250, 501)];
            for (int k = 0; k < kropla.Length; k++)
                kropla[k] = new Kropla(
                            ClientSize.Width / 2, ClientSize.Height, // pozycja
                            gen.Next(2, 6),                          // rozmiar
                            Color.FromArgb(gen.Next(50, 256),        // czerwony
                                           gen.Next(50, 256),        // niebieski
                                           gen.Next(50, 256)),       // zielony
                            this);                                   // to okno
        }

        private void Animacja()
        {
            using (Graphics g = CreateGraphics())
                for (int k = 0; ; k = (++k % kropla.Length))
                    kropla[k].Przesuń(g, DT);
        }

        private void startToolStripMenuItem_Click(object sender, EventArgs e)
        {
            startToolStripMenuItem.Enabled = false;
            stopToolStripMenuItem.Enabled = true;
            wątek = new Thread(Animacja);
            wątek.IsBackground = true;
            wątek.Start();
        }

        private void stopToolStripMenuItem_Click(object sender, EventArgs e)
        {
            startToolStripMenuItem.Enabled = true;
            stopToolStripMenuItem.Enabled = false;
            wątek.Abort();
        }

        private void koniecToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            foreach (Kropla k in kropla)
                k.Pokaż(e.Graphics);
        }
    }
}

Poniższy rysunek przedstawia obraz fontanny uchwycony po ok. trzech minutach działania programu (procesor i5-430M, grafika proce­sora). W porów­naniu z analo­gicznym programem w C++ wydajność programu w C# jest wyraźnie niższa, nawet przy trzykro­tnie mniejszej statysty­cznie liczbie obiektów reprezen­tujących krople wody.


Opracowanie przykładu: listopad 2019