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

Przykład C#

Liniowa aproksymacja średniokwadratowa Moduł obliczeniowy Rzeczywisty i ekranowy układ kartezjański Zakres współrzędnych Projekt formularza Program w C# Poprzedni przykład Następny przykład Program w C++ Kontakt

Liniowa aproksymacja średniokwadratowa

Jednym z zagadnień pojawiających się przy rozwiązy­waniu wielu problemów fizy­cznych jest odtwo­rzenie funkcji na podstawie pomiarów jej wartości w skoń­czonej liczbie punktów. Zazwyczaj charakter funkcji f opisu­jącej dane zjawisko nie jest dokładnie znany, a wyniki pomiarów są obarczone błędami. W takich przypad­kach można jedynie zadowolić się rozwią­zaniem przybli­żonym, znajdując funkcję g w ustalonej klasie funkcji, np. w postaci wielo­mianu, która aproksy­muje (przybliża) funkcję f.

W najprostszym przypadku poszukiwaną funkcją aproksy­mującą jest funkcja liniowa (wielo­mian stopnia co najwyżej pierwszego), a metoda jej wyzna­czania jest oparta na minima­lizacji odchy­lenia średniokwa­dratowego. W celu uściślenia problemu zakładamy, że w punktach x1,x2,...,xn dane są wartości y1,y2,...,yn (dokładne lub przybli­żone) funkcji f:

Zagadnienie liniowej aproksymacji średniokwa­dratowej, znane w niektórych środo­wiskach zaintere­sowań pod nazwą regresji liniowej, polega na znale­zieniu funkcji g postaci

takiej, aby suma kwadratów odchyleń

była jak najmniejsza. Odchylenia funkcji aproksymu­jącej od punktów (xk,yk) są mierzone wzdłuż osi y (rys.).

Wyrażenie S traktowane jako funkcja dwóch zmiennych rzeczy­wistych a i b jest wielo­mianem stopnia drugiego o warto­ściach nieuje­mnych, ma więc minimum. Aby wyznaczyć wartości a i b, dla których S osiąga to minimum, przyrównu­jemy do zera pochodne cząstkowe S względem a i b. W rezul­tacie otrzymu­jemy układ dwóch równań liniowych o dwóch niewia­domych a i b:

Rozwiązujemy go w taki sposób, że najpierw obliczamy cztery sumy:

a następnie wartości a i b, korzystając ze wzorów Cramera wyraża­jących rozwiązanie układu poprzez wartości jego odpowie­dnich wyzna­czników:

Oczywiście do tych samych wzorów końcowych można dojść bez korzy­stania z wzorów Cramera, rozwią­zując powyższy układ równań metodą podsta­wiania lub przeci­wnych współczyn­ników. Układ ma dokładnie jedno rozwią­zanie (jest nieoso­bliwy) wtedy i tylko wtedy, gdy mianownik w powyższych wyraże­niach jest różny od zera. Łatwo zauważyć, że metoda wymaga co najmniej dwóch punktów.

