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

Przykład C#

Ruch harmoniczny tłumiony Formularz główny i parametry wykresu Kreślenie wykresu z użyciem delegatów Formularz podrzędny i jego obsługa Program w C# Poprzedni przykład Następny przykład Program w C++ Kontakt

Ruch harmoniczny tłumiony

Zadaniem programu jest sporządzenie wykresu funkcji opisu­jącej ruch harmoni­czny tłumiony, zwany również drganiem harmoni­cznym tłumionym. Wychy­lenie punktu material­nego z poło­żenia równowagi zmienia się w takim ruchu okresowo (periody­cznie, sinusoi­dalnie) i maleje wraz z upływem czasu za przyczyną siły hamującej (tarcie, lepkość, opór powie­trza itp.). Zależność wychy­lenia y drgają­cego punktu od czasu x można wyrazić za pomocą wzoru

w którym A oznacza amplitudę drgań (maksymalne wychy­lenie punktu), b – czynnik tłumiący, ω – częstotli­wość drgań (pulsację), φ – fazę początkową. Wartości A, bω są liczbami dodatnimi (dla b=0 ruch nie jest tłumiony, a dla ω=0 nie jest perio­dyczny). Wychylenie punktu jest funkcją zawiera­jącą się pomiędzy dwiema obwiedniami

znikającą w czasie (przy b≠0). Ze względu na obecność funkcji cosinus w wyrażeniu opisu­jącym ruch harmo­niczny wygodnie jest wyrażać czas x i fazę φ w stopniach lub radianach.

Formularz główny i parametry wykresu

Rozbudowę formularza aplikacji rozpoczynamy od usta­wienia jego właści­wości Text na Ruch harmo­niczny tłumionyBackColor na Window (białe tło obszaru robo­czego okna) oraz utworzenia menu głównego (kompo­nent MenuStrip) z dwiema pozy­cjami ParametryKoniec (rys.). Efekt podkre­ślenia liter P i K będący konse­kwencją poprze­dzenia ich symbo­lem & umożliwia korzystanie ze skrótów klawiatu­rowych Alt+P i Alt+K. Środo­wisko Visual C# nazwie menu główne menuStrip1, zaś jego pozycje parametryTool­StripMenuItemkoniecTool­StripMenuItem. Pierwsza pozycja menu posłuży do zmiany parame­trów ruchu, druga do zakoń­czenia aplikacji.

Możemy teraz przystąpić do edycji wygenero­wanego przez środo­wisko szablo­nowego kodu źródło­wego formularza klasy Form1. Zaczynamy od zadekla­rowania szeregu pól prywa­tnych dla parametrów ruchu harmoni­cznego tłumionego oraz zakresów współrzę­dnych okna w układzie rzeczywistym i widoku w układzie ekranowym, a także czynników skalu­jących używanych przy przecho­dzeniu od jednego do drugiego układu:

private double A = 2;                       // Amplituda drgań;
private int T = 360;                        // Przedział czasowy (stopnie)
private double b = 0.5;                     // Czynnik tłumiący
private double omega = 8;                   // Częstotliwość drgań (Hz)
private int faza = 0;                       // Faza początkowa (stopnie)
private double fi;                          // Faza początkowa (radiany)

private double xRmin, xRmax, yRmin, yRmax;  // Parametry układu rzeczywistego
private int xEmin, xEmax, yEmin, yEmax;     // Parametry układu ekranowego
private int xOmin, xOmax, yOmin, yOmax;     // Zakres osi układu ekranowego
private double sx, sy;                      // Czynniki skalujące

Jak widać, parametry ruchu mają przypisane wartości przykładowe, by po urucho­mieniu aplikacji pojawił się na ekranie typowy dla rozpatry­wanego zjawiska wykres. Ma się rozumieć, użytkownik będzie mógł je zmieniać, wybie­rając w menu polecenie Parametry. Dla ułatwienia przedział czasowy i fazę początkową będzie podawał w stopniach, dlatego też przy wyzna­czaniu zakresu współrzę­dnych okna w układzie rzeczy­wistym, w którym jest określona funkcja opisująca ruch i jej obwiednie, należy dokonać ich konwersji na radiany:

