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

Przykład C#

Epicykloida i hipocykloida Formularz główny i parametry krzywych Kreślenie krzywych z użyciem delegata Formularz podrzędny i jego obsługa Program w C# Poprzedni przykład Następny przykład Program w C++ Kontakt

Epicykloida i hipocykloida

Zadaniem programu jest rysowanie epicy­kloidy i hipocy­kloidy – krzywych mających zastoso­wanie w różnych zagadnie­niach techni­cznych. Obie krzywe opisuje koniec P ramienia przytwier­dzonego sztywno do okręgu (koła) toczącego się stycznie bez poślizgu po stałym okręgu. Gdy okręgi stykają się zewnę­trznie (rys. poniżej z lewej), krzywa jest epicykloidą, a gdy wewnę­trznie (rys. poniżej z prawej), hipocykloidą. W drugim przypadku promień rucho­mego okręgu powinien być mniejszy od promienia stałego okręgu.

Na rysunkach oznaczono przez O i S środki okręgów stałego i ruchomego, przez R i r ich promienie, zaś przez d długość ramienia SP przytwier­dzonego do toczącego się okręgu. Przyjęto ponadto, że środek O stałego okręgu jest jednocze­śnie środkiem układu współ­rzędnych i na początku ruchu środek S toczącego się okręgu leży na dodatniej półosi x, a punkt P po jego lewej stronie, gdy okręgi stykają się zewnętrznie, albo po prawej, gdy stykają się wewnętrznie. Przy tych założe­niach uzyskuje się równania epicykloidy

oraz równania hipocykloidy

Kształt krzywych zależy od stosunku R do r. Jeżeli R jest podzielne przez r, krzywa jest zamknięta, a jeżeli nie, zamyka się po pewnej liczbie obiegów ruchomego okręgu, gdy R/r jest liczbą wymierną, a nie zamyka się nigdy, gdy jest liczbą niewy­mierną. Rozróżnia się również epicy­kloidę i hipocy­kloidę zwyczajną, gdy punkt P leży na toczącym się okręgu, czyli gdy d=r, skróconą, gdy d<r, oraz wydłużoną, gdy d>r. Dla d<0 ustawienie początkowe punktu P jest przeciwne względem środka S, a dla d=0 krzywa redukuje się do okręgu.

Formularz główny i parametry krzywych

Po otwarciu nowego projektu zmieniamy właści­wość BackColor formu­larza na Window (białe tło obszaru robo­czego) i wsta­wiamy do niego kompo­nenty MenuStrip (menu główne) i StatusStrip (pasek statusu). Następnie tworzymy dwie pozycje menu, których właści­wości Text zmieniamy na &Parametry&Koniec. Pierwsza posłuży do zmiany parame­trów krzywej, zaś druga do zakoń­czenia aplikacji. Tworzymy też trzy etykiety StatusLabel na pasku statusu. Ich właści­wości BackColor ustawiamy na Control (jasno­szare tło), Border­Sides na All (wszystkie strony obramo­wania) i Border­Style na Sunken­Outer (zato­pione względem zewnętrza). Etykiety te będą używane do wyświe­tlania parametrów R, r i d krzywej. Zaprojekto­wany formularz jest pokazany na poniższym rysunku.

Rozbudowę wygenero­wanego przez środo­wisko Visual Studio szablo­nowego kodu źródło­wego formu­larza klasy Form1 rozpoczy­namy od zadekla­rowania szeregu pól prywa­tnych dla parame­trów krzywej i kilku wielkości przyda­tnych do jej zobrazo­wania:

private bool epic = false;  // Epicykloida (true), hipocykloida (false)
private double R = 5;       // Promień stałego okręgu
private double r = 1;       // Promień ruchomego okręgu
private double d = 2;       // Długość ramienia

private float p, q;         // Środek obszaru roboczego
private double skala;       // Czynnik skalujący
private int n;              // Liczba odcinków łamanej

