Preview only show first 10 pages with watermark. For full document please download

Pascal Lazarus Wstęp Do Programowania W Języku Object Pascal Z Użyciem środowiska Lazarus.

2016 Pascal Lazarus Wstęp do programowania w języku Object Pascal z użyciem środowiska Lazarus. Sławomir Marczyński Technical University of Szczecin Spis treści Wstęp... 3 Zamiana stopni Celsjusza

   EMBED

  • Rating

  • Date

    June 2018
  • Size

    3.5MB
  • Views

    6,426
  • Categories


Share

Transcript

2016 Pascal Lazarus Wstęp do programowania w języku Object Pascal z użyciem środowiska Lazarus. Sławomir Marczyński Technical University of Szczecin Spis treści Wstęp... 3 Zamiana stopni Celsjusza na stopnie Fahrenheita... 3 Dzielenie linii tekstu na wyrazy, komponent TMemo Program obliczający NWP i NWW Generowanie liczb pseudolosowych i TStringGrid Czytanie i zapisywanie danych: pliki tekstowe Czytanie i pisanie danych: używanie read i write Strumienie danych... 32 Wstęp Pascal jako język programowania został opracowany w 1971 roku przez Niklausa Wirtha, na szwajcarskiej Politechnice Federalnej w Zurychu. Miał to być język do nauki programowania. Istniały już wtedy różne inne języki programowania, ale były one zwykle zbyt skomplikowane dla początkujących. Prawie pół wieku później Pascal jest nadal używany do uczenia programowania. Komputery zmieniały się: od dużych mainframe przez PC-ty z lat 90-tych do dzisiejszych tabletów i mikrokontrolerów. Pascal też ewoluował. Powstawały jego odmiany i dialekty, w tym Object Pascal spopularyzowany jako Delphi. Ucząc się programować w Delphi (darmowe środowisko Pascal Lazarus jest kompatybilne z Delphi, patrz będziemy używać rzeczy jakie nie istniały w pierwotnym Pascalu: obiektów, interfejsu graficznego, komponentów. Delphi/Lazarus to środowisko programistyczne (IDE) przeznaczone do szybkiego tworzenia aplikacji (RAD) poprzez składanie ich z gotowych komponentów. Dlatego znaczna część pracy nie polega na pisaniu linijek programu. Gdy program jest aplikacją z interfejsem graficznym (GUI), to naszym zadaniem będzie napisanie instrukcji wywoływanych w odpowiedzi na określone zdarzenia: naciśnięcie guzika, zmianę rozmiaru okna, ruch myszy. IDE samo wygeneruje część kodu źródłowego. Dlatego w naszych programach nie będziemy pisać słowa kluczowego program, od którego zaczynają się programy w Pascalu. Ograniczoną przydatność będą miały też read, readln, write i writeln, które służą w Pascalu do czytania i pisania na konsoli. Zamiana stopni Celsjusza na stopnie Fahrenheita W Polsce powszechnie używa się stopni Celsjusza do określania temperatury. W USA jest używana skala Fahrenheita. Nasza skala dzieli na sto stopni przedział od temperatury zamarzania wody do temperatury wrzenia (w standardowych warunkach). Amerykańska za zero przyjmuje temperaturę mieszaniny topniejącego śniegu z chlorkiem amonu, za sto temperaturę zbliżoną do temperatury ludzkiego ciała. Aby przeliczyć temperaturę ze stopni Celsjusza na stopnie Fahrenheita należy pomnożyć wartość wyrażoną w stopniach Celsjusza przez 9, potem podzielić przez 5 i dodać 32. Możemy to zapisać jako wzór matematyczny, w pseudokodzie lub rozrysować na schemacie blokowym (norma ISO 5807:1985). Rysunek 1. Schemat blokowy konwersji ze stopni Celsjusza na stopnie Fahrenheita. Gdy uruchomimy IDE Lazarusa, to zobaczymy (rysunek poniżej) kilka okienek: u góry paletę z komponentami, inspektora obiektów, edytora źródeł i Form1 (czyli projekt jak będzie wyglądać okno naszej aplikacji). Rysunek 2. Środowisko Lazarus Pascal nowy pusty projekt. Wstawiamy z palety komponentów do Form1 dwie etykiety TLabel, dwa pola TEdit, jeden przycisk TButton i rozmieszczamy je tak jak pokazane jest to na kolejnym rysunku. Używając inspektora obiektów zmieniamy stopień kroju pisma (wielkość czcionek), tło komponentów TEdit, napisy, zmieniamy Caption (czyli tytuł) całego okienka z Form1 na Przeliczanie temperatur (Rysunek 4.). Jeżeli chcemy układ komponentów np. przesłać jako możemy (po zapisaniu projektu) odnaleźć plik unit1.lfm (Lazarus) lub unit1.dfm (Delphi), w którym to co robiliśmy jest czytelnie zapisane jako zwykły tekst. Rysunek 3. Komponenty dodane do Form1. Rysunek 4. Odpowiednie ułożenie i właściwości komponentów. Teraz zaczynamy prawdziwe programowanie. Klikamy dwukrotnie na przycisk Button1 (czyli ten z napisem Oblicz ) umieszczony na Form1. (Zauważmy, że zmiana captionów nie zmienia ani nazw komponentów w inspektorze obiektów, ani zmiennych reprezentujących te komponenty w programie. Po prostu napis na obiekcie nie jest już taki sam jak nazwa obiektu.) W edytorze źródeł (Rysunek 5.) automatycznie utworzona zostanie procedura TForm1.Button1Click. W inspektorze obiektów, zakładka Zdarzenia, przy zdarzeniu OnClick będzie dopisane Button1Click. Rysunek 5. Tworzenie obsługi zdarzenia OnClick dla guzika Button1. Co teraz chcemy zrobić? Chcemy, aby (gdy naciśnięty został przycisk Button1) tekst z Edit1 przeczytać jako liczbę, wykonać obliczenia, wynik wpisać jako tekst w Edit2. Potrzebne nam będą: czytanie i pisanie tekstu z komponentu TEdit; zamiana tekstu na liczbę; zamiana liczby na tekst. Tekst w okienku TEdit jest właściwością Text. Czyli tekst w okienku Edit1 to Edit1.Text, tekst w okienku Edit2 to Edit2.Text. Konwersje pomiędzy napisem a liczbą z przecinkiem dziesiętnym robi się w Delphi funkcjami StrToFloat i FloatToStr. Możemy więc napisać swoją linijkę programu pomiędzy a end: procedure TForm1.Button1Click(Sender: TObject); Edit2.Text := FloatToStr( StrToFloat(Edit1.Text) * 9.0/ ) Gdy uruchomimy program wszystko będzie już działać jeżeli tylko ktoś zamiast temperatury w stopniach Celsjusza nie wpisze Bo wtedy konwersja StrToFloat się nie uda i cały program przestanie pracować prawidłowo. Co możemy zrobić? Użyjemy bloku try-except-end, tak jak to jest napisane poniżej: procedure TForm1.Button1Click(Sender: TObject); try Edit2.Text := FloatToStr(StrToFloat(Edit1.Text) * 9.0 / ) except Edit2.Text := '???' Pomiędzy try a except umieszczamy to co jest naszym planem A. Jeżeli będzie jakiś problem i powstanie sytuacja wyjątkowa, to przerywana jest realizacja planu A, aby natychmiast przejść do zapasowego planu B (jaki jest pomiędzy except a odpowiednim end). Niezależnie od tego co się zdarzy program będzie mógł działać dalej. (Gdy uruchamiamy program bezpośrednio z IDE i tak pojawi się ostrzeżenie jako wyskakujące okienko. Możemy je zignorować i wybrać opcję by nie pokazywało się więcej.) Rysunek 6. Prawie gotowy program uruchomiony przez IDE. Spróbujmy nasz program ulepszyć. Po pierwsze, stan programu zaraz po uruchomieniu, ale jeszcze przed naciśnięciem guzika. Obiekty Edit1 i Edit2 pokazują wtedy teksty Edit1 i Edit2, czyli zupełnie nie to co trzeba. Po drugie, zautomatyzujmy obliczenia tak, aby nie trzeba było naciskać przycisku Button1. Moglibyśmy użyć inspektora obiektów i usunąć napisy Edit1 i Edit2 całkowicie lub wpisując odpowiednie wartości. Zrobimy jednak co innego: klikniemy dwukrotnie na Form1, czyli samo projektowane okienko jako takie. W edytorze źródeł powstanie procedura Form1.FormCreate, w którą, pomiędzy i end, przez kopiuj-wklej wstawiamy to samo co jest już w procedurze Form1.Button1Click dopisując na początku jeszcze jedną linię: Edit1.Text := FloatToStr(36.6); Rysunek 7. Obsługa zdarzenia OnCreate dla Form1. Co się teraz stanie gdy uruchomimy program? Gdy będzie tworzone okno Form1, wtedy okienko Edit1 pokaże wartość 36,6, a okienko Edit2 to co pokazałoby po naciśnięciu guzika Button1. Czy naciskanie guzika Button1 jest w ogóle potrzebne? Jeżeli przeglądając zdarzenia jakie może obsłużyć Edit1 znajdziemy OnChange. Jeżeli go użyjemy (klikamy na wielokropek w inspektorze obiektów) i wstawimy do TForm1.Edit1Change dokładnie to samo co jest już w TForm1.Button1Click, to nie trzeba będzie klikać na guzik Button1 (czyli podpisany Oblicz ), bo obliczenia będą robiły się automatycznie przy jakiejkolwiek zmianie zawartości kontrolki Edit1. Dlatego możemy: usunąć przycisk Button1 jako już niepotrzebny, skasować całą procedurę TForm1.Button1Click, usunąć cały blok z try-except-end z procedury TForm1.FormCreate. Jak to możliwe, że program będzie działał zupełnie dobrze? Przy starcie zostanie wywołane FormCreate, ono zmieni zawartość Edit1, a zmiana zawartości Edit1 automatycznie aktualizuje Edit2. Możemy jeszcze zmienić (przez inspektora obiektów) właściwość ReadOnly obiektu Edit2 ustawiając ją na true. Cały plik unit1.pas będzie wyglądał wtedy tak jak widać (Rysunek 9.). Rysunek 8. Obsługa zdarzenia OnChange dla Edit1. Rysunek 9. Ostateczna wersja programu, od implementation do end. Rysunek 10. Zmiana nazwy dla gotowego programu na CesiusToFahrenheit. Domyślnie nazwa projektu to project1, a wykonywalnego pliku project1.exe. Można to zmienić (Rysunek 10), na przykład na CelsiusToFahrenheit. Opłacalne może też być wyłączenie w opcjach debugowania zmniejszy to bardzo znacznie rozmiar gotowego pliku wykonywalnego (CelsiusToFahrenheit.exe). Choć możliwe jest skonfigurowanie konsolidatora inaczej, to domyślnie wszystkie biblioteki są łączone statycznie. Czyli cały nasz gotowy program będzie w jednym pliku. Jeżeli otworzymy folder z projektem, to powinniśmy łatwo ten plik (z rozszerzeniem.exe) znaleźć. A co jeżeli chcemy mieć program do przeliczania pomiędzy wieloma skalami temperatur? Bez wnikania w szczegóły warto rozplanować sobie architekturę takiego programu. Mając pięć różnych skal temperatur będziemy musieli umieć przeprowadzić 25 różnych konwersji, w tym pięć trywialnych. (Trywialne to np. konwersja stopni Celsjusza na stopnie Celsjusza.) Mając skal temperatur będzie to konwersji. Gdybyśmy jednak strukturę programu tak zaprojektowali, że wszystkie temperatury byłyby wewnętrznie reprezentowane w kelwinach, to różnych konwersji byłoby, czyli już przy pięciu skalach zaledwie 9. Gdy zamiast 100 różnych przeliczeń mielibyśmy 19 wzorów. Dla (tyle skal temperatur nie ma, ale jeżeli będziemy przeliczali kursy walut, to całkiem możliwe) mamy odpowiednio 10 tysięcy i 199. W naiwnym podejściu ilość pracy nad programem rośnie wraz z kwadratem liczby skal (ew. walut). Oszczędność pracy wynikająca z dobrej architektury programu jest oczywista. Dzielenie linii tekstu na wyrazy, komponent TMemo. Tworzymy nowy projekt aplikacji w Lazarusie, a w nim w Form1 z dwoma obiektami klasy TMemo i jednym przyciskiem TButton, tak jak na rysunku (Rysunek 11). Klikamy dwa razy na Button1, aby wstawić obsługę naciśnięcia guzika jako Button1Click. Chcemy w niej zaprogramować czytanie tekstu z Memo1 i przepisywanie go do Memo2. Czytać chcemy linia po linii, po każdej przepisanej całej linii chcemy wypisać (do Memo2) oddzielnie same wyrazy i podać ile ich było. Rysunek 11. Komponenty programu MemoLinesAndWords To co chcemy zrobić robimy w procedurze TForm1.Button1Click, której automatycznie utworzony szkielet wygląda tak: procedure TForm1.Button1Click(Sender: TObject); Będziemy potrzebowali zmiennych (obiekty Memo1 i Memo2 już mamy), w których będzie bieżąca linia tekstu, linia tekstu rozbita na wyrazy, jeden wyraz i licznik wyrazów. Nazwy zmiennych powinny być znaczące, zwyczajowo używa się języka angielskiego, lepiej dać dłuższe ale zrozumiałe nazwy. procedure TForm1.Button1Click(Sender: TObject); line: string; { linia tekstu nie podzielona na słowa } words: TStringList; { lista słów w jednej linii } word: string; { jedno słowo z listy słów, może być puste '' } wordcounter: integer; { licznik niepustych słów } Zmienne definiujemy po słowie kluczowym, podając nazwę zmiennej i po dwukropku nazwę typu. Typ string jest w Object Pascalu (Delphi) standardowym typem do przechowywania napisów. Typ TStringList jest czymś (klasą obiektów) co zaprojektowano do przechowywania wielu obiektów string jako listy. Typ integer to liczby całkowite, dodatnie lub ujemne, czyli ze znakiem. (Owszem, można w Pascalu użyć typu cardinal dla liczb dodatnich, ale niewiele na tym zyskamy w naszym programie.) Tekst w nawiasach klamrowych jest komentarzem. Moglibyśmy line i word zdefiniować w jednej linii, oddzielając je przecinkiem, ale trudniej byłoby wstawić komentarz. Trzymanie się zasady jedna zmienna, jedna linia ułatwia pracę nad programem. Chcemy przejść przez wszystkie linie tekstu jakie są wpisane w Memo1. Moglibyśmy po prostu dowiedzieć się z Memo1.Lines.Count ile tych linii jest i potem użyć zwykłej pętli for aby dostać się do linii od Memo1.Lines[0] do Memo1.Lines[Memo1.Lines.Count-1]. Ale jest bardziej eleganckie rozwiązanie: pętla po wszystkich elementach kolekcji: for line in Memo1.Lines do To co jest pomiędzy i end zostanie wykonane dla każdej line która jest w kolekcji linii Lines jaką ma Memo1. (Tak jak dla zwykłej pętli for można nie pisać i end jeżeli między a end byłaby tylko jedna instrukcja.) Aby podzielić line na wyrazy użyjemy words, czyli obiektu klasy TStringList. Najpierw musimy go utworzyć (wywołując Create) i skonfigurować. Potem wystarczy tylko wpisać do niego tekst instrukcją words.text := line; ale na wszelki wypadek resetujemy obiekt words dla każdej linii wywołując jego metodę Clear. Moglibyśmy co prawda tworzyć na nowo (Create) i niszczyć po użyciu obiekt dostępny przez words, ale byłoby to prawdopodobnie wolniejsze. words := TStringList.Create; { tworzymy pustą listę słów } words.linebreak := ' '; { konfigurujemy: znak rozdzielający słowa } for line in Memo1.Lines do words.clear; { na wszelki wypadek czyścimy listę słów } words.text := line; { wstawienie lini automatycznie tworzy listę słów } Mamy już listę słów, dodajmy do naszego programu wypisywanie całych linii i poszczególnych słów do Memo2. Memo2.Clear; { czyścimy Memo2, usuwamy to co było tam wcześniej } words := TStringList.Create; { tworzymy pustą listę słów } words.linebreak := ' '; { konfigurujemy: znak rozdzielający słowa } for line in Memo1.Lines do Memo2.Lines.Add(' ' + line); { dopisujemy kolejną linię tekstu do Memo2 } words.clear; { na wszelki wypadek czyścimy listę słów } words.text := line; { wstawienie lini automatycznie tworzy listę słów } for word in words do Memo2.Lines.Add(word); Jeżeli uruchomimy program, to zauważymy, że działa on źle: pomija ostatnie słowo w linii; gdy pomiędzy słowami jest odstęp większy niż jedna spacja wypisuje puste linie z pustymi słowami. Aby to skorygować dopisujemy do line dodatkową spację na końcu oraz sprawdzamy instrukcją warunkową czy łańcuch word jest niepusty. Dodajemy też zliczanie słów. Możemy też zamienić Memo2.Lines.Add(word) na Memo2.Append(word) itd. Ponieważ listę słów utworzyliśmy dynamicznie (przez TStringList.Create) to musimy ją sami skasować, najlepiej używając FreeAndNil(words), tak jak to robimy. FreeAndNil nie tylko skasuje obiekt wskazywany przez words, ale zapewni też że potem words będzie bezpiecznie wskazywało na nil, czyli na nic, dzięki czemu nigdy potem nie popełnimy błędu polegającego na wywołaniu obiektu który został już skasowany. Pozostawienie dynamicznie tworzonego obiektu w pamięci, jeżeli nie mamy automatycznego odśmiecania pamięci (standardowo Delphi i C++ nie ma, Java i C# ma), prowadzi do tzw. wycieku pamięci: program potrzebuje coraz więcej pamięci, która marnuje się w zapomnianych, ale ciągle przydzielonych blokach. Ostatecznie otrzymujemy moduł w Object Pascalu, tzw. unit, taki jak poniżej. unit Unit1; {$mode objfpc}{$h+} interface uses Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls; type { TForm1 } TForm1 = class(tform) Button1: TButton; Memo1: TMemo; Memo2: TMemo; procedure Button1Click(Sender: TObject); procedure FormCreate(Sender: TObject); private { private declarations } public { public declarations } Form1: TForm1; implementation {$R *.lfm} { TForm1 } procedure TForm1.Button1Click(Sender: TObject); line: string; { linia tekstu nie podzielona na słowa } words: TStringList; { lista słów w jednej linii } word: string; { jedno słowo z listy słów, może być puste '' } wordcounter: integer; { licznik niepustych słów } Memo2.Clear; { czyścimy Memo2, usuwamy to co było tam wcześniej } words := TStringList.Create; { tworzymy pustą listę słów } words.linebreak := ' '; { konfigurujemy: znak rozdzielający słowa } for line in Memo1.Lines do wordcounter := 0; { zerujemy licznik słów w linii } Memo2.Append(' ' + line); { dopisujemy kolejną linię tekstu do Memo2 } words.clear; { na wszelki wypadek czyścimy listę słów } words.text := line + words.linebreak; { dopisujemy dodatkowy znak podziału } for word in words do { dla wszystkich słów z listy słów } if word '' then { jeżeli słowo nie jest puste } wordcounter := wordcounter + 1; { zlicz kolejne słowo } Memo2.Append(word); { wypisz słowo do okienka Memo2 } Memo2.Append(' liczba wyrazów = ' + IntToStr(wordCounter)); FreeAndNil(words); { bardzo ważne: usuwamy utworzony dynamicznie obiekt } procedure TForm1.FormCreate(Sender: TObject); Memo1.Clear; Memo2.Clear; end. Cały program działa całkiem dobrze (Rysunek 12.), ale takie dzielenie na słowa i ich zliczanie nie jest dla nas celem samym w sobie. Skoro potrafimy podzielić tekst na takie części, to możemy użyć naszego programu (po małych zmianach) do obliczeń: w lewe okno wpisywać będziemy liczby, w prawym będziemy mieli średnią arytmetyczną. Oczywiście, moglibyśmy liczyć nie tylko średnią arytmetyczną, ale także geometryczną, harmoniczną, odchylenie standardowe, kurtozę i cokolwiek byłoby nam potrzebne. Średnia to tylko przykład. Rysunek 12. Działający program MemoLinesAndWords. Rysunek 13. Modyfikacja programu obliczanie średniej arytmetycznej. Listing programu obliczającego średnią arytmetyczną procedura Button1Click: procedure TForm1.Button1Click(Sender: TObject); line: string; { linia tekstu nie podzielona na słowa } words: TStringList; { lista słów w jednej linii } word: string; { jedno słowo z listy słów, może być puste '' } numbercounter: integer; { licznik zliczający ile słów to liczby } Value: real; { wartość liczbowa } sum: real; { suma wszystkich wartości } mean: real; { średnia arytmetyczna } Memo2.Clear; { czyścimy Memo2, usuwamy to co było tam wcześniej } words := TStringList.Create; { tworzymy pustą listę słów } words.linebreak := ' '; { konfigurujemy: znak rozdzielający słowa } numbercounter := 0; sum := 0; for line in Memo1.Lines do words.clear; { na wszelki wypadek czyścimy listę słów } words.text := line + words.linebreak; { dopisujemy dodatkowy znak podziału } for word in words do { dla wszystkich słów z listy słów } if word '' then { jeżeli słowo nie jest puste } try Value := StrToFloat(word); { jak się nie uda to plan B } sum := sum + Value; { sumowanie wszystkich liczb } numbercounter := numbercounter + 1; except if numbercounter 0 then mean := sum / numbercounter; Memo2.Lines.Add('Przetworzono ' + IntToStr(numberCounter) + ' liczb'); Memo2.Lines.Add('Średnia arytmetyczna = ' + FloatToStr(mean)); end else Memo2.Lines.Add('Brak danych'); FreeAndNil(words); { bardzo ważne: usuwamy utworzony dynamicznie obiekt } procedure TForm1.FormCreate(Sender: TObject); Memo1.Clear; Memo2.Clear; end. Program obliczający NWP i NWW Dla dwóch liczb całkowitych, większych od zera, największy wspólny podzielnik (NWP) to największa liczba całkowita, która jest podzielnikiem jednocześnie obu danych liczb. Najmniejsza wspólna wielokrotność (NWW) to najmniejsza liczba dodatnia, która jest wielokrotnością obu danych liczb. Spróbujmy napisać w Delphi/Object Pascalu program obliczający NWP i NWW. Zaczynamy od utworzenia nowego folderu o nazwie GCDLCM do którego wrzucamy ikonkę cat.ico (z serwisu findicons.com z darmowymi ikonami). Nazwa folderu nie jest przypadkowa: po angielsku NWP to GCD, a NWW to LCM. Tworzymy nowy projekt1 w folderze GCDLCM, wybieramy ikonę cat.ico jako ikonę programu, zmieniamy nazwę aplikacji na GcdLcm i nazwę pliku wynikowego (target) też na GcdLcm (Rysunek 14.). Rysunek 14. Zmiana ikony programu. Rysunek 15. Projekt okna programu GcdLcd. Do Form1 wrzucamy cztery TLabel i cztery TEdit, zmieniamy teksty i rozmieszczamy tak jak na rysunku (Rysunek 15). Teraz bardziej ambitne zadanie: tworzymy jedną wspólną procedurę Form1.EditChange obsługi zdarzenia OnChange i podpinamy ją do obu kontrolek Edit1 i Edit2. W ten sposób zostanie ona wywołana niezależnie co i gdzie się zmieni w danych wejściowych. Procedura ta oczywiście ma obliczać NWP i NWW. Rysunek 16. Dodawanie istniejącej już funkcji do obsługi zdarzenia. Procedura TForm1.EditChange ma pobrać dane z obiektów Edit1 i Edit2, spróbować (przyda się tryexcept-end) przetworzyć je na liczby, wykonać właściwe obliczenia i wyniki zapi