private void Parametry_Rzeczywiste()
{
    xRmin = 0;
    xRmax = Math.PI * T / 180;
    yRmax = (A > 0) ? A : 1;
    yRmin = -yRmax;
    fi = Math.PI * faza / 180;
}

Zakres współrzędnych y okna w układzie rzeczy­wistym określa amplituda drgań A. W celu uniknięcia błędu dzielenia przez zero przy obli­czaniu czynników skalujących zostaje skorygo­wany do prze­działu od –1 do 1, gdyby użytkownik nadał jej wartość zerową.

Zobrazowanie funkcji ruchu harmonicznego i jej obwiedni wymaga również wyzna­czenia zakresu współrzę­dnych widoku w układzie ekranowym. Oczywistym elementem wzbogaca­jącym rysunek będzie powszechnie stosowany prostokątny układ dwóch odcinków zakończo­nych strzałkami symboli­zujący osie x i y. Naturalnie, ze względów estety­cznych zakresy tych odcinków powinny być nieco większe od rozmiarów widoku i nieco mniejsze od rozmiarów obszaru roboczego okna. Długość i szerokość strzałek można ustalić, odpowie­dnio, na 8 i 6 pikseli. Rozważania te prowadzą do następu­jącej definicji metody formularza Form1:

private void Parametry_Ekranowe()
{
    xOmin = ClientSize.Width / 25;
    xOmax = ClientSize.Width - xOmin;
    xEmin = 2 * xOmin;
    xEmax = xOmax - xEmin - 8;
    int d = (ClientSize.Height - menuStrip1.Height) / 25;
    yOmax = menuStrip1.Height + d;
    yOmin = ClientSize.Height - d;
    yEmax = yOmax + d + 8;
    yEmin = yOmin - d;
}

Nie ulega kwestii, że parametry okna w układzie rzeczy­wistym i widoku w układzie ekranowym powinny być wyznaczone przed ryso­waniem wykresu. Jednak obliczanie ich wewnątrz metody obsługi zdarzenia Paint formu­larza nie byłoby opty­malne, gdyż może być ono wyzwolone, kiedy parametry te pozostają niezmie­nione, na przykład gdy okno jest odkry­wane po uprzednim zakryciu przez inne okno. Istnieją trzy sytuacje, w których wypada wyznaczać parametry obu rodzajów lub tylko jednego:

Pomijając na razie ostatni z tych przypadków, możemy metody obsługi zdarzeń LoadResize formu­larza sformu­łować następująco:

private void Form1_Load(object sender, EventArgs e)
{
    Parametry_Ekranowe();
    Parametry_Rzeczywiste();
}

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

Kreślenie wykresu z użyciem delegatów

W kolejnym kroku precyzowania programu zajmiemy się zdefinio­waniem metody obsługi zdarzenia Paint formu­larza. Jej kod będzie bardziej czytelny i zrozu­miały, gdy wydzielimy z niej metodę kreślenia układu współrzę­dnych i metodę rysowania wykresu funkcji opisu­jącej ruch harmo­niczny tłumiony. W obu tych pomocni­czych metodach konieczne będzie przecho­dzenie od współrzę­dnych kartezjań­skich układu rzeczywi­stego, w którym ruch harmoni­czny jest zdefinio­wany, do współrzę­dnych układu ekrano­wego, w którym ma być zobrazowany.

Przedstawiona poniżej pierwsza z wydzie­lonych metod kreśli układ współrzę­dnych oraz opisuje jego osie i przedział czasowy. Obrazem punktu (0,0) układu rzeczywi­stego jest w przekształ­ceniach xEyE początek układu ekranowego, toteż wyznaczenie jego współrzę­dnych pozwoliło na określenie pozycji rysowa­nych odcinków reprezentu­jących osie x i y. Metoda kończy je krótkimi odcinkami obrazu­jącymi strzałki i opisuje utworzoną na początku czcionką Arial o rozmiarze 10 punktów drukar­skich niszczoną na końcu. Współ­rzędne opisów zostały dobrane ekspery­mentalnie.