Jak widać, parametry krzywej są zainicjali­zowane wartościami określa­jącymi hipocy­kloidę, by po urucho­mieniu aplikacji pojawił się w oknie przykła­dowy rysunek krzywej. Naturalnie użytko­wnik będzie mógł je zmieniać, wybie­rając w menu polecenie Parametry. Warto zwrócić uwagę na typ float pól p i q. Współ­rzędne punktów mogą być w metodach klasy Graphics obsługu­jącej powierz­chnię ryso­wania nie tylko liczbami całkowi­tymi typu int, lecz także zmiennopozy­cyjnymi typu float. Drugi wariant jest lepszy, gdy obliczane wartości nie są całkowite, ponieważ są one wtedy automaty­cznie zaokrą­glane do wartości całkowitych reprezen­tujących współ­rzędne ekranowe, a nie obcinane.

Rzeczy­wista krzywa mieści się w okręgu o środku (0,0) i pro­mieniu R+r+|d|, gdy jest epicy­kloidą, albo R–r+|d|, gdy jest hipocy­kloidą. Zobrazo­wanie jej na ekranie wymaga stosownego przeskalo­wania, przesu­nięcia początku układu współrzę­dnych do środka obszaru robo­czego okna i odwrócenia osi y. Przy wyborze czynnika skalują­cego wypada ze względów estety­cznych uwzglę­dnić niewielki margines wokół krzywej, przyjmując na przykład, że ma się ona mieścić w okręgu o pro­mieniu równym 9/10 mniejszej odle­głości środka obszaru robo­czego od jego lewego (lub prawego) brzegu i menu (lub paska statusu). Oczywiście zamiast krzywej rysowana będzie łamana o pewnej liczbie wierzchołków leżących na krzywej. Liczba ta powinna być nie za wielka, ale jedno­cześnie wystarcza­jąca, by rysunek wiernie oddawał kształt krzywej. Wydaje się, że przyjęcie iloczynu długości promienia okręgu, w którym krzywa się mieści, i liczby R/r (liczby obiegów toczącego się okręgu po stałym okręgu) jest satysfakcjo­nujące. Rozwa­żania te prowadzą do następu­jącej metody wyzna­czania parametrów rysunku:

private void Parametry_Rysunku()
{
    p = ClientSize.Width / 2;
    q = ClientSize.Height / 2;
    n = (int)(0.9 * Math.Min(p, q - menuStrip1.Height));
    skala = n / ((epic ? (R + r) : (R - r)) + Math.Abs(d));
    if (R > r)
        n = (int)(n * (R / r));
}

Rzecz jasna, parametry rysunku powinny być znane przed ryso­waniem krzywej. Chociaż metoda Parametry­_Rysunku nie jest czaso­chłonna, wywołanie jej wewnątrz metody obsługi zdarzenia Paint formu­larza nie byłoby eleganckie, gdyż może być ono wyzwolone, kiedy parametry te pozostają niezmie­nione, np. gdy okno jest odkry­wane po uprzednim zakryciu przez inne okno. Istnieją trzy sytuacje, w których wyzna­czanie parametrów rysunku jest wymagane:

Pozostańmy na razie przy metodach obsługi zdarzeń LoadResize formu­larza. Pierwsza powinna oprócz wywołania metody Parametry­_Rysunku umieścić nazwę krzywej na pasku tytułowym okna i wartości jej parametrów na etykietach paska statusu, zaś druga po wyzna­czeniu parametrów rysunku wymusić przery­sowanie obszaru robo­czego okna. Ich kod źródłowy może mieć następującą postać:

private void Form1_Load(object sender, EventArgs e)
{
    Text = epic ? "Epicykloida" : "Hipocykloida";
    toolStripStatusLabel1.Text = "R = " + R;
    toolStripStatusLabel2.Text = "r = " + r;
    toolStripStatusLabel3.Text = "d = " + d;
    Parametry_Rysunku();
}

private void Form1_Resize(object sender, EventArgs e)
{
    Parametry_Rysunku();
    Invalidate();
}

Kreślenie krzywych z użyciem delegata