Należy pamiętać, że obliczenia kompute­rowe na liczbach rzeczywi­stych (typu floatdouble w C#) nie są dokładne, toteż należy unikać dzielenia nie tylko przez zero, lecz także przez wartości bliskie zero. Z tego względu w dalszych rozważa­niach przyjmiemy, że wartość bezwzględna miano­wnika w powyż­szych wzorach nie może być niższa od ½.10-8. Użytko­wnik może określić inną minimalną wartość dodatnią jako istotnie różną od zera.

Moduł obliczeniowy

Opisany w języku matematycznym algorytm wyzna­czania prostej, która najlepiej przybliża dany zbiór punktów w sensie aproksy­macji średniokwa­dratowej, wyrazimy w postaci klasy statycznej LinApr. Dostępna w niej metoda Aproksymacja będzie zawierać pięć parame­trów (argu­mentów): x i y – tablice współrzę­dnych punktów, a i b – współczyn­niki szukanej prostej, eps – minimalna wartość dodatnia trakto­wana jako istotnie różna od zera. Pierwsze dwa parametry metody reprezen­tują tablice jednowy­miarowe, muszą więc być typu referen­cyjnego double[]. W kolejnych dwóch parame­trach ma być przekazany do miejsca jej wywołania wynik obliczeń, toteż poprze­dzamy je słowem kluczowym out oznacza­jącym, że są przeka­zywane przez referencję. Ostatni parametr określa minimalną wartość dodatnią miano­wnika, która ma być traktowana w oblicze­niach jako różna od zera (wartości niższe wskazują na układ osobliwy – zob. wzory powyżej). Jest to tzw. parametr domyślny. Jeśli nie zostanie podany, jego wartością będzie stała aprEPS równa 0.5e-8. Kompletny kod źródłowy klasy LinApr zawiera­jący komentarze XML z informacjami wyświe­tlanymi w podpo­wiedziach systemu IntelliSence może wyglądać następująco:

using System;

namespace Lin.Apr
{
    /// <summary>
    /// Liniowa aproksymacja średniokwadratowa
    /// </summary>
    static public class LinApr
    {
        /// <summary>
        /// Wyznaczanie prostej aproksymującej punkty
        /// </summary>
        /// <param name="x">Współrzędne x punktów</param>
        /// <param name="y">Współrzędne y punktów</param>
        /// <param name="a">Współczynnik prostej y = ax + b</param>
        /// <param name="b">Współczynnik prostej y = ax + b</param>
        /// <param name="eps">Minimalna wartość istotnie różna od zera</param>
        static public void Aproksymacja(double[] x, double[] y,
                                        out double a, out double b,
                                        double eps = aprEPS)
        {
            int n = x.Length;
            double sx = 0, sy = 0, sxx = 0, sxy = 0, det;
            for (int k = 0; k < n; k++)
            {
                sx += x[k];
                sy += y[k];
                sxx += x[k] * x[k];
                sxy += x[k] * y[k];
            }
            if (Math.Abs(det = sxx * n - sx * sx) < eps)
                throw new Exception("Dzielenie przez zero (układ osobliwy).");
            a = (sxy * n - sx * sy) / det;
            b = (sxx * sy - sx * sxy) / det;
        }
        /// <summary>
        /// Domyślna minimalna wartość istotnie różna od zera
        /// </summary>
        public const double aprEPS = 0.5e-8;
    }
}

Rzeczywisty i ekranowy układ kartezjański

Zazwyczaj rysunek jest definiowany w rzeczy­wistym układzie współrzę­dnych kartezjań­skich. W celu zobrazo­wania go na ekranie urządzenia grafi­cznego konieczne jest przejście od tego układu rzeczywi­stego do układu ekranowego. Operacja ta, zwana okienkowaniem (ang. windowing), dotyczy dwóch obszarów prosto­kątnych o bokach równole­głych do osi współrzę­dnych: okna w układzie rzeczy­wistym (rys. poniżej z lewej) i widoku (ang. viewport) w układzie ekra­nowym (rys. poniżej z prawej). Często widok nie zajmuje całego ekranu. Na przykład może być otoczony ramką lub może być oprócz niego wyświe­tlana dodatkowa infor­macja tekstowa, tabelka lub inny element graficzny.

Załóżmy, że okno w układzie rzeczywistym jest określone warto­ściami grani­cznymi xRmin, xRmax, yRminyRmax, zaś widok w układzie ekranowym warto­ściami xEmin, xEmax, yEminyEmax. Wówczas odwzoro­wanie opisujące przejście od współrzę­dnych (x,y) okna do współrzę­dnych (xE,yE) widoku ma postać:

gdzie sx i sy są tzw. czynnikami skalują­cymi spełnia­jącymi zależności

Zakres współrzędnych

Zobrazowanie rysunku na ekranie monitora kompute­rowego wymaga określenia mini­malnych i maksy­malnych wartości współrzę­dnych okna w układzie rzeczy­wistym i widoku w układzie ekranowym. Wyzna­czanie ograniczeń xRmin, xRmax, yRminyRmax okna rozpoczy­namy od iteracji przebiega­jącej wszystkie punkty:

xRmin = xRmax = x[0];
yRmin = yRmax = y[0];
for (int k = 1; k < n; k++)
{
    if (x[k] < xRmin)
        xRmin = x[k];
    else if (x[k] > xRmax)
        xRmax = x[k];
    if (y[k] < yRmin)
        yRmin = y[k];
    else if (y[k] > yRmax)
        yRmax = y[k];
}

Kierując się względami estetycznymi, przesuwamy nieco (np. o 1/20 szerokości okna) lewy brzeg okna w lewo i jego prawy brzeg w prawo, aby końce odcinka prostej aproksy­mującej punkty wystawały poza ich zakres wzdłuż osi x. Korekty może również wymagać dolne i górne ograni­czenie okna, bowiem końce odcinka prostej nie powinny znajdować się poza zakresem okna wzdłuż osi y:

d = 0.05 * (xRmax - xRmin);
xRmin -= d;
xRmax += d;
if ((yPoc = a * xRmin + b) < yRmin)
    yRmin = yPoc;
else if (yPoc > yRmax)
    yRmax = yPoc;
if ((yKon = a * xRmax + b) < yRmin)
    yRmin = yKon;
else if (yKon > yRmax)
    yRmax = yKon;

Nie jest to jednak koniec korekty granic okna, ponieważ nie możemy zaakce­ptować jego zerowej wysokości, która wiodłaby do nadmiaru przy obli­czaniu czynnika skalują­cego sy. Przypadek taki zachodzi wtedy, gdy wszystkie punkty mają tę samą współ­rzędną y. Jeśli jest ona istotnie różna od zera, rozsze­rzamy okno w górę i w dół o jej wartość bezwzględną, w przeci­wnym razie rozsze­rzamy okno w obu tych kierun­kach o stałą aprEPS wyrażającą najmniejszą wartość dodatnią trakto­waną jako istotnie różną od zera:

if (yRmax - yRmin < LinApr.aprEPS)
{
    d = Math.Max(Math.Abs(yRmax), LinApr.aprEPS);
    yRmax += d;
    yRmin -= d;
}

Ograniczenia xEmin, xEmax, yEminyEmax widoku możemy łatwo określić, znając rozmiar obszaru robo­czego formu­larza (okna apli­kacji). Mając ponownie na uwadze względy estety­czne i odwrotny kierunek osi y obszaru roboczego, brzegi widoku możemy wyzna­czyć następująco:

xEmin = ClientSize.Width / 20;
xEmax = ClientSize.Width - xEmin;
yEmax = ClientSize.Height / 20;
yEmin = ClientSize.Height - yEmax;

Te wyliczenia byłyby poprawne, gdyby obszar roboczy nie zawierał żadnych elementów grafi­cznego interfejsu użytko­wnika (GUI). W przy­padku aplikacji, którą zamierzamy zbudować, jej okno będzie zawierało u góry menu (kompo­nent MenuStrip) i na dole pasek statusu (kompo­nent StatusStrip). Powyższy fragment kodu należy więc zmodyfi­kować, gdyż wymaga uwzglę­dnienia wyso­kości obu tych obiektów zajmu­jących część obszaru roboczego.

Projekt formularza

Aplikacja ma wczytać liczbę punktów i ich współ­rzędne z pliku teksto­wego wskaza­nego przez użytko­wnika, wyzna­czyć prostą przybliża­jącą te punkty w sensie aproksy­macji średniokwa­dratowej, wyświe­tlić współczyn­niki tej prostej oraz narysować ją i aproksy­mowane przez nią punkty. Naturalnym wydaje się więc, aby właści­wości Text formu­larza apli­kacji nadać wartość Aproksy­macja średniokwa­dratowa, a właści­wości BackColor wartość Window (białe tło obszaru robo­czego okna zamiast jasno­szarego).

Komunikacja aplikacji z użytkownikiem powinna odbywać się za pośredni­ctwem inter­fejsu grafi­cznego. Jego budowę rozpoczy­namy od wstawienia do formu­larza kompo­nentów MenuStrip (menu główne), StatusStrip (pasek statusu) i OpenFile­Dialog (okno dialo­gowe otwie­rania pliku). Środo­wisko umieszcza je na pasku podrę­cznym pod formu­larzem (rys.), gdyż należą one do kompo­nentów niewizu­alnych. Jedno­cześnie na formu­larzu poja­wiają się dwa jasno­szare puste paski – menu u góry i statusu na dole.

Edycję menu zaczynamy od zaznaczenia komponentu menuStrip1 na pasku podręcznym lub kliknięcia na pustym pasku menu. Następnie tworzymy dwie podsta­wowe pozycje menu, wpisując w dwóch polach z napisem Wpisz tutaj tekst &Plik i &Koniec (rys.). Znak & powoduje dodanie podkre­ślenia pod literami P i K, co podczas działania aplikacji umożliwi użytko­wnikowi przejście do tych pozycji za pomocą skrótów klawiatu­rowych Alt+P i Alt+K. Pierwsza będzie powo­dować wyświe­tlanie okna dialo­gowego wyboru pliku i odczyt zawartych w nim danych, zaś druga kończenie wyko­nania programu. Środowisko nazwie je plikTool­StripMenuItemkoniecTool­StripMenuItem.

Pasek statusu służy do wyświetlania ważnych informacji w trakcie wykony­wania programu. Podobnie jak w przy­padku menu, możemy po zazna­czeniu komponentu statusStrip1 lub kliknięciu na pasku statusu umieścić na nim różne elementy składowe, wybierając je z listy rozwi­janej (rys.). Ograniczymy się tylko do dwóch etykiet StatusLabel, które środo­wisko nazwie toolStrip­StatusLabel1toolStrip­StatusLabel2. Właści­wości BackColor obu etykiet nadajemy wartość Control (jasno­szare tło zamiast białego). Ponadto właści­wość Text pierwszej etykiety usta­wiamy na y=ax+b (równanie prostej), zaś drugiej na Brak danych (wstępnie nie ma danych, ale po wczy­taniu pliku i wyzna­czeniu prostej zostaną w tym miejscu wyświe­tlone jej współczyn­niki albo infor­macja o błędzie, gdy pojawi się problem z uzyska­niem rozwią­zania). Pionowe wgłę­bienie rozdzie­lające etykiety jest efektem usta­wienia właści­wości BorderSides (strony obramo­wania) pierwszej z nich na Right (prawa) i BorderStyle (styl obramo­wania) na Etched (wyryty).

Zaprojektowany formularz jest pokazany na poniż­szym rysunku. Ponieważ nie zmienia­liśmy jego właści­wości FormBor­derStyle (styl obramo­wania formu­larza), ma ona wartość Sizable, która oznacza, że okno aplikacji będzie można maksyma­lizować, będzie też można zmieniać jego rozmiary za pomocą myszy lub klawiatury.

Na koniec przejdźmy do komponentu openFileDialog1 reprezen­tującego okno dialo­gowe wyboru pliku wejścio­wego. Jego właści­wość FileName (nazwa pliku) pozosta­wiamy pustą, usuwając propono­waną przez system nazwę, natomiast właści­wość Filter (filtr) ustawiamy tak, by ograni­czyć dostęp tylko do plików pewnych rodzajów. Chociaż w zasadzie aplikacja ma czytać pliki tekstowe (*.txt), możemy zwycza­jowo dopuścić również wszystkie pliki (*.*). Wymagania te określa poniższy tekst stano­wiący wartość właści­wości Filter:

Pliki tekstowe (*.txt)|*.txt|Wszystkie pliki (*.*)|*.*

Program w C#

Przystępujemy do rozbudowy wygenero­wanego przez środo­wisko C# kodu źródło­wego klasy Form1 zawartego w pliku Form1.cs. Zaczynamy od dodania pliku LinApr.cs do projektu programu oraz włączenia przestrzeni nazw System.IO (stru­mienie w C#) i Lin.Apr (moduł oblicze­niowy liniowej aproksy­macji średniokwa­dratowej) za pomocą dyrektywy using. Dekla­rujemy również szereg pól klasy Form1:

private double[] x = null, y = null;          // Tablice współrzędnych punktów
private double   a, b;                        // Współczynniki prostej y = ax + b
private double   xRmin, xRmax, yRmin, yRmax;  // Parametry układu rzeczywistego
private int      xEmin, xEmax, yEmin, yEmax;  // Parametry układu ekranowego
private double   yPoc, yKon;                  // Wartości końcowe y = ax + b
private double   sx, sy;                      // Czynniki skalujące

Całą pracę aplikacji będzie wykonywać metoda obsługi zdarzenia Click pole­cenia Plik menu, bowiem rola pole­cenia Koniec spro­wadza się jedynie do zamknięcia okna i zakoń­czenia programu. Gdy użytko­wnik wybierze plik wejściowy, metoda ma wczytać z niego współ­rzędne punktów, znaleźć współczyn­niki prostej przybliża­jącej te punkty i wyświe­tlić je oraz nary­sować tę prostą i aproksy­mowane przez nią punkty. Oto wstępna wersja tej metody:

private void plikToolStripMenuItem_Click(object sender, EventArgs e)
{
    if (openFileDialog1.ShowDialog() == DialogResult.OK)
    {
        // Czytaj dane z pliku openFileDialog1.FileName
        // Oblicz współczynniki prostej y = ax + b
        // Wyznacz zakres okna w układzie rzeczywistym
        Invalidate();
    }
}

Wywołanie metody ShowDialog komponentu OpenFileDialog powoduje wyświe­tlenie modalnego okna dialo­gowego, w którym użytko­wnik może wybrać plik (okno modalne nie pozwala na przejście do innego okna apli­kacji, dopóki nie zostanie zamknięte). Gdy użytko­wnik potwierdzi wybór, metoda zwróci wartość OK typu wylicze­niowego DialogResult, która oznacza, że należy wczytać dane z pliku i wykonać obliczenia. Nazwę pliku określa właści­wość FileName.

Aby kod metody plikToolStrip­MenuItem_Click zbytnio się nie rozrósł, wydzielimy z niej trzy metody pomocnicze. Pierwsza ma czytać plik. Zakładamy, że w jego pierwszym wierszu podana jest liczba punktów, zaś w nastę­pnych wierszach po dwie współ­rzędne x i y kolejnych punktów oddzie­lone co najmniej jedną spacją. Kod tej metody może wyglądać następująco:

private void CzytajDane(StreamReader plik)
{
    int n = int.Parse(plik.ReadLine().Trim());
    if (n < 2)
        throw new Exception("Wymagane co najmniej dwa punkty.");
    x = new double[n];
    y = new double[n];
    for (int k = 0; k < n; k++)
    {
        string s = plik.ReadLine().Trim();
        int p = s.IndexOf(' ');
        x[k] = double.Parse(s.Substring(0, p));
        y[k] = double.Parse(s.Substring(p + 1));
    }
}

Zadaniem dwóch metod pomocni­czych, które nazwiemy MinMaxKorekta, jest wyzna­czenie zakresu współrzę­dnych okna w układzie rzeczy­wistym. Pierwsza znajduje mini­malne i maksy­malne wartości współrzę­dnych wczyta­nych punktów (wartości pól xRmin, xRmax, yRminyRmax), zaś druga koryguje je, uwzglę­dniając końce kreślo­nego odcinka prostej (wartości pól xRminyPoc oraz xRmaxyKon). Operacje czytania pliku i obliczania współczyn­ników prostej mogą uruchomić wyjątek, toteż pełne sformuło­wanie metody obsługi zdarzenia Click polecenia Plik ma postać:

private void plikToolStripMenuItem_Click(object sender, EventArgs e)
{
    if (openFileDialog1.ShowDialog() == DialogResult.OK)
    {
        try
        {
            using (StreamReader plik = new StreamReader(openFileDialog1.FileName))
                CzytajDane(plik);
            LinApr.Aproksymacja(x, y, out a, out b);
            MinMax();
            Korekta();
            Invalidate();
        }
        catch (Exception ex)
        {
            x = y = null;
            Invalidate();
            toolStripStatusLabel2.Text = "Błąd danych";
            MessageBox.Show(ex.Message, "Wyjątek");
        }
    }
}

Użycie instrukcji using daje gwarancję, że po zakoń­czeniu lub przerwaniu operacji czytania pliku zasoby systemowe z nim związane zostaną zwolnione. Wyjaśnijmy jeszcze, że gdy w przy­padku wyjątku pola x i y zawie­rają referencje do tablic współrzę­dnych, przypisanie im wartości null jest dla mecha­nizmu garbage collection (odśmie­cacz pamięci) wska­zówką, że obiekty te nie są już potrzebne, więc może je zniszczyć (zwolnić przydzie­loną im pamięć). Ogólna infor­macja o wyjątku pojawia się na pasku statusu, a bardziej szcze­gółowa w oknie modalnym komuni­katu wyświe­tlonym przez metodę Show klasy MessageBox (rys.).

Narysowanie prostej i aproksymowanych przez nią punktów wymaga uprze­dniego określenia zakresu współrzę­dnych widoku w układzie ekranowym (wartości pól xEmin, xEmax, yEminyEmax). Obliczenia te najwygo­dniej jest umieścić w metodzie obsługi zdarzenia Load formu­larza, które jest wyzwalane, gdy okno zostało załadowane do pamięci i ma być po raz pierwszy wyświe­tlone na ekranie. Jest oczywiste, że parametry widoku powinny być również wyznaczane przy każdej zmianie rozmiaru okna, czyli w metodzie obsługi zdarzenia Resize. Oto definicje tych metod:

private void Form1_Load(object sender, EventArgs e)
{
    xEmin = ClientSize.Width / 20;
    xEmax = ClientSize.Width - xEmin;
    int d = (ClientSize.Height - menuStrip1.Height - statusStrip1.Height) / 20;
    yEmin = ClientSize.Height - statusStrip1.Height - d;
    yEmax = menuStrip1.Height + d;
}

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

Przejdźmy wreszcie do sformułowania metody obsługi zdarzenia Paint formu­larza. Oczywi­stym ułatwie­niem będzie posłużenie się dwiema metodami pomocni­czymi określa­jącymi przejście od współrzę­dnych okna w układzie rzeczywi­stym do współrzę­dnych widoku w układzie ekranowym (zob. p. Rzeczy­wisty i ekra­nowy układ karte­zjański):

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

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

Naturalnie rysowanie prostej i punktów jest możliwe, gdy tablice współrzę­dnych tych punktów nie są puste. Wówczas w pierwszej kolej­ności obliczamy czynniki skalujące używane przez metody pomocnicze, następnie rysujemy odcinek prostej i aproksy­mowane przez nią punkty, a na koniec wyświe­tlamy na pasku statusu współczyn­niki tej prostej, przypi­sując właści­wości Text jego drugiej etykiety stosowny łańcuch sformatowany:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    if (x != null)
    {
        sx = (xEmax - xEmin) / (xRmax - xRmin);
        sy = (yEmax - yEmin) / (yRmax - yRmin);
        e.Graphics.DrawLine(Pens.Blue, xEmin, yE(yPoc), xEmax, yE(yKon));
        for (int k = 0; k < x.Length; k++)
        {
            int xp = xE(x[k]) - 2, yp = yE(y[k]) - 2;
            e.Graphics.FillEllipse(Brushes.Cyan, xp, yp, 4, 4);
            e.Graphics.DrawEllipse(Pens.DarkCyan, xp, yp, 4, 4);
        }
        toolStripStatusLabel2.Text = string.Format("a = {0:F5}; b = {1:F5}", a, b);
    }
}