private void Rysuj_Układ(Graphics g, Pen pen, Brush brush)  // Wersja uproszczona
{
    int xZero = xE(0), yZero = yE(0);
    Font font = new Font("Arial", 10);
    g.DrawLine(pen, xOmin, yZero, xOmax, yZero);
    g.DrawLine(pen, xOmax - 8, yZero - 3, xOmax, yZero);
    g.DrawLine(pen, xOmax - 8, yZero + 3, xOmax, yZero);
    g.DrawString("x", font, brush, xOmax - 9, yZero - 18);
    g.DrawLine(pen, xZero, yOmin, xZero, yOmax);
    g.DrawLine(pen, xZero - 3, yOmax + 8, xZero, yOmax);
    g.DrawLine(pen, xZero + 3, yOmax + 8, xZero, yOmax);
    g.DrawString("y", font, brush, xZero + 4, yOmax - 6);
    g.DrawString("0", font, brush, xZero - 12, yZero - 16);
    g.DrawLine(pen, xEmax, yZero - 2, xEmax, yZero + 2);
    string s = string.Format("{0}°", T);
    g.DrawString(s, font, brush, xEmax, yZero + 2);
    font.Dispose();
}

Druga z wydzielonych metod dotyczy kreślenia wykresu funkcji opisującej ruch harmoni­czny tłumiony, a w istocie trzech wykresów obejmu­jących także górną i dolną obwiednię. Te trzy funkcje są zdefinio­wane w tym samym wspólnym oknie w układzie rzeczy­wistym i mają taką samą sygnaturę (taką samą liczbę parame­trów, ich typy i typ zwracanej wartości). Ich implemen­tacja, w której unika się powta­rzania części wyrażeń arytmety­cznych, wygląda następu­jąco:

private double Amax(double x)
{
    return A * Math.Exp(-b * x);
}

private double Amin(double x)
{
    return -Amax(x);
}

private double Ruch(double x)
{
    return Amax(x) * Math.Cos(omega * x + fi);
}

Naturalnym oczekiwaniem jest, aby metoda rysowała wykres dowolnej funkcji określonej w parame­trze. Potrójny wykres byłby wówczas efektem jej trzykro­tnego wywołania: dla argu­mentów Amax, AminRuch. Zadanie dotyczy problemu przekazy­wania parame­trów reprezen­tujących metody i wymaga użycia w C# tzw. delegatów, których odpowie­dnikami są w C i C++ wskaźniki reprezen­tujące funkcje. Formalnie delegat (ang. delegate) jest obiektowym typem danych, który wskazuje na metodę. Definicja delegata przypomina nagłówek metody i może wystąpić wewnątrz klasy lub poza nią. Zawiera słowo kluczowe delegate, nazwę delegata, specyfi­kację parame­trów (jeśli istnieją) i typ zwracanej wartości. W rozpatry­wanym programie delegat ma wskazywać na prywatną w klasie Form1 metodę przyjmu­jącą jeden parametr typu double i zwraca­jącą wartość typu double:

private delegate double Funkcja(double x);

Po tych rozważaniach możemy przejść do sformu­łowania metody ryso­wania wykresu funkcji określonej przez parametr zdefinio­wanego typu Funkcja. Oczywiście zamiast krzywej przedsta­wiającej graficznie funkcję rysujemy kolejne odcinki łamanej o odpo­wiednio dużej liczbie wierz­chołków leżących na tej krzywej:

private void Rysuj_Wykres(Graphics g, Pen pen, Funkcja f)
{
    int n = xEmax - xEmin;
    int x1 = xEmin, y1 = yE(f(xRmin)), x2, y2;
    for (int k = 1; k <= n; k++)
    {
        x2 = xEmin + k;
        y2 = yE(f(k / sx + xRmin));
        g.DrawLine(pen, x1, y1, x2, y2);
        x1 = x2;
        y1 = y2;
    }
}

Gwoli wyjaśnienia, wierzchołek o indeksie k ∈ {0,1,...,n} ma w układzie ekra­nowym współ­rzędną x równą wartości xEmin+k. W układzie rzeczy­wistym odpo­wiada mu dokła­dnie jeden punkt o współ­rzędnej x równej wartości k/sx+xRmin i współ­rzędnej y równej wartości f(k/sx+xRmin). Zatem w układzie ekra­nowym współ­rzędną y tego wierz­chołka jest wartość wyra­żenia yE(f(k/sx+xRmin)).

Metoda obsługi zdarzenia Paint formu­larza, w której po obliczeniu czynników skalu­jących wywoły­wane są sformuło­wane powyżej metody Rysuj_UkładRysuj_Wykres, ma przy przykła­dowym wyborze pióra i pędzla postać:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    sx = (xEmax - xEmin) / (xRmax - xRmin);
    sy = (yEmax - yEmin) / (yRmax - yRmin);
    Rysuj_Układ(e.Graphics, Pens.Black, Brushes.Black);
    Rysuj_Wykres(e.Graphics, Pens.Blue, Amin);
    Rysuj_Wykres(e.Graphics, Pens.Blue, Amax);
    Pen pen = new Pen(Color.Red, 2);
    Rysuj_Wykres(e.Graphics, pen, Ruch);
    pen.Dispose();
}

Program nie jest jeszcze ukończony, możemy go jednak skompi­lować i uruchomić, by sprawdzić, czy nie zawiera błędów i działa poprawnie. Chociaż na ekranie pojawi się wtedy prawi­dłowy wykres ruchu harmoni­cznego o ustalo­nych wstępnie parame­trach, czas jego rysowania, w szczegól­ności przy każdej zmianie rozmiaru okna, jest nie do zaakcepto­wania. Aby usunąć ten defekt, konstru­ujemy lepszą metodę rysowania funkcji. Jej idea polega na przechowy­waniu współrzę­dnych wszystkich wierz­chołków łamanej w tablicy elementów typu Point i pojedyn­czego wywołania metody DrawLines klasy Graphics zamiast wielu wywołań metody DrawLine:

private void Rysuj_Wykres(Graphics g, Pen pen, Funkcja f)
{
    Point[] P = new Point[xEmax - xEmin + 1];
    for (int k = 0; k < P.Length; k++)
    {
        P[k].X = xEmin + k;
        P[k].Y = yE(f(k / sx + xRmin));
    }
    g.DrawLines(pen, P);
}

Przy tej wersji metody Rysuj_Wykres czas tworzenia wykresu funkcji ruchu harmoni­cznego i jej obwiedni nie budzi żadnych zastrzeżeń, ale minima­lizacja okna programu ujawnia inny jego defekt sygnalizowany w oknie komunikatu (rys.). Gdy klikniemy na przycisku Szczegóły, z dostar­czonych przez platformę .NET infor­macji możemy wydedu­kować, że przyczyną błędu jest nieprawi­dłowa (ujemna lub zerowa) liczba elementów tablicy Point.

Błąd można łatwo naprawić, blokując w metodzie obsługi zdarzenia Paint formu­larza kreślenie rysunku w przy­padku absurdalnej szero­kości widoku:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    if (xEmax > xEmin)
    {
        sx = (xEmax - xEmin) / (xRmax - xRmin);
        sy = (yEmax - yEmin) / (yRmax - yRmin);
        Rysuj_Układ(e.Graphics, Pens.Black, Brushes.Black);
        Rysuj_Wykres(e.Graphics, Pens.Blue, Amin);
        Rysuj_Wykres(e.Graphics, Pens.Blue, Amax);
        Pen pen = new Pen(Color.Red, 2);
        Rysuj_Wykres(e.Graphics, pen, Ruch);
        pen.Dispose();
    }
}