Równania parametryczne epicykloidy i hipocy­kloidy podane na początku niniej­szej witryny określają zależność współrzę­dnych x, y punktów krzywej od parametru φ przebiega­jącego przedział od 0 do π. W języku C# można je sformu­łować następująco:

private void Epicykloida(double fi, out double x, out double y)
{
    x = (R + r) * Math.Cos(fi) - d * Math.Cos((R + r) * fi / r);
    y = (R + r) * Math.Sin(fi) - d * Math.Sin((R + r) * fi / r);
}

private void Hipocykloida(double fi, out double x, out double y)
{
    x = (R - r) * Math.Cos(fi) + d * Math.Cos((R - r) * fi / r);
    y = (R - r) * Math.Sin(fi) - d * Math.Sin((R - r) * fi / r);
}

Obie metody mają taką samą sygnaturę (taką samą liczbę argu­mentów, ich typy i typ zwracanej wartości). Obie również obliczają współ­rzędne punktu i przypisują je parame­trom x, y przekazy­wanym przez referencję (słowo kluczowe out), dzięki czemu obliczane wartości są bezpośre­dnio umieszczane w argumen­tach wywołań tych metod. Decyzja o tym, która krzywa ma być ryso­wana, a tym samym, która z dwóch metod ma być używana do obli­czania współ­rzędnych wierz­chołków łamanej rysowanej zamiast krzywej, jest podejmo­wana przez użytko­wnika w trakcie wykony­wania apli­kacji. Algorytm wyzna­czania wierz­chołków łamanej można przedstawić w postaci:

for (int k = 0; k <= n; k++)
{
    double x, y;   // Współrzędne wierzchołka
    if (epic)
        Epicykloida(2 * k * Math.PI / n, out x, out y);
    else
        Hipocykloida(2 * k * Math.PI / n, out x, out y);
    ...           // Działania dotyczące wierzchołka (x,y)
}

Jak widać, w każdym kroku iteracyjnym wybiera się za pomocą instrukcji warun­kowej odpowie­dnią, za każdym razem tę samą metodę. To samo zadanie można zreali­zować, dokonując tylko jednego wyboru metody przed iteracją. Zmiana ta wymaga uprze­dniego zadeklaro­wania tzw. delegata – typu referen­cyjnego, którego wartościami są metody o odpowia­dającej mu sygna­turze. W rozpatry­wanym przypadku dekla­racja nowego typu może wyglądać następująco:

private delegate void Cykloida(double fi, out double x, out double y);

Teraz można zadeklarować zmienną typu Cykloida i przypisać jej odpowie­dnią metodę. Od tego momentu użycie tej zmiennej jest równoważne użyciu przypi­sanej jej metody, toteż ulepszony algorytm wyzna­czania wierz­chołków łamanej można zapisać w postaci:

Cykloida cykloida;
if (epic)
    cykloida = Epicykloida;
else
    cykloida = Hipocykloida;
for (int k = 0; k <= n; k++)
{
    double x, y;
    cykloida(2 * k * Math.PI / n, out x, out y);
    ...           // Działania dotyczące wierzchołka (x,y)
}

Fragment kodu występujący przed pętlą for można skrócić, zastępując instrukcję warunkową wyrażeniem warunkowym inicjali­zującym zmienną cykloida. Wymagane jest wówczas, w odróżnieniu do przypisań występu­jących w instrukcji if–else, użycie jawnej konwersji (rzutowania) nazw metod na typ Cykloida, gdyż w przeci­wnym razie wykryty zostałby błąd kompilacji. To nieco dziwaczne zachowanie kompila­tora Visual C# 2017 nie umniejsza faktu, że powyższy algorytm można przekształcić do bardziej zwartej postaci:

Cykloida cykloida = epic ? (Cykloida)Epicykloida : (Cykloida)Hipocykloida;
for (int k = 0; k <= n; k++)
{
    double x, y;
    cykloida(2 * k * Math.PI / n, out x, out y);
    ...           // Działania dotyczące wierzchołka (x,y)
}