Punkty są w powyższej metodzie Form1_Paint zobrazowane jako elipsy za pomocą metod FillEllipse (wypeł­nianie) i DrawEllipse (ryso­wanie) klasy Graphics. Określa się w nich, oprócz pędzla (koloru Cyan) w pierw­szej i pióra (koloru DarkCyan) w drugiej, lewy górny róg prosto­kąta opisanego na elipsie oraz jego szerokość i wysokość (obie równe 4).

Powyższe rozważania prowadzą do zaprezento­wanego poniżej kodu źródłowego programu w języku C#. W programie dodatkowo uwzglę­dniono zdarzenie FormClosing formu­larza, które jest wyzwalane tuż przez zamknięciem okna. Metoda Form1_Form­Closing obsługi tego zdarzenia wyświetla okno modalne z zapytaniem, czy rzeczy­wiście program ma być zakończony, oraz dwoma przyciskami TakNie. Gdy użytko­wnik naciśnie przycisk Tak, wyko­nanie programu zostaje zakoń­czone, a gdy przycisk Nie, właści­wość Cancel obiektu e klasy FormClosing­EventArgs zostaje ustawiona na true, co skutkuje anulowaniem zamknięcia okna i końca wykonania programu.

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.IO;
using Lin.Apr;

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

        private void CzytajDane(StreamReader plik)
        {
            int n = int.Parse(plik.ReadLine().Trim());
            if (n < 2)
                throw new Exception("Wymagane co najmniej dwa punkty.");
            x = new double[n];
            y = new double[n];
            for (int k = 0; k < n; k++)
            {
                string s = plik.ReadLine().Trim();
                int p = s.IndexOf(' ');
                x[k] = double.Parse(s.Substring(0, p));
                y[k] = double.Parse(s.Substring(p + 1));
            }
        }

        private void MinMax()
        {
            xRmin = xRmax = x[0];
            yRmin = yRmax = y[0];
            for (int k = 1; k < x.Length; k++)
            {
                if (x[k] < xRmin)
                    xRmin = x[k];
                else if (x[k] > xRmax)
                    xRmax = x[k];
                if (y[k] < yRmin)
                    yRmin = y[k];
                else if (y[k] > yRmax)
                    yRmax = y[k];
            }
        }

        private void Korekta()
        {
            double d = 0.05 * (xRmax - xRmin);
            xRmin -= d;
            xRmax += d;
            if ((yPoc = a * xRmin + b) < yRmin)
                yRmin = yPoc;
            else if (yPoc > yRmax)
                yRmax = yPoc;
            if ((yKon = a * xRmax + b) < yRmin)
                yRmin = yKon;
            else if (yKon > yRmax)
                yRmax = yKon;
            if (yRmax - yRmin < LinApr.aprEPS)
            {
                yRmax += (d = Math.Max(Math.Abs(yRmax), LinApr.aprEPS));
                yRmin -= d;
            }
        }

        private void plikToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    using (StreamReader plik = new StreamReader(openFileDialog1.FileName))
                        CzytajDane(plik);
                    LinApr.Aproksymacja(x, y, out a, out b);
                    MinMax();
                    Korekta();
                    Invalidate();
                }
                catch (Exception ex)
                {
                    x = y = null;
                    Invalidate();
                    toolStripStatusLabel2.Text = "Błąd danych";
                    MessageBox.Show(ex.Message, "Wyjątek");
                }
            }
        }

        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) != DialogResult.Yes)
                e.Cancel = true;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            xEmin = ClientSize.Width / 20;
            xEmax = ClientSize.Width - xEmin;
            int d = (ClientSize.Height - menuStrip1.Height - statusStrip1.Height) / 20;
            yEmin = ClientSize.Height - statusStrip1.Height - d;
            yEmax = menuStrip1.Height + d;
        }

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

        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 (x != null)
            {
                sx = (xEmax - xEmin) / (xRmax - xRmin);
                sy = (yEmax - yEmin) / (yRmax - yRmin);
                e.Graphics.DrawLine(Pens.Blue, xEmin, yE(yPoc), xEmax, yE(yKon));
                for (int k = 0; k < x.Length; k++)
                {
                    int xp = xE(x[k]) - 2, yp = yE(y[k]) - 2;
                    e.Graphics.FillEllipse(Brushes.Cyan, xp, yp, 4, 4);
                    e.Graphics.DrawEllipse(Pens.DarkCyan, xp, yp, 4, 4);
                }
                toolStripStatusLabel2.Text = string.Format("a = {0:F5}; b = {1:F5}", a, b);
            }
        }

        private double[] x = null, y = null;        // Tablice współrzędnych punktów
        private double a, b;                        // Współczynniki prostej y = ax + b
        private double xRmin, xRmax, yRmin, yRmax;  // Parametry układu rzeczywistego
        private int xEmin, xEmax, yEmin, yEmax;     // Parametry układu ekranowego
        private double yPoc, yKon;                  // Wartości końcowe y = ax + b
        private double sx, sy;                      // Czynniki skalujące
    }
}

Wynik wykonania programu dla przykła­dowego pliku Punkty2.txt zawiera­jącego współ­rzędne dziesięciu punktów jest pokazany na poniż­szym rysunku. Warto odno­tować, że plik ten różni się od pliku Punkty.txt dla analogi­cznego programu w C++ jedynie użyciem przecinka zamiast kropki w roli separa­tora dziesię­tnego w zapisie liczb rzeczy­wistych.


Opracowanie przykładu: luty 2019