Formularz podrzędny i jego obsługa

Parametry ruchu harmoni­cznego tłumio­nego najwygodniej jest określać, posługując się formula­rzem podrzę­dnym, który wyświetla je i umożliwia ich modyfi­kację. Aby dodać taki formu­larz do aplikacji, rozwijamy pozycję Projekt w menu głównym środo­wiska i wybieramy z listy dowolne z poleceń:

Następnie w części centralnej okna, które się pojawi, klikamy dwukro­tnie na opcji Formularz systemu Windows lub po jej wybraniu naciskamy przycisk Dodaj. Wówczas środo­wisko Visual C# wygene­ruje nową klasę Form2 wywo­dzącą się od standar­dowej klasy Form. Podobnie jak w przypadku klasy Form1, jej kod jest rozdzie­lony pomiędzy dwa pliki utworzone w folderze projektu:

Nowemu formularzowi nadajemy tytuł Parametry i blokujemy możli­wość zmiany jego rozmiarów, usta­wiając właści­wość FormBorder­Style na Fixed­Single (pojedyncza stała ramka) i Maxi­mizeBox na False (zabloko­wana maksymali­zacja), po czym wsta­wiamy do niego pięć kontrolek edycyjnych reprezen­tujących parametry ruchu harmoni­cznego (trzy typu TextBox i dwie typu Numeric­UpDown) oraz pięć opisu­jących je etykiet (typ Label). Symbol stopnia (o) niedo­stępny na klawia­turze kopiu­jemy do właściwości Text dwóch etykiet z tablicy znaków Windows (akcesoria systemu). Na koniec wstawiamy do formu­larza dwa przyciski (typ Button) z napisami OKAnuluj oraz korygu­jemy jego rozmiary i rozmie­szczenie kompo­nentów. Wynik końcowy tych czyn­ności jest pokazany na poniż­szym rysunku.

Zanim przejdziemy do rozbudowy kodu źródłowego formu­larza podrzę­dnego, zmieniamy jeszcze szereg właści­wości jego kompo­nentów. Po pierwsze, nadajemy pięciu kontrolkom edycyjnym nazwy sugeru­jące ich rolę w programie, ustawiając właści­wość Name tych kontrolek na parA (amplituda), parT (przedział czasowy), parB (czynnik tłumiący), parOmega (często­tliwość) i parFaza (faza początkowa). Po drugie, przypisu­jemy właści­wościom Minimum, MaximumIncrement kontrolki parT wartości 90, 1800 i 90, zaś tożsamym właści­wościom kontrolki parFaza wartości -180, 180 i 15. Dzięki temu górną granicę przedziału czasowego będzie można zmieniać za pomocą strzałek w zakresie od 90o do 5x360o (=1800o) co 90o, a fazę początkową od -180o do 180o co 15o. Po trzecie wreszcie, właściwość DialogResult przycisku button2 o etykiecie Anuluj ustawiamy na Cancel, ponieważ jego kliknięcie ma tylko powodować zamknięcie okna. Inaczej jest w przy­padku przycisku button1 o etykiecie OK, bowiem gdy okno zawiera nieprawi­dłowe dane, nie powinno dać się zamknąć, by zapobiec tworzeniu rysunku dla parame­trów niezgo­dnych z założe­niami.

Okno parame­trów będzie wyświe­ltlane jako dialogowe modalne, gdy w menu okna głównego wybierzemy polecenie Parametry. Aby takie okno pokazać, wywołujemy jego metodę ShowDialog, zaś aby je zamknąć, przypi­sujemy jego właści­wości DialogResult wartość różną od None typu wylicze­niowego DialogResult. Zazwyczaj jest nią wartość związana z przyci­skiem, np. OK dla przycisku o etykiecie OKCancel dla przycisku Anuluj. Gdy taki przycisk zostanie naciśnięty, jego właści­wość DialogResult zostaje przypisana właści­wości DialogResult okna, co spowoduje jego zamknięcie, a de facto tylko ukrycie. Wartość zwrotna metody ShowDialog pozwala sprawdzić, w jaki sposób okno zostało zamknięte (ukryte), a tym samym określić, jaką akcję należy podjąć w dalszej kolejności.