Po tych rozważaniach możemy przejść do sformu­łowania metody obsługi zdarzenia Paint formu­larza, której zadaniem jest naryso­wanie określonej w parame­trach krzywej, a w istocie łamanej o odpo­wiednio dużej liczbie wierz­chołków leżących na tej krzywej. Pomni kompli­kacji, jakie pojawiły się w trakcie programo­wania operacji tworzenia wykresu ruchu harmoni­cznego tłumionego (zbyt długi czas ryso­wania i nieocze­kiwany błąd przy minima­lizacji okna), ze względu na duże podobień­stwo obu zaga­dnień przyjmiemy przez analogię dwa ustalenia:

Uzyskujemy w ten sposób następującą postać metody kreślenia krzywej:

private void Form1_Paint(object sender, PaintEventArgs e)   // Wersja uproszczona
{
    if (n > 1)
    {
        Cykloida cykloida = epic ? (Cykloida)Epicykloida : (Cykloida)Hipocykloida;
        PointF[] P = new PointF[n + 1];
        for (int k = 0; k <= n; k++)
        {
            double x, y;
            cykloida(2 * k * Math.PI / n, out x, out y);
            P[k].X = (float)(skala * x) + p;
            P[k].Y = q - (float)(skala * y);
        }
        Pen pen = new Pen(Color.Red, 2);
        pen.LineJoin = System.Drawing.Drawing2D.LineJoin.Round;
        e.Graphics.DrawLines(pen, P);
        pen.Dispose();
    }
}

Użycie tablicy elementów typu PointF zamiast typu Point pozwala na bardziej korzystną od całko­witej reprezen­tację zmiennopo­zycyjną współrzę­dnych wierz­chołków łamanej. Co prawda metoda DrawLines i tak je przekształaca na typ całkowity, lecz je zaokrągla, a nie obcina. Z kolei ustawienie właści­wości LineJoin pióra na Round zapewnia gładkie łączenie odcinków łamanej.

Przytoczoną metodę rysowania krzywej warto nieco rozbu­dować, by ukazywała mniej intensy­wnym kolorem obydwa okręgi i ramię przytwier­dzone do rucho­mego okręgu. Rysowanie okręgu umożliwia metoda DrawEllipse, która rysuje elipsę określoną przez prostokąt o danych współrzę­dnych lewego górnego rogu, szerokości i wysokości. Stosowny kod uzupełnia­jący może wyglądać następująco:

float R = (float)(skala * this.R);
e.Graphics.DrawEllipse(Pens.SkyBlue, p - R, q - R, 2 * R, 2 * R);
float r = (float)(skala * this.r);
float s = p + (epic ? R + r : R - r);
e.Graphics.DrawEllipse(Pens.SkyBlue, s - r, q - r, 2 * r, 2 * r);
float d = (float)(skala * this.d);
e.Graphics.DrawLine(Pens.SkyBlue, s, q, epic ? s - d : s + d, q);

Zmienne lokalne R, rd inicjalizowane przeskalo­wanymi wartościami promieni okręgów i długości ramienia mają identy­czne nazwy jak pola reprezen­tujące parametry ich odpowie­dników rzeczy­wistych, toteż należało za pomocą słowa kluczowego this wskazać, które nazwy dotyczą pól. Oczywiście można by tym zmiennym nadać inne nazwy, a wtedy użycie słowa this nie byłoby konieczne. Urucho­mienie nieukończo­nego jeszcze programu ukazuje efekt działania ulepszonej metody Form1_Paint dla ustalo­nych na początku parametrów krzywej (rys.).

Formularz podrzędny i jego obsługa

Naszym zadaniem jest teraz zaprogra­mowanie funkcjonal­ności menu. Rozpoczy­namy od utworzenia nowego formu­larza klasy Form2, w którym będzie można modyfi­kować parametry rysowanej krzywej. Utworzo­nemu formula­rzowi nadajemy tytuł Parametry, po czym blokujemy możli­wość zmiany jego rozmiarów, usta­wiając właści­wość FormBorder­Style na Fixed­SingleMaxi­mizeBox na False. Następnie wstawiamy do niego dwa przyciski radiowe (typ RadioButton) z napisami EpicykloidaHipocykloida, trzy kontrolki edycyjne (typ TextBox) i trzy opisu­jące je etykiety (typ Label) z tekstem R, r i d. Wstawiamy jeszcze dwa przyciski (typ Button) z napisami OKAnuluj. Właściwość DialogResult drugiego z nich ustawiamy na Cancel, by kliknięcie na nim powodo­wało jedynie zamknięcie okna bez uwzglę­dnienia wprowa­dzonych zmian parametrów. Na koniec korygu­jemy rozmiary formularza i kompo­nentów oraz ich rozmieszczenie. Wynik końcowy tych czyn­ności przedstawia poniższy rysunek.

Powróćmy do formularza głównego klasy Form1, by zdefi­niować metodę obsługi zdarzenia Click polecenia Parametry (polecenie Koniec ma jedynie zamknąć okno główne za pomocą metody Close, a tym samym zakończyć wykonanie programu). Metoda powinna utworzyć okno podrzędne klasy Form2, ustawić w nim parametry naryso­wanej krzywej i wyświe­tlić go, a gdy użytko­wnik zamknie je przyciskiem OK, pobrać podane w nim parametry, uaktualnić napis na pasku tytułowym i etykiety na pasku statusu okna głównego, wyznaczyć nowe parametry rysunku i wreszcie wymusić odświe­żenie zawar­tości obszaru roboczego (rysowanie krzywej). Pomijając na razie kwestię przekazy­wania danych pomiędzy oknami, możemy tę metodę sformu­łować następująco:

private void parametryToolStripMenuItem_Click(object sender, EventArgs e)
{
    Form2 Param = new Form2();
    //                         Ustaw dane w oknie Param
    if (Param.ShowDialog() == DialogResult.OK)
    {
        //                     Pobierz dane z okna Param
        Form1_Load(sender, e);
        Invalidate();
    }
}

Jak widać, metoda parametryToolStripMenuItem_Click realizuje część operacji (uaktual­nienie tytułu okna i etykiet paska statusu, wyzna­czenie parame­trów rysunku), wywołując przyto­czoną na początku metodę obsługi zdarzenia Load formu­larza klasy Form1.

W kolejnym kroku precyzowania programu przecho­dzimy do rozbudowy wygenero­wanego przez Visual Studio kodu źródłowego Form2.cs. Zaczynamy od sformuło­wania metody publicznej klasy Form2, która ustawia w kontrolkach okna podrzędnego parametry krzywej zobrazo­wanej w oknie głównym:

public void Ustaw(bool epic, double R, double r, double d)
{
    if (epic)
        radioButton1.Checked = true;
    else
        radioButton2.Checked = true;
    textBox1.Text = R.ToString();
    textBox2.Text = r.ToString();
    textBox3.Text = d.ToString();
}

Zależnie od wartości argumentu epic metoda zaznacza pierwszy lub drugi przycisk radiowy, pokazując w ten sposób, czy aktualnie jest naryso­wana epicy­kloida, czy hipocy­kloida. W odróżnieniu od pól wyboru (typ CheckBox) przyciski radiowe umieszczone we wspólnym konte­nerze współdzia­łają ze sobą, umożli­wiając zazna­czenie tylko jednego. Metoda zapisuje pozostałe parametry krzywej w polach edycyjnych. Jej wywo­łanie umieszczamy w przedsta­wionej powyżej metodzie parametryTool­StripMenuItem_Click klasy Form1 zamiast pierw­szego komen­tarza.

Aby zaprogra­mować operację odwrotną, która pobiera parametry z kontrolek okna podrzę­dnego i przesyła je do miejsca wywołania, posłużymy się pomocniczą metodą, która przekształca tekst pola edycy­jnego na liczbę rzeczy­wistą. Dwa pola odpowia­dające promieniom okręgów powinny reprezen­tować liczby nieujemne, trzecie liczbę dowolnego znaku. Przyjmiemy, że pierwszym argu­mentem metody jest kontrolka, której tekst ma być zamieniony na liczbę, zaś drugim wartość logiczna określa­jąca, czy ma to być liczba dodatnia (true – tak, false – nieko­niecznie). Zadanie to da się wyrazić następująco:

private double Liczba(TextBox tb, bool plus)
{
    tb.Focus();
    double x = Convert.ToDouble(tb.Text);
    if (plus && (x <= 0))
        throw new Exception("Wartość powinna być dodatnia.");
    return x;
}

Efektem wywołania metody Focus jest ustawienie kursora wewnątrz kontrolki, co oznacza, że będzie ona przyjmować zdarzenia genero­wane przez klawia­turę. Gdy w trakcie przekształ­cania tekstu na liczbę zostanie zgłoszony wyjątek (błąd konwersji lub niedodatnia wartość), program wyświetli okno komuni­katu informu­jące o przy­czynie niepowo­dzenia. Po jego zamknięciu użytkownik będzie znał miejsce wystąpienia błędu, gdyż kursor będzie widoczny wewnątrz pola, którego tekst wymaga poprawy.

Metodę Liczba wywołujemy trzykrotnie w metodzie button1_­Click obsługi zdarzenia Click przycisku OK formu­larza pomocni­czego, by pobrać dane numery­czne z trzech kontrolek edycyjnych do pól prywa­tnych R, rd klasy Form2. Gdy ta operacja przebiegnie pomyślnie, właści­wości Dialog­Result okna parametrów przypisu­jemy wartość OK typu wylicze­niowego Dialog­Result, co powoduje jego zamknięcie, a w istocie tylko ukrycie, natomiast gdy zgłoszony zostanie wyjątek, wyświe­tlamy komunikat o błędzie:

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        R = Liczba(textBox1, true);
        r = Liczba(textBox2, true);
        if (radioButton2.Checked && (R <= r))
            throw new Exception("Nieprawidłowe dane (powinno być r < R).");
        d = Liczba(textBox3, false);
        DialogResult = DialogResult.OK;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Błąd", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

private double R, r, d;

Po tych przygotowaniach możemy wreszcie sprecyzować prostą metodę publiczną klasy Form2 umożli­wiającą pobranie parametrów krzywej z okna podrzę­dnego i przekazanie ich do miejsca wywołania:

public void Pobierz(out bool epic, out double R, out double r, out double d)
{
    epic = radioButton1.Checked;
    R = this.R;
    r = this.r;
    d = this.d;
}

Oczywiście parametry metody są przekazywane przez referencję. Jej wywołanie umieszczamy w omówionej powyżej metodzie parametryTool­StripMenuItem_Click klasy Form1 zamiast drugiego komen­tarza.

Program w C#