Powróćmy na moment do formularza klasy Form1, by zdefi­niować metodę obsługi zdarzenia Click polecenia menu Parametry. Metoda powinna utworzyć i wyświe­tlić na ekranie okno podrzędne klasy Form2, a gdy użytko­wnik zamknie je przyciskiem OK, wymusić odświe­żenie zawar­tości okna głównego (naryso­wanie układu współrzę­dnych i wykresu) dla pobranych z okna podrzę­dnego parametrów ruchu harmoni­cznego. Pomijając na razie kwestię przekazy­wania danych pomiędzy tymi oknami, wstępną postać metody możemy sformu­łować następująco:

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

Gdy uruchomimy aplikację i wybierzemy polecenie Parametry, okno podrzędne pojawi się na ekranie, a gdy naciśniemy w nim przycisk Anuluj, zniknie. Naszym zadaniem jest teraz zaprogra­mowanie jego współpracy z oknem głównym. Zaczynamy od sprecyzo­wania metody klasy Form2, która ustawia parametry zobrazo­wanego w oknie głównym ruchu harmoni­cznego w kontrolkach edycyj­nych okna podrzędnego:

public void Ustaw(double A, int T, double b, double omega, int faza)
{
    parA.Text = A.ToString();
    parT.Value = T;
    parB.Text = b.ToString();
    parOmega.Text = omega.ToString();
    parFaza.Value = faza;
}

Metoda jest publiczna (public), tzn. jest dostępna nie tylko w klasie Form2, ale i poza nią. Jej wywołanie umieszczamy w przedsta­wionej powyżej metodzie parametryTool­StripMenuItem_Click klasy Form1 zamiast pierw­szego komen­tarza. Nieco trudniej jest zaprogra­mować operację odwrotną, która pobiera parametry z kontrolek okna podrzędnego i przesyła je do miejsca wywołania, ponieważ wymaga sprawdzenia poprawności wprowa­dzonych danych, gdy naciśnięty zostanie przycisk OK. Kłopot mogą sprawić trzy kontrolki typu TextBox (parA, parBparOmega), których właści­wość Text może nie dać się przekształ­cić na wartość numery­czną bądź może reprezen­tować liczbę ujemną. Dla wygody wprowa­dzamy pomocniczą metodę, która zwraca nieujemną liczbę zapisaną w kontrolce lub zgłasza wyjątek, gdy zapis ten nie jest poprawny:

private double Liczba(TextBox tb)
{
    double x = Convert.ToDouble(tb.Text);
    if (x < 0)
        throw new Exception("Wartość ujemna niedozwolona.");
    return x;
}

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 kontrolek parA, parBparOmega do pól prywa­tnych A, bomega 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 (ukrycie), natomiast w przy­padku błędu wyświe­tlamy komunikat informu­jący o przy­czynie niepowo­dzenia:

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        A = Liczba(parA);
        b = Liczba(parB);
        omega = Liczba(parOmega);
        DialogResult = DialogResult.OK;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Błąd");
    }
}

private double A, b, omega;

Dzięki wprowa­dzeniu pól A, b i omega unikamy ponownej konwersji właściwości Text kontrolek na liczby w metodzie klasy Form2 pobiera­jącej parametry ruchu harmoni­cznego z okna podrzę­dnego i przekazu­jącej je do miejsca wywołania. Możemy ją sformu­łować następująco:

public void Pobierz(out double A, out int T, out double b, out double omega, out int faza)
{
    A = this.A;
    T = (int)parT.Value;
    b = this.b;
    omega = this.omega;
    faza = (int)parFaza.Value;
}

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 wykres ruchu harmoni­cznego tłumionego jest przedsta­wiona na dwóch poniższych listingach. W obu wprowa­dzono drobne udoskona­lenia nadające programowi bardziej profesjo­nalny charakter. Otóż w metodzie RysujWykres klasy Form1 uzupeł­niono opis układu współrzę­dnych o maksy­malną i mini­malną wartość wychylenia drgają­cego punktu w przy­padku niezerowej amplitudy, a do wyzna­czenia pozycji rysowa­nego na ekranie tekstu użyto metody Measure­String, która zwraca jego szerokość i wysokość w pikselach jako strukturę typu SizeF. Ponadto wzboga­cono okno komuni­katu wyświe­tlane przez metodę obsługi zdarzenia FormClosing o wymowną ikonę zapytania określoną przez wartość Question typu wylicze­niowego Message­BoxIcon. Pełny kod zawarty w pliku Form1.cs wygląda następująco:

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

        private delegate double Funkcja(double x);

        private void Rysuj_Wykres(Graphics g, Pen pen, Funkcja f)
        {
            Point[] P = new Point[xEmax - xEmin + 1];
            for (int k = 0; k < P.Length; k++)
            {
                P[k].X = xEmin + k;
                P[k].Y = yE(f(k / sx + xRmin));
            }
            g.DrawLines(pen, P);
        }

        private void Rysuj_Układ(Graphics g, Pen pen, Brush brush)
        {
            int xZero = xE(0), yZero = yE(0);
            Font font = new Font("Arial", 10);
            g.DrawLine(pen, xOmin, yZero, xOmax, yZero);
            g.DrawLine(pen, xOmax - 8, yZero - 3, xOmax, yZero);
            g.DrawLine(pen, xOmax - 8, yZero + 3, xOmax, yZero);
            g.DrawString("x", font, brush, xOmax - 9, yZero - 18);
            g.DrawLine(pen, xZero, yOmin, xZero, yOmax);
            g.DrawLine(pen, xZero - 3, yOmax + 8, xZero, yOmax);
            g.DrawLine(pen, xZero + 3, yOmax + 8, xZero, yOmax);
            g.DrawString("y", font, brush, xZero + 4, yOmax - 6);
            g.DrawString("0", font, brush, xZero - 12, yZero - 16);
            g.DrawLine(pen, xEmax, yZero - 2, xEmax, yZero + 2);
            string s = string.Format("{0}°", T);
            g.DrawString(s, font, brush, xEmax, yZero + 2);
            if (A > 0)
            {
                string s = A.ToString();
                SizeF d = g.MeasureString(s, font);
                g.DrawString(s, font, brush, xZero - d.Width, yE(A) - 8);
                s = (-A).ToString();
                d = g.MeasureString(s, font);
                g.DrawString(s, font, brush, xZero - d.Width, yE(-A) - 8);
            }
            font.Dispose();
        }

        private void Parametry_Ekranowe()
        {
            xOmin = ClientSize.Width / 25;
            xOmax = ClientSize.Width - xOmin;
            xEmin = 2 * xOmin;
            xEmax = xOmax - xEmin - 8;
            int d = (ClientSize.Height - menuStrip1.Height) / 25;
            yOmax = menuStrip1.Height + d;
            yOmin = ClientSize.Height - d;
            yEmax = yOmax + d + 8;
            yEmin = yOmin - d;
        }

        private void Parametry_Rzeczywiste()
        {
            xRmin = 0;
            xRmax = Math.PI * T / 180;
            yRmax = (A > 0) ? A : 1;
            yRmin = -yRmax;
            fi = Math.PI * faza / 180;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Parametry_Ekranowe();
            Parametry_Rzeczywiste();
        }

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

        private double Amax(double x)
        {
            return A * Math.Exp(-b * x);
        }

        private double Amin(double x)
        {
            return -Amax(x);
        }

        private double Ruch(double x)
        {
            return Amax(x) * Math.Cos(omega * x + fi);
        }

        private int xE(double x)
        {
            return (int)(sx * (x - xRmin)) + xEmin;
        }

        private int yE(double y)
        {
            return (int)(sy * (y - yRmin)) + yEmin;
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            if (xEmax > xEmin)
            {
                sx = (xEmax - xEmin) / (xRmax - xRmin);
                sy = (yEmax - yEmin) / (yRmax - yRmin);
                Rysuj_Układ(e.Graphics, Pens.Black, Brushes.Black);
                Rysuj_Wykres(e.Graphics, Pens.Blue, Amin);
                Rysuj_Wykres(e.Graphics, Pens.Blue, Amax);
                Pen pen = new Pen(Color.Red, 2);
                Rysuj_Wykres(e.Graphics, pen, Ruch);
                pen.Dispose();
            }
        }

        private void parametryToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Form2 Parametry = new Form2();
            Parametry.Ustaw(A, T, b, omega, faza);
            if (Parametry.ShowDialog() == DialogResult.OK)
            {
                Parametry.Pobierz(out A, out T, out b, out omega, out faza);
                Parametry_Rzeczywiste();
                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;
        }

        private double A = 2;                       // Amplituda drgań;
        private int T = 360;                        // Przedział czasowy (stopnie)
        private double b = 0.5;                     // Czynnik tłumiący
        private double omega = 8;                   // Częstotliwość drgań (Hz)
        private int faza = 0;                       // Faza początkowa (stopnie)
        private double fi;                          // Faza początkowa (radiany)

        private double xRmin, xRmax, yRmin, yRmax;  // Parametry układu rzeczywistego
        private int xEmin, xEmax, yEmin, yEmax;     // Parametry układu ekranowego
        private int xOmin, xOmax, yOmin, yOmax;     // Zakres osi układu ekranowego
        private double sx, sy;                      // Czynniki skalujące
    }
}