Ostateczna wersja kodu źródłowego C# zawartego w plikach Form1.csForm2.cs programu kreślącego epicy­kloidę lub hipocy­kloidę (zależnie od decyzji użytko­wnika) jest przedsta­wiona na dwóch poniższych listingach. Kod zawarty w pliku Form1.cs ma postać:

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 EpiHipo
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private bool epic = false;  // Epicykloida (true), hipocykloida (false)
        private double R = 5;       // Promień stałego okręgu
        private double r = 1;       // Promień ruchomego okręgu
        private double d = 2;       // Długość ramienia

        private float p, q;         // Środek obszaru roboczego
        private double skala;       // Czynnik skalujący
        private int n;              // Liczba odcinków łamanej

        private delegate void Cykloida(double fi, out double x, out double y);

        private void Epicykloida(double fi, out double x, out double y)
        {
            x = (R + r) * Math.Cos(fi) - d * Math.Cos((R + r) * fi / r);
            y = (R + r) * Math.Sin(fi) - d * Math.Sin((R + r) * fi / r);
        }

        private void Hipocykloida(double fi, out double x, out double y)
        {
            x = (R - r) * Math.Cos(fi) + d * Math.Cos((R - r) * fi / r);
            y = (R - r) * Math.Sin(fi) - d * Math.Sin((R - r) * fi / r);
        }

        private void Parametry_Rysunku()
        {
            p = ClientSize.Width / 2;
            q = ClientSize.Height / 2;
            n = (int)(0.9 * Math.Min(p, q - menuStrip1.Height));
            skala = n / ((epic ? (R + r) : (R - r)) + Math.Abs(d));
            if (R > r)
                n = (int)(n * (R / r));
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Text = epic ? "Epicykloida" : "Hipocykloida";
            toolStripStatusLabel1.Text = "R = " + R;
            toolStripStatusLabel2.Text = "r = " + r;
            toolStripStatusLabel3.Text = "d = " + d;
            Parametry_Rysunku();
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            if (n > 1)
            {
                float R = (float)(skala * this.R);
                e.Graphics.DrawEllipse(Pens.SkyBlue, p - R, q - R, 2 * R, 2 * R);
                float r = (float)(skala * this.r);
                float s = p + (epic ? R + r : R - r);
                e.Graphics.DrawEllipse(Pens.SkyBlue, s - r, q - r, 2 * r, 2 * r);
                float d = (float)(skala * this.d);
                e.Graphics.DrawLine(Pens.SkyBlue, s, q, epic ? s - d : s + d, q);
                Cykloida cykloida = epic ? (Cykloida)Epicykloida : (Cykloida)Hipocykloida;
                PointF[] P = new PointF[n + 1];
                for (int k = 0; k <= n; k++)
                {
                    double x, y;
                    cykloida(2 * k * Math.PI / n, out x, out y);
                    P[k].X = (float)(skala * x) + p;
                    P[k].Y = q - (float)(skala * y);
                }
                Pen pen = new Pen(Color.Red, 2);
                pen.LineJoin = System.Drawing.Drawing2D.LineJoin.Round;
                e.Graphics.DrawLines(pen, P);
                pen.Dispose();
            }
        }

        private void Form1_Resize(object sender, EventArgs e)
        {
            Parametry_Rysunku();
            Invalidate();
        }

        private void parametryToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Form2 Param = new Form2();
            Param.Ustaw(epic, R, r, d);
            if (Param.ShowDialog() == DialogResult.OK)
            {
                Param.Pobierz(out epic, out R, out r, out d);
                Form1_Load(sender, e);
                Invalidate();
            }
        }

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

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (MessageBox.Show("Czy rzeczywiście zakończyć program?", "Koniec",
                MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
                e.Cancel = true;
        }
}

Kod źródłowy C# zawarty w pliku Form2.cs 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 EpiHipo
{
    public partial class Form2 : Form
    {
        public Form2()
        {
            InitializeComponent();
        }

        private double R, r, d;

        public void Ustaw(bool epic, double R, double r, double d)
        {
            if (epic)
                radioButton1.Checked = true;
            else
                radioButton2.Checked = true;
            textBox1.Text = R.ToString();
            textBox2.Text = r.ToString();
            textBox3.Text = d.ToString();
        }

        private double Liczba(TextBox tb, bool plus)
        {
            tb.Focus();
            double x = Convert.ToDouble(tb.Text);
            if (plus && (x <= 0))
                throw new Exception("Wartość powinna być dodatnia.");
            return x;
        }

        public void Pobierz(out bool epic, out double R, out double r, out double d)
        {
            epic = radioButton1.Checked;
            R = this.R;
            r = this.r;
            d = this.d;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                R = Liczba(textBox1, true);
                r = Liczba(textBox2, true);
                if (radioButton2.Checked && (R <= r))
                    throw new Exception("Nieprawidłowe dane (powinno być r < R).");
                d = Liczba(textBox3, false);
                DialogResult = DialogResult.OK;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Błąd",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }
}

A oto wynik wykonania programu dla kilku przykła­dowych zestawów parame­trów krzywej:

a) epicykloida dla R=5, r=1, d=2,

b) epicykloida dla R=r=d=2,5 (kardioida),

c) hipocykloida dla R=5, r=d=1,25 (asteroida),

a) hipocykloida dla R=100, r=5, d=75.


Opracowanie przykładu: kwiecień 2019