Ulepszenia kodu źródłowego zawartego w pliku Form2.cs dotyczą sytuacji, gdy użytko­wnik wprowadzi nieprawi­dłowy tekst do kontrolki okna parametrów. Tym razem okno komuni­katu wyświe­tlane przez metodę button1_Click klasy Form2 zostało ozdobione ikoną błędu określoną przez wartość Error typu wylicze­niowego Message­BoxIcon. Ważniej­szym usprawnie­niem jest wskazanie miejsca wystąpienia błędu poprzez odwołanie się metody Liczba do metody Focus, która ustawia kontrolkę zawiera­jącą błędny tekst jako przyjmu­jącą zdarzenia genero­wane przez klawia­turę. W rezul­tacie po zamknięciu okna komunikatu kursor będzie widoczny wewnątrz pola edycyj­nego, którego tekst wymaga poprawy. Oto pełny kod C# zawarty w pliku Form2.cs:

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

        public void Ustaw(double A, int T, double b, double omega, int faza)
        {
            parA.Text = A.ToString();
            parT.Value = T;
            parB.Text = b.ToString();
            parOmega.Text = omega.ToString();
            parFaza.Value = faza;
        }

        public void Pobierz(out double A, out int T, out double b,
                            out double omega, out int faza)
        {
            A = this.A;
            T = (int)parT.Value;
            b = this.b;
            omega = this.omega;
            faza = (int)parFaza.Value;
        }

        private double Liczba(TextBox tb)
        {
            tb.Focus();
            double x = Convert.ToDouble(tb.Text);
            if (x < 0)
                throw new Exception("Wartość ujemna niedozwolona.");
            return x;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                A = Liczba(parA);
                b = Liczba(parB);
                omega = Liczba(parOmega);
                DialogResult = DialogResult.OK;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Błąd",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private double A, b, omega;
    }
}

Wynik wykonania programu dla przykła­dowych parametrów jest pokazany na poniż­szym rysunku.


Opracowanie przykładu: marzec 2019