Zadanie, które wykonamy w tym ćwiczeniu pozwoli Ci zapoznać się z elementami graficznego interfejsu użytkownika. Mówiąc inaczej ze wszystkimi rodzajami elementów, których używamy korzystając z różnych aplikacji. Są to m.in. pola tekstowe, listy rozwijane, menu, przyciski.
Pamiętaj! Ćwiczenie, które wykonamy wymaga kontrolek, które nie są dostarczone z podstawową wersją Greenfoot. Należy pobrać wersję dostarczoną przez CoderDojo. |
Na przykładzie naszego ulubionego Greenfoot-a przejrzyjmy listę kontrolek, które zapewne znasz z innych aplikacji.
Pole tekstowe
Checkbox
Lista wyboru
Menu górne
Menu kontekstowe
Suwak
Przyciski
To jednak tylko część elementów, każdy z nich ma inne właściwości i używa się ich tak, aby użytkownik aplikacji w jak najprostszy sposób mógł z niej korzystać.
Zapoznanie z Greenfoot GUI Components
Greenfoot posiada rozszerzenie stworzone przez Taylora Borna, które pozwala na używanie takich elementów we własnej aplikacji. Taką aplikację stworzymy właśnie w tym ćwiczeniu. Na początek przyjrzyjmy się oferowanym przez Taylora kontrolkom.
Wyszukaj w google frazy “Greenfoot GUI compoenents” lub wejdź na poniższy link [http://www.greenfoot.org/scenarios/7578]. Powinieneś zobaczyć opis rozszerzenia jak na poniższym obrazku.
Kliknij Open in Greenfoot. Zostanie pobrany plik o rozszerzeniu gfar, który możesz otworzyć w Greenfoot. Kliknij po prostu dwukrotnie na plik lub przez menu Greenfoot otwórz go wybierając kolejno Scenario -> Open, i wskaż na pobrany plik.
Po otwarciu powinieneś zobaczyć okno Greenfoot takie, jak na obrazku poniżej.
Teraz skompilujmy ten program. Uwaga! Ze względu na to, że w Greenfoocie, który otrzymaliście od nas, już dodaliśmy te klasy, pojawią się ostrzeżenia (kilkanaście), jak na poniższym obrazku. Kliknij po prostu przy każdym OK.
Po zamknięciu wszystkich wiadomości projekt się skompiluje i Twoim oczom powinno pojawić się okno takie, jak poniżej.
Poklikaj w aplikacji, zobacz jak działają poszczególne elementy GUI.
Jak możesz zauważyć, świat nazywa się GUI_DemoWorld. Świat, jak w każdej aplikacji, pozwala na dodawanie aktorów. W tym przypadku to kontrolki są aktorami.
Przeanalizujmy strukturę kontrolek.
- Wszystkie z nich dziedziczą z klasy GUI_Component, jest to klasa bazowa dla wszystkich kontrolek. Jeśli zajrzysz w jej dokumentację, dowiesz się jakie ma metody i na co pozwalają wszystkie kontrolki. Klasa ta pozwala m.in. na zmianę tła, czcionki, obramowania. Oznacza to, że wszystkie dziedziczące po niej kontrolki mają te metody i możesz ich używać.
- Następna klasa to WindowComponent. Jest klasą bazową dla wszystkich podstawowych elementów takich jak lista rozwijana (DropDownList), pole tekstowe (TextBox), przycisk (Button), hasło (Password), itd.
- Kolejna klasa bazowa to Window. Jest to klasa bazowa, z której dziedziczą klasy odpowiedzialne za wyświetlanie wyskakujących okien. Jeśli w przykładowej aplikacji wybierzesz Window Examples i któryś z elementów podmenu, to zobaczysz nowe okno.
Założenie aplikacji i stworzenie świata
Zapoznanie z klasą GUI_DemoWorld
W pierwszej kolejności przed przystąpieniem do stworzenia nowego projektu spójrzmy jak wygląda kod klasy GUI_DemoWorld. Pozwoli to na zapoznanie się z tym, jak autor widział wykorzystanie napisanych przez niego klas.
Otwórz klasę klikając na nią dwukrotnie.
Na początku klasy widać pola, które są kontrolkami. Umieszczono je tutaj, aby mogły być wykorzystane w całej klasie. Zobaczmy jak są inicjowane.
W konstruktorze, czyli w momencie tworzenia świata, tworzone są obiekty, a ich wartości są przypisane do pól.
String text = "A TextBox that ..."; txtB = new TextBox(new Point(250, 100), text, new Font("Helvetica", Font.PLAIN, 15));
Do pola txtB zostaje przypisane pole tekstowe, w które można wprowadzić własny tekst.
Następnie za pomocą polecenia
addObject(txtB, 470, 73);
pole tekstowe txtB jest dodawane do świata, aby je wyświetlić. Obiekt ten dodaje się do świata tak samo jak wszystkich aktorów, gdyż właśnie TextBox jest dzieckiem klasy Actor.
Jeśli klikasz na przyciski, możesz zauważyć, że wywołują one pewne akcje. Zobaczmy jeszcze jak to jest obsłużone. W metodzie act() zobaczysz taki oto kawałek kodu:
W pierwszej instrukcji sprawdzane jest, czy przycisk btnClick został kliknięty. Jeśli tak, to w następnym kroku sprawdzana jest aktualna akcja.
if (btnAction.getText().equals("Add Counter"))
Jeśli akcja ma polegać na dodaniu do licznika, to za pomocą takiej instrukcji w wyświetlanej etykiecie (label), wartość zostaje zwiększona o 1.
lblCounter.setText("" + (Integer.parseInt(lblCounter.getText()) + 1));
Uwaga! Projekt GUI_Components warto jest mieć otwarty, aby móc się na nim wzorować. Greenfoot może być uruchomiony w kilku oknach. |
Stworzenie świata
Załóżmy teraz nasz własny projekt, w którym napiszemy naszą wersję dzienniczka ucznia.
W Greenfoot wybierz z menu Scenario -> New, następnie wskaż miejsce do zapisu projektu, stwórz nowy folder Dziennik i wybierz Create.
Zostanie stworzony nowy projekt, z którym będziemy pracować i krok po kroku rozwijać.
Teraz skupimy się na umieszczeniu pierwszych kontrolek w naszym świecie, tak abyśmy na koniec tej części ćwiczenia otrzymali obraz jak poniżej.
Aby tego dokonać dodajmy najpierw klasę świata, w którym ustawimy odpowiednie elementy graficzne (GUI - graphical user interface). Kliknijmy na klasę World prawym przyciskiem myszy (PPM) i wybierzmy opcję New subclass. W oknie które się pojawi, wprowadźmy dowolną nazwę klasy. W przykładzie będę używał nazwy DziennikUcznia.
Pamiętajmy jednak, że powinna być ona znacząca, tak aby łatwiej móc z takiej klasy korzystać w przyszłości. Dzięki temu wracając do programu po kilku tygodniach dalej wiemy co dana klasa lub metoda robiła.
Kliknijmy PPM na nową klasę i wybierzmy new DziennikUcznia() tworząc tym samym nowy świat.
Otwórzmy naszą klasę i zacznijmy kodowanie.
W pierwszej kolejności ustawmy na środku etykietę (ang. Label) z imieniem i nazwiskiem ucznia. Pod spodem zróbmy podobną etykietę z nazwą przedmiotu.
Sprawdźmy w projekcie GUI_Components jakie mamy dostępne konstruktory klasy Label. Możemy to zrobić klikając PPM na klasę i sprawdzając jakie ma konstruktory (poprzedzone słówkiem new)
Lepiej jest jednak sprawdzić to w dokumentacji (JavaDoc). |
Na początku użyjmy prostszego konstruktora, któremu wystarczy dostarczyć właściwy tekst. Stwórzmy 2 obiekty w konstruktorze naszej klasy i umieśćmy je w świecie Greenfoot. Spróbujmy umieścić je na środku. Możemy wykorzystać do tego metodę prób i błędów:) Po kliknięciu przycisku Compile w oknie świata powinna pojawić się stworzona etykieta.
Kod z kolei wygląda tak:
public DziennikUcznia() { // Create a new world with 600x400 cells with a cell size of 1x1 pixels. super(600, 400, 1); // etykiety nazw Label uczenLabel = new Label("Jan Kowalski"); Label przedmiotLabel = new Label("matematyka"); addObject(uczenLabel, 300, 30); addObject(przedmiotLabel, 300, 60);
Dodajmy jeszcze listę (ListBox) z ocenami. Umieśćmy nad nią etykietę “Oceny”. Podobnie jak poprzednio przyjrzyjmy się, jakie mamy dostępne konstruktory i jakie obiekty możemy stworzyć.
Zróbmy to teraz w trochę inny sposób. Programując często warto zobaczyć jak to robi ktoś inny, można wtedy nauczyć się czegoś nowego, poznać nowe klasy i sposób ich użycia. Zajrzyjmy do klasy ListBox, klikając na nią dwukrotnie w projekcie GUI_Components. Gdy pojawi się dokumentacja zmieńmy widok na Source Code. Znajdźmy konstruktor i przyjrzyjmy się jak on wygląda? Czy potrafisz na podstawie opisu (komentarze nad konstruktorem) oraz implementacji określić jak obiekt jest tworzony, co jest potrzebne aby stworzyć obiekt?
Jak widać do działania potrzebuje on obiektu typu Point o nazwie size. Z opisu wynika, że trochę niejednoznacznie obiekt Point opisuje wielkość tworzonego obiektu. Point nie jest pełną nazwą klasy, znajduje się on w pakiecie. Czy potrafisz określić jaki to pakiet? Czy potrafisz znaleźć kawałek kodu, który inicjalizuje obiekt Point?
W importach widzimy, że klasa Point pochodzi z pakietu java.awt. Znajdźmy w internecie te klasę i jej dokumentację. Dostępna jest pod adresem:
[http://docs.oracle.com/javase/7/docs/api/java/awt/Point.html]
Pakiet powstał ze względu na konieczność rozróżnienia różnych klas tworzonych przez programistów. W projektach często mamy do czynienia z tysiącami klas i ze względu na łatwiejsze ich użycie wprowadzono taką konstrukcję.
Drugi parametr konstruktora klasy ListBox to lista typu java.util.ArrayList. ArrayList to jeden z podstawowych typów Javy. Warto poznać ją szerzej.
ArrayList pozwalaja na przechowywanie dowolnego rodzaju danych w postaci listy. Lista charakteryzuje się tym, że wartości są w niej dodawane w kolejności, domyślnie na koniec listy. Elementy na liście są zdefiniowane w kolejności i jeśli nic nie zmieniamy, to możemy je łatwo odczytać. Sprawdźmy jakie metody ma lista i jak możemy jej używać. Należy też pamiętać, że podobnie jak z tablicami elementy są numerowane od zera. To oznacza, że po stworzeniu pustej listy i dodaniu elementu, posiadamy listę o rozmiarze 1, ale wartość pobieramy przez metodę z podaniem indeksu 0, np. lista.get(0). Analogicznie drugi element pobieramy przez podanie indeksu 1, lista.get(1).
[https://docs.oracle.com/javase/7/docs/api/java/util/List.html]
Najważniejsze metody to:
Metoda | Opis |
---|---|
boolean add(E e) | Dodaje element na koniec listy. E jest to dowolny obiekt. |
void add(int index, E element) | Dodaje element do listy pod wskazanym indeksem. Element można wstawić w dowolne miejsce listy, elementy po podanym indeksie zostają przesunięte w prawo. Nie można podać liczby mniejszej od 0 i większej od wielkości listy. |
boolean contains(Object o) | Sprawdza czy podany obiekt jest już na liście. Porównanie za pomocą metody equals. |
E get(int index) | Pobiera element z listy z podanego indeksu. Nie można podać wartości mniejszej od 0 i większej, bądź równej rozmiarowi listy. |
int indexOf(Object o) | Zwraca indeks pod którym znajduje się podany obiekt. |
boolean isEmpty() | Sprawdza czy lista jest pusta. |
E remove(int index) | Usuwa element pod podanym indeksem. |
boolean remove(Object o) | Usuwa podany obiekt. |
int size() | Zwraca wielkość listy. |
W tej chwili wiemy, że aby stworzyć obiekt klasy ListBox musimy przekazać do jej konstruktora dwa inne obiekty: rozmiar elementu widziany na ekranie oraz listę wartości. Spróbujmy dodać ja na ekranie. Tworząc listę spróbuj dodać od razu kilka ocen. Na koniec dodajmy jeszcze listę. Pamiętaj, aby dodać na początku klasy w sekcji import wpisy:
import java.awt.Point;
import java.util.ArrayList;
Dzięki temu można korzystać z klas ArrayList i Point. Przykładowy kawałek kodu:
//stworzenie listy ocen ArrayList<String> oceny = new ArrayList<String>(); oceny.add("5 "); oceny.add("4 "); oceny.add("6 "); Point rozmiar = new Point(30, 60); ListBox<String> ocenyListBox = new ListBox<String>(rozmiar, oceny); addObject(ocenyListBox, 300, 160);
Obok ArrayList oraz ListBox możesz zauważyć wyrażenie <String>. Jak wspomniałem wcześniej, lista - jak i w tym wypadku ListBox - mogą przechowywać obiekty dowolnego typu. Taka konstrukcja mówi kompilatorowi (a także programiście), jakiego typu dane są przechowywane w danym obiekcie.
Możesz spotkać się z takimi konstrukcjami jak np.
ArrayList<String> - przechowuje napisy typu String
ArrayList<Integer> - lista przechowuje liczby
ArrayList<Actor> - lista przechowuje aktorów, a nawet
ArrayList<ArrayList<Integer>> - lista przechowująca listy liczb
Dodajmy teraz informacje o uwagach i osiągnięciach. Zróbmy to analogicznie do danych o ocenach.
//wyswietlanie uwag uwag ArrayList<String> uwagi = new ArrayList<String>(); uwagi.add("Swietnie poszlo w ulamkami."); uwagi.add("Sprobuj poprawic trygonometrie."); rozmiar = new Point(200, 60); ListBox<String> uwagiListBox = new ListBox<String>(rozmiar, uwagi); addObject(uwagiListBox, 300, 250); //stworzenie etykiety uwag Label uwagiLabel = new Label("Uwagi/osiagniecia"); addObject(uwagiLabel, 300, 210);
Uruchom aplikację. Czy wszystko poszło ok, jak wygląda teraz?
Spróbuj jeszcze ustawić tło, aby ekran wyglądał lepiej. Na tym etapie ekran powinien wyglądać podobnie do prezentowanego na poniższym obrazku. Powinieneś użyć metody setBackground z klasy World.
Najpierw znajdź np. w internecie obrazek który chciałbyś wyświetlać w tle. Następnie pobierz go i zmień nazwę na tlo. Pamiętaj, aby obrazek był typu jpg.
Następnie obrazek umieść w folderze projektu w folderze images.
Teraz na końcu konstruktora wywołaj funckję setBackground.
//ustawienie tla ekranu setBackground("tlo.jpg");
Po uruchomieniu aplikacji powinieneś zobaczyć ekran jak poniżej.
Wprowadzanie oceny
W tym kroku pozwolimy nauczycielowi wprowadzić ocenę do naszego dzienniczka.
W pierwszej kolejności potrzebny będzie nam przycisk dodawania oceny. Sprawdźmy klasę Button i to, jak wygląda jej konstruktor.
Jak widzimy, aby utworzyć przycisk, należy jako pierwszy argument podać wyświetlany tekst. Drugi argument to rozmiar, który jest znanego nam już typu Point.
Spróbuj dodać przycisk, tak aby pojawił się on obok tabeli z ocenami.
Najpierw utwórz pole przyciskOceny.
public class DziennikUcznia extends World { private Button przyciskOceny;
Następnie utwórzmy nową metodę w klasie o nazwie utworzElementyGuiOcen(). Przenieśmy do niej kod odpowiedzialny za tworzenie listy.
private void utworzElementyGuiOcen() { //stworzenie listy ocen ArrayList<String> oceny = new ArrayList<String>(); oceny.add("5 "); oceny.add("4 "); oceny.add("6 "); Point rozmiar = new Point(30, 60); ListBox<String> ocenyListBox = new ListBox<String>(rozmiar, oceny); //stworzenie etykiety ocen Label ocenyLabel = new Label("Oceny"); addObject(ocenyLabel, 300, 120); }
Poniżej tego kodu dodajmy tworzenie przycisku:
Label ocenyLabel = new Label("Oceny"); addObject(ocenyLabel, 300, 120); //stworzenie przycisku przyciskOceny = new Button("dodaj ocene", new Point(80, 20)); addObject(przyciskOceny, 380, 140);
Przycisk zostanie dodany na ekranie. Skompiluj program i uruchom. Czy jest widoczny? Zmieniając parametry wywołania new Point(80, 20) możesz modyfikować jego wielkość. Z kolei zmieniając wartości w metodzie addObject addObject(przyciskOceny, 380, 140); możesz zmieniać jego położenie.
Świetnie, mamy już przycisk, teraz musimy dodać logikę, która zapyta nauczyciela o ocenę. Najwłaściwszy będzie tu element, który wyświetli nam nowe okno (nazywane oknem dialogowym, z ang. dialog window). Użyjmy do tego celu klasy InputWindow, wyświetli ona nam komunikat z prośbą o podanie oceny i przyciskami “ok” oraz “cancel”. Aby wyświetlić to okno musimy przekazać do przycisku odpowiednią instrukcję, która wywoła to okno po kliknięciu przycisku.
Zacznijmy od wprowadzenia kilku zmian w metodzie utworzElementyGuiOcen.
- usuńmy dodawanie ocen, pozostawmy pustą listę
- zdefiniujmy listBox z ocenami i przycisk, jako pola klasy, a nie jak było do dotychczas jak zmienne lokalne
Dodaj pole OcenyListBox na początku klasy
private Button przyciskOceny; private ListBox<String> ocenyListBox;
Następnie zmień w metodzie utworzElementyGuiOcen sposób tworzenia listyOcen
private void utworzElementyGuiOcen() { //stworzenie listy ocen Point rozmiar = new Point(30, 60); ArrayList<String> oceny = new ArrayList<String>(); ocenyListBox = new ListBox<String>(rozmiar, oceny); addObject(ocenyListBox, 300, 160);
ocenyListBox nie jest już zmienną lokalną i można jej użyć we wszystkich metodach klasy.
Dokładniej mówiąc, we wszystkich niestatycznych metodach klasy. To znaczy takich metodach, które można wywołać tylko dla obiektu. Przykładem metody statycznej jest metoda startująca aplikację
Greenfoot.start(); W następnej kolejności dodajmy nowe pole typu InputWindow o nazwie np. oknoOceny, będzie ono przechowywać okno do wprowadzania ocen. Od razu zainicjujmy je. Sprawdźmy jak wygląda konstruktor:
Jak widać przyjmuje 2 parametry:
- prompt - komunikat wyświetlany użytkownikowi, wpiszmy np. “Proszę o podanie oceny”
- max - ilość możliwych do wprowadzenia znaków, ustawmy 1
Kod powinien wyglądać mniej więcej tak, jak poniżej:
public class DziennikUcznia extends World { private Button przyciskOceny; private ListBox<String> ocenyListBox; private InputWindow oknoOceny = new InputWindow("Podaj ocene", 1);
Musimy jeszcze dodać logikę, która będzie nasłuchiwać na kliknięcie przycisku, wyświetlać okno z pytaniem o ocenę oraz - po wprowadzeniu - doda ją na listę ocen.
Musimy nadpisać metodę void act() klasy World, która pozwoli nam przechwycić zdarzenia występujące w świecie. Aby kod wyglądał czytelnie, stwórzmy nową metodę void sluchajNaPrzyciskuOceny(), w której umieścimy odpowiedni kod, a samą metodę wywołajmy w metodzie act(). W ten sposób podzielimy kod na czytelne części.
@Override public void act() { sluchajNaPrzyciskuOceny(); }
W metodzie sluchajNaPrzyciskuOceny() dodajmy sprawdzanie czy przycisk został wciśnięty i pokażmy okno z pytaniem o ocenę. Zerknijmy w dokumentacje klas Button oraz InputWindow i znajdźmy potrzebne nam metody:
Interesują nas 2 metody, Button.wasClicked() oraz InputWindow.toogleShow(value). Pierwsza z nich sprawdzi czy przycisk został wciśnięty, druga pozwoli na pokazanie okna, jeśli jest ono ukryte i przekaże do niej pusty napis.
Kod powinien wyglądać mniej więcej jak poniżej:
private void sluchajNaPrzyciskuOceny() { if (przyciskOceny.wasClicked()) { oknoOceny.toggleShow(""); } }
Zapiszmy zmiany (CTRL+S), skompilujmy projekt i uruchommy nasz program klikając przycisk Run. Po kliknięciu na przycisk powinno pojawić się okno, w które możemy wprowadzić wartość.
Można zauważyć, że jednak po jej wprowadzeniu i kliknięciu OK nic się nie stanie. Musimy dodać jeszcze sprawdzenie czy kliknięto w okno OK i pobrać tę wartość. Spróbuj w metodach klasy InputWindow odnaleźć metodę, która pozwoli to sprawdzić.
Odpowiednia metoda to InputWindow.getResult(). Jeśli zwróci ona wartość, to oznacza, że wprowadzono wartość i kliknięto ok. Dodaj sprawdzanie czy metoda zwraca wartość, a jeśli tak jest, dodaj wartość do listy ocen przez wywołanie metody: ocenyListBox.add(wprowadzonaOcena)
Pamiętaj, że po wywołaniu getResult() drugie wywołanie zwróci pustą wartość, więc konieczne będzie przechowanie wartości getResult() w zmiennej lokalnej.
Kod powinien wyglądać mniej więcej tak.
if (przyciskOceny.wasClicked()) { oknoOceny.toggleShow(""); } String wprowadzonaOcena = oknoOceny.getResult(); if (wprowadzonaOcena != null) { ocenyListBox.add(wprowadzonaOcena); }
Przed zakończeniem tego ćwiczenia, dodajmy jeszcze wywołanie w na końcu konstruktora świata.
//ustawienie tla ekranu setBackground("tlo.jpg"); Greenfoot.start(); }
Dzięki temu, już po kompilacji aplikacja się uruchomi i nie będziemy musieli za każdym razem klikać przycisku Run.
Przetestuj czy dodawanie ocen działa prawidłowo. Zobacz jakie wartości możesz dodać. Spróbuj dodać pustą wartość, co się stanie?
Walidacja danych
Dobrą praktyką jest sprawdzanie czy wprowadzane wartości są prawidłowe. Dodajmy prostą walidację, która doda do listy tylko prawidłowe wartości. Załóżmy, że są to oceny 1,2,3,4,5,6.
Spróbuj dodać tę logikę po sprawdzeniu wartości InputWindow.getResult() w metodzie sluchajNaPrzyciskuOceny.
W pierwszej kolejności dodajmy najprostsze sprawdzanie, czyli porównanie każdej wartości - sprawdzenie czy wprowadzona wartość jest równa “1”, “2”, “3” itd.. Pamiętaj, że w Javie do porównywania obiektów, w tym napisów (String) służy funkcja Object.equals(Object o), np. “1”.equals(wartosc). W naszym przypadku będzie to kod:
if (wprowadzonaOcena != null) { if (wprowadzonaOcena.equals("1") || wprowadzonaOcena.equals("2") || wprowadzonaOcena.equals("3") || wprowadzonaOcena.equals("4") || wprowadzonaOcena.equals("5") || wprowadzonaOcena.equals("6")) { ocenyListBox.add(wprowadzonaOcena); }
Operator || jest operatorem logicznym i oznacza albo. Przy wpisaniu wyrażeń, ważne jest aby conajmniej jedno z nich było prawdziwe. Tłumacząc na pseudokod mamy tutaj wyrażenie :
jeśli wprowadzona ocena = 1 albo wprowadzona ocena = 2 |
Dodaj ten kod sprawdzający i zobacz, czy teraz możesz wprowadzić inne wartości niż podane w warunku if.
Powyższe rozwiązanie jest jednak brzydkie i czasochłonne. Jeśli trzeba byłoby wymienić 100 wartości, pojawiłby się duży problem.
Inny sposób to zastosowanie listy możliwych wartości i sprawdzenie jej. Kod wyglądałaby np. tak:
ArrayList<String> prawidloweWartosc = new ArrayList<String>(); prawidloweWartosc.add("1"); prawidloweWartosc.add("2"); prawidloweWartosc.add("3"); prawidloweWartosc.add("4"); prawidloweWartosc.add("5"); prawidloweWartosc.add("6"); if (prawidloweWartosc.contains(wprowadzonaOcena)) { ocenyListBox.add(wprowadzonaOcena); }
Zmienną prawidloweWartosci można wprowadzić jako stałą w aplikacji i wtedy może być użyta wielokrotnie. Zmień kod który wprowadziłeś wcześniej i zobacz czy aplikacja dalej działa tak samo.
Jak widać to rozwiązanie może być odrobinę lepsze, ale nie różni się znacząco od pisania wielu warunków w if.
Lepszym rozwiązaniem byłoby zamienienie wprowadzonej oceny na liczbę i sprawdzenie czy jest z przedziału <1,6>. Możemy to zrobić za pomocą poniższego kodu. Spróbuj go użyć w swojej aplikacji. Sprawdź czy aplikacja dalej działa prawidłowo.
try { //rzutowanie wartosci na integer i sprawdzenie czy wartosc jest prawidlowa Integer wartosc = Integer.valueOf(wprowadzonaOcena); if (wartosc >= 1 && wartosc <= 6) { //dodanie do listy wartosci ocenyListBox.add(wprowadzonaOcena); } } catch (NumberFormatException e) { //ignorujemy, po prostu nie dodajmy do listy }
Ten przykład może wydawać się odrobinę skomplikowany, prześledźmy go dokładnie.
W pierwszej kolejności pobieramy liczbową wartość zmiennej, jest to linijka:
Integer wartosc = Integer.valueOf(wprowadzonaOcena);
Od teraz wartosc będzie liczbą. Teraz musimy sprawdzić czy jest ona z odpowiedniego przedziału, do tego służy warunek if.
if (wartosc >= 1 && wartosc <= 6)
Operator >= oznacza większe, bądź równe.
Operator <= oznacza mniejsze, bądź równe.
Operator && jest to operator logiczny i. Warunek jest spełniony, jeśli oba wyrażenia są prawdziwe.
W pseudokodzie możemy zapisać to jako:
jeśli wartosc jest większa, bądź równa 1 |
Na pewno zastanawia Cię konstrukcja
try { ... } } catch (NumberFormatException e) { }
Jest to tak zwane łapanie wyjątku. Niektóre metody, w tym przypadku Integer.valueOf() zwracają wartość, ale jeśli przekaże się im na przykład nieprawidłowe dane to rzucają wyjątek. W takim wypadku, trzeba go złapać i obsłużyć. Jeśli tego nie zrobimy, aplikacja może nie działać prawidłowo.
W ramach eksperymentu usuńmy na chwilę te linijki i zobaczmy co się stanie jak wpiszesz np. wartość “a”, zamiast cyfry. Zostawmy samo
//rzutowanie wartosci na integer i sprawdzenie czy wartosc jest prawidlowa Integer wartosc = Integer.valueOf(wprowadzonaOcena); if (wartosc >= 1 && wartosc <= 6) { //dodanie do listy wartosci ocenyListBox.add(wprowadzonaOcena); }
Powinieneś zobacz okno z informacją o błędzie jak poniżej:
Dodatkowo możesz zaobserwować, że aplikacja przestała działać.
Przywróć kod do poprzedniej postaci, aby błędnie wprowadzone wartości były ignorowane.
Stworzenie listy uwag i osiągnięć.
Spróbuj w sposób analogiczny do listy ocen stworzyć listę uwag i osiągnięć. Pamiętaj o:
- wydzieleniu metod, które pozwolą Ci w łatwy sposób odnaleźć się w kodzie aplikacji
- zamianie zmiennych lokalnych elementów GUI na pola klasy
- ustawieniu pustej listy uwag i osiągnięć na starcie aplikacji
- dodaniu okna uwag i ustawieniu odpowiedniej szerokości (odpowiedni parametr w konstruktorze)
- dodaniu odpowiedniej metody sprawdzającej stan przycisku i czy kliknięto przycisk ok w oknie
- dodaniu walidacji, spróbuj nie dodawać uwagi, gdy ktoś wprowadzi pusty tekst (sprawdzenie przez użycie metody !"".equals(tekst))
W pierwszej kolejności zmień zmienną lokalną uwagiListBox z konstruktora na pole. Na początku klasy, pod polem oknoOcen zadeklaruj pole uwagiListBox:
private InputWindow oknoOceny = new InputWindow("Podaj ocene", 1); private ListBox<String> uwagiListBox;
Zmień kod inicjujący listę, usuwając również wpisy uwag, na:
ArrayList<String> uwagi = new ArrayList<String>(); Point rozmiarUwag = new Point(200, 60); uwagiListBox = new ListBox<String>(rozmiarUwag, uwagi); addObject(uwagiListBox, 300, 250);
Skompiluj projekt i zobacz czy działa prawidłowo.
Teraz poprawmy trochę czytelność kodu. Stwórz nową metodę utworzElemenyUwag()
private void utworzElementyUwag() { }
Przenieś (wytnij i wklej) do niej kod odpowiedzialny za tworzenie listy uwag i etykiety.
private void utworzElementyUwag() { //stworzenie wyswietlania uwag ArrayList<String> uwagi = new ArrayList<String>(); Point rozmiarUwag = new Point(200, 60); uwagiListBox = new ListBox<String>(rozmiarUwag, uwagi); addObject(uwagiListBox, 300, 250); //stworzenie etykiety uwag Label uwagiLabel = new Label("Uwagi/osiagniecia"); addObject(uwagiLabel, 300, 210); }
Pamiętaj o wywołaniu metody w konstruktorze.
utworzElementyGuiOcen(); utworzElementyUwag();
Ponownie skompiluj projekt. Wszystko powinno działać jak wcześniej.
Teraz jesteśmy gotowi na dodanie funkcjonalności. Zadeklaruj pole z przyciskiem dodawania uwag.
private Button przyciskUwag;
W metodzie utworzElementyUwag zainicjuj przycisk i dodaj do świata.
//stworzenie etykiety uwag Label uwagiLabel = new Label("Uwagi/osiagniecia"); addObject(uwagiLabel, 300, 210); przyciskUwag = new Button("Dodaj uwage", new Point(80, 20)); addObject(przyciskUwag, 450, 270);
Skompiluj projekt. Czy widzisz nowo dodany przycisk?
Teraz potrzebujemy uruchomienia akcji na kliknięcie przycisku i otwarcia okna z pytaniem o uwagę.
Dodajmy pole oknoUwag, tak ak w przypadku ocen.
private InputWindow oknoUwag = new InputWindow("Dodaj ocene lub osiagniecie", 100);
Teraz dodajmy metodę sluchajNaPrzyciskuUwag, w której otworzymy okno po klinięciu na przycisk.
private void sluchajNaPrzyciskuUwag() { //sprawdzenie czy wcisniego przycisk if (przyciskUwag.wasClicked()) { //pokazanie okna, z czyszczeniem wartosci oknoUwag.toggleShow(""); } }
Teraz musimy już tylko wywołać tę metodę w metodzie act.
public void act() { sluchajNaPrzyciskuOceny(); sluchajNaPrzyciskuUwag(); }
Ponownie skompiluj. Sprawdź czy jeśli klikniesz na przycisku, pojawi się okno.
Teraz wystarczy dodać kod, który doda uwagę do listy uwag. W metodzie sluchajNaPrzyciskuUwag dodaj kod, który do uwagiListBox doda wprowadzony wpis. Sprawdźmy od razu czy wpis nie jest pusty, a jeśli tak, zignorujmy go.
//sprawdzenie czy wprowadzono uwage String wprowadzonaUwaga = oknoUwag.getResult(); if (wprowadzonaUwaga != null && !"".equals(wprowadzonaUwaga)) { //dodanie do listy uwag uwagiListBox.add(wprowadzonaUwaga); }
Instrukcja !"".equals(wprowadzonaUwaga) jest dość skomplikowana. W pierwszej kolejności przyjrzyjmy się konstrukcji "".equals(wprowadzonaUwaga) sprawdza ona tylko czy zmienna wprowadzonaUwaga nie jest pustym napisem. "" - oznacza pusty napis Wykrzyknik na początku ! jest opertorem negacji. To oznacza, że jeśli wartość jest prawdziwa (true), to po jej zanegowaniu będzie fałszywa (false). Wyrażenie if(!false){ jest równoznaczne if(true){
Skompiluj program. Spróbuj dodać nową uwagę. Czy wszystko działa prawidłowo? Spróbuj też dodać pustą uwagę, co się dzieje?
Stworzenie klas ucznia i dziennika przedmiotu
Poprzedni rozdział pozwolił Ci na szlifowanie nabytych wcześniej umiejętności. W tym poznasz nowe elementy języka Java i przygotujesz aplikację tak, aby przypominała prawdziwy dziennik.
Jak pewnie zauważyłeś, w aplikacji brakuje możliwości zmiany przedmiotu i ucznia. Żeby tego dokonać potrzebujemy klas, które przechowają uczniów oraz ich oceny i uwagi z poszczególnych przedmiotów. Z tego względu zróbmy najpierw nową klasę, która będzie przechowywać dla nas oceny i uwagi z pojedynczego przedmiotu.
Wybierz w menu Edit -> NewClass aby dodać nową klasę.
Podaj jej nazwę, WynikiUcznia.
Zostanie stworzona klasa wygenerowana z polem x i przykładową metodą.
Usuń zarówno pole jak i metodę. Pozostaw tylko definicję klasy i konstruktor.
W tym momencie potrzebujemy listy przedmiotów oraz listy uwag, które obecnie są w naszej klasie świata. W sekcji importów dodaj:
import java.util.*;
Utwórzmy pola z listą ocen i listą uwag.
public class WynikiUcznia { private ArrayList<String> oceny; private ArrayList<String> uwagi;
Zainicjuj te pola w konstruktorze.
public WynikiUcznia() { oceny = new ArrayList<String>(); uwagi = new ArrayList<String>(); }
Dodajmy tu jeszcze metody, które pozwolą nam dodać uwagę i ocenę oraz pobrać obie te listy. Pamiętaj o dodaniu metod publicznych, aby były dostępne w innych klasach.
public void dodajOcene(String ocena) { oceny.add(ocena); } public ArrayList<String> pobierzOceny() { return oceny; } public void dodajUwage(String uwaga) { uwagi.add(uwaga); } public ArrayList<String> pobierzUwagi() { return uwagi; }
Świetnie, mamy już obiekt, w którym będziemy przechowywać dane z przedmiotu. Dodajmy jeszcze klasę ucznia. Załóż nową klasę, nazywając ją np. Uczen i podobnie jak poprzednio, usuń zbędne pole i metodę. Zastanów się jakie wartości będzie przechowywać.
Analizując to, co prezentujemy na ekranie ważne będą:
- imię i nazwisko ucznia
- lista przedmiotów ucznia i jego oceny
Dodajmy na początku dwa pola: imię i nazwisko.
public class Uczen { private String imie; private String nazwisko;
Oceny przechowujemy w klasie WynikiUcznia, teraz musimy się zastanowić jak będziemy przechowywać te informacje w klasie Uczen.
Do tego celu idealnie nadaje się klasa Javy HashMap. Mapa jest obiektem w którym możemy przechowywać dane na zasadzie klucz -> wartość. Oznacza to, że możemy powiedzieć takiemu obiektowi, żeby pod podanym kluczem przechował nam inny obiekt. W naszym przypadku możemy użyć mapy do przechowania pod kluczem z nazwą przedmiotu, obiektu klasy WynikiUcznia z interesującymi nas wartościami.
Spójrzmy jak wygląda konstruktor klasy HashMap i jakie ma główne metody.
Obrazek. Konstruktory klasy HashMap.
Pierwszy konstruktor jest bezparametrowy, tworzy domyślną mapę, to jest dla nas wystarczające.
Najważniejsze metody klasy HashMap to:
Metoda | Opis |
---|---|
Object get(Object key) | zwraca wartość z mapy znajdującą się pod podanym w parametrze kluczem |
Object put(Object key, Object value) | dodaje pod podanym w pierwszym parametrze kluczem wartość podaną w drugim parametrze |
void clear() | usuwa wszystkie elementy mapy |
boolean containsKey(Object key) | sprawdza czy mapa posiada wartość pod danym kluczem |
boolean isEmpty() | sprawdza czy mapa jest pusta |
Set<Object> keySet() | zwraca listę kluczy. Metoda jest bardzo przydatna podczas przechodzenia pętlą po mapie |
int size() | zwraca liczbę elementów znajdującą się w mapie |
Podobnie jak poprzednio z klasą ArrayList, aby użyć klasy HashMap dodaj na początku pliku import:
import java.util.*;
Teraz stwórzmy pole z mapą wyników z danych przedmiotów.
private String imie; private String nazwisko; private HashMap<String, WynikiUcznia> przedmioty;
Tworząc obiekt ucznia powinniśmy znać jego imię i nazwisko. Zmodyfikuj konstruktor tak aby przyjmował w parametrach te wartości i przypisze je do pól. Pamiętaj, że jeśli parametry nazwiesz tak samo jak pola, to musisz użyć słówka kluczowego this przed nazwą pola.
W konstruktorze także zainicjuj mapę przedmiotów.
public Uczen(String imie, String nazwisko) { this.imie = imie; this.nazwisko = nazwisko; przedmioty = new HashMap<String, WynikiUcznia>(); }
Dodajmy teraz metody zwracające imię i nazwisko ucznia.
public String pobierzImie() { return imie; } public String pobierzNazwisko() { return nazwisko; }
Dodaj jeszcze metodę która zwróci połączone imię i nazwisko. Obiekty typu String możemy łączyć przez operator +. Możemy je łaczyć także z innymi obiektami i w ten sposób dostać połączony napis. Przykłady użycia:
String wartosc1 = "dziś jest " + 21 + " maja"; String wartosc2 = "Ma na imie " + imie;
Łącząc imię i nazwisko pamiętaj o odstępie pomiędzy słowami. Wystarczy użyć spacji w napisie.
public String pobierzUcznia() { return imie + " " + nazwisko; }
Przyda nam się także metoda tworząca przedmiot i zwracająca obiekt WynikiUcznia dla danego przedmiotu. Dodaj obie te metody.
public void dodajWyniki(String przedmiot) { //dodaje nowy przedmiot na mape, tworzy nowy obiekt, aby moc korzystac z przedmiotu przedmioty.put(przedmiot, new WynikiUcznia()); } public WynikiUcznia pobierzWyniki(String przedmiot) { return przedmioty.get(przedmiot); }
Mamy już klasy do obsługi ocen z przedmiotów i uczniów. W tej chwili musimy poprawić kod w klasie DziennikUcznia, aby korzystał ze stworzonych klas. Dodaj dwa pola w klasie DziennikUcznia o typie Uczen i WynikiUcznia. Uczen będzie przechowywał dane ucznia, WynikiUcznia przechowa aktualnie wyświetlane wyniki z przedmiotu.
//pola uczenia i przedmiotu private Uczen uczen; private WynikiUcznia aktualneWyniki;
W konstruktorze klasy DziennikUcznia stwórz nowy obiekt ucznia i dodaj przedmiot matematyka. Pamiętaj jaki konstruktor ma uczeń. Pobierz ten przedmiot do zmiennej aktualnego przedmiotu. Zmień inicjację etykiety ucznia, aby pobierał imię i nazwisko z metody klasy Uczen.
Kod powinien wyglądać podobnie do podanego poniżej.
uczen = new Uczen("Jan", "Kowalski"); uczen.dodajWyniki("matematyka"); aktualneWyniki = uczen.pobierzWyniki("matematyka"); // etykiety nazw Label uczenLabel = new Label(uczen.pobierzUcznia());
Musimy jeszcze zmienić kod dodający ocenę i uwagę, aby dodawał je do pola aktualneWyniki po czym odświeżał wartości na liście. Do odświeżania napiszmy metodę, która przyjmie listę gui (ListBox) oraz ArrayList napisów do wyświetlenia. Musimy przejść pętlą po liście wartości i dodać każdą do ListBox. Musimy też najpierw wyczyścić ListBox. Przypomnij sobie, jakie metody w dokumentacji GuiComponents ma ListBox.
Metoda powinna wyglądać tak:
private void odswiezListBox(ListBox<String> listBox, ArrayList<String> wartosci) { listBox.clear(); for (String wartosc : wartosci) { listBox.add(wartosc); } }
Aby przejść po liście, Java oferuje bardzo przyjazną konstrukcję, która pozwala w bardzo prosty sposób przejść po każdej z wartości.
for (String wartosc : wartosci) { listBox.add(wartosc); }
Teraz zmieńmy kod do wprowadzenia nowej uwagi- powinien wyglądać jak poniżej.
if (wprowadzonaUwaga != null && !"".equals(wprowadzonaUwaga)) { //dodanie do listy uwag aktualneWyniki.dodajUwage(wprowadzonaUwaga); odswiezListBox(uwagiListBox,aktualneWyniki.pobierzUwagi()); }
Najpierw do aktualnego wyniku dodawana jest uwaga, a później wywołane jest odświeżenie listy. Wykorzystana jest wspólna metoda.
Spróbuj zrobić podobnie dla ocen. Wykorzystaj wspólną metodę do odświeżania listy box.
if (wartosc >= 1 && wartosc <= 6) { //dodanie do listy ocen aktualneWyniki.dodajOcene(wprowadzonaOcena); odswiezListBox(ocenyListBox, aktualneWyniki.pobierzOceny()); }
Tutaj także, najpierw do aktualnego wyniku dodawana jest nowa ocena, a później lista ocen jest odświeżana.
Skompiluj teraz projekt. Czy wszystko działa tak jak poprzednio? Czy możesz dodawać uwagi i oceny?
To ćwiczenie było bardzo wymagające, ale pozwoliło Ci zapoznać się z listami, mapami. Zobaczyłeś jak iterować po liście. Umiesz już tworzyć klasy i konstruktory z parametrami. Gratulacje!
Zmiana przedmiotów na ekranie
Każdy dziennik zawiera wiele przedmiotów. W tym ćwiczeniu:
- dodasz możliwość zmiany przedmiotów,
- poćwiczysz dalsze wykorzystanie list,
- stworzysz też kilka metod, które spowodują, że kod będzie bardziej czytelny i łatwiejszy w obsłudze.
Przejdź do klasy Dziennik i stwórz kolejne przyciski - następny przedmiot, poprzedni przedmiot. Będą one potrzebne w metodze act(), dlatego niech będą polami w klasie.
//Przyciski zmiany przedmiotow private Button poprzedniPrzedmiot, nastepnyPrzedmiot;
Dodaj jeszcze tworzenie przycisków i umieśćmy je w świecie. W konstruktorze dodajmy wpisy:
nastepnyPrzedmiot = new Button("nastepny przedmiot", new Point(120, 20)); poprzedniPrzedmiot = new Button("poprzedni przedmiot", new Point(120, 20)); addObject(nastepnyPrzedmiot, 450, 60); addObject(poprzedniPrzedmiot, 150, 60);
Ekran powinien wyglądać jak poniżej.
Brakuje nam teraz list przedmiotów, które chcemy wyświetlać oraz pola, które przechowa nam numer obecnego przedmiotu na liście. Dodaj 3 pola do klasy Dziennik:
- Lista przedmiotów
- indeks aktualnie wyświetlanego przedmiotu, domyślnie niech będzie to 0
- etykietę przedmiotu. Musi od teraz być polem, gdyż będziemy ją zmieniać.
Nowe pola.
//obsluga przedmiotow private ArrayList<String> przedmioty; private int aktualnyPrzedmiot = 0; private final Label przedmiotLabel;
Zainicjuj listę w konstruktorze kilkoma przedmiotami. Zmień inicjalizację etykiety przedmiotu, aby domyślnie była pusta
public DziennikUcznia() { ... przedmioty = new ArrayList<String>(); przedmioty.add("matematyka"); przedmioty.add("jezyk polski"); przedmioty.add("informatyka"); przedmiotLabel = new Label("");
Skompiluj projekt. Czy widzisz różnice? Pole z nazwą przedmiotu jest puste. Za chwilę zajmiemy się jego aktualizacją.
Teraz potrzebujemy napisać kod do obsługi wciśnięcia przycisków i zmiany aktualnie wyświetlanego przedmiotu.
Dodaj metodę sluchajNaZmianePrzedmiotu().
private void sluchaNaZmianePrzedmiotu() { }
Teraz dodaj kod w tej metodzie, który sprawdzi czy wciśnięto któryś z tych przycisków. Jeśli wciśnięto przycisk “następny przedmiot”, musisz:
- zwiększyć wartość pola aktualnyPrzedmiot o 1. Pamiętaj, że jeśli aktualnyPrzedmiot będzie równy wielkości listy przedmiotów, musisz ustawić wartość na 0, aby iść od początku listy
- zmień etykietę przedmiotLabel. Obecnie jest zmienną lokalną, zrób z niej pole, aby była dostępna w metodzie sluchajNaZmianePrzedmiotu. Ustaw wartość, która jest na liście pod indeksem aktualnyPrzedmiot.
private void sluchajNaZmianePrzedmiotu() { //czy wcisnieto nastepny przedmiot if (nastepnyPrzedmiot.wasClicked()) { //przejscie na nastepny przedmiot aktualnyPrzedmiot = aktualnyPrzedmiot + 1; //sprawdzenie czy nie doszedl do konca listy if (aktualnyPrzedmiot >= przedmioty.size()) { aktualnyPrzedmiot = 0; } String aktualnaNazwa = przedmioty.get(aktualnyPrzedmiot); przedmiotLabel.setText(aktualnaNazwa); }
Wywołaj teraz metodę w metodzie act.
public void act() { ... sluchaNaZmianePrzedmiotu(); }
Skompiluj projekt. Wciśnij przycisk następny przedmiot. Czy coś się dzieje?
Podobnie zrób przy wciśnięciu przycisku “poprzedni przedmiot”. Pamiętaj, że tam należy zmniejszyć wartość aktualnyPrzedmiot. Pamiętaj też, że indeks nie może być mniejszy niż 0. Zwróć też uwagę, że lista jest numerowana od 0, więc jeśli jej rozmiar wynosi 5, to poprawne indeksy to wartości od 0 do 4.
//czy wcisnieto przycisk porzedmiego przedmiotu if (poprzedniPrzedmiot.wasClicked()) { //przejscie na nastepny przedmiot aktualnyPrzedmiot = aktualnyPrzedmiot - 1; //sprawdzenie czy nie jest mniejszy od zera if (aktualnyPrzedmiot < 0) { aktualnyPrzedmiot = przedmioty.size() - 1; } String aktualnaNazwa = przedmioty.get(aktualnyPrzedmiot); przedmiotLabel.setText(aktualnaNazwa); }
Teraz uruchom aplikację. Poklikaj na stworzone przyciski. Czy nazwa przedmiotu się zmienia?
Spróbuj teraz dodać nową ocenę lub uwagę. Następnie zmień przedmiot. Czy aplikacja działa prawidłowo? Czy po zmianie przedmiotu lista nie powinna być czysta? Czy możesz powiedzieć dlaczego aplikacja właśnie tak się zachowuje?
Problemem jest to, że lisa przedmiotów nie jest w tym momencie w żaden sposób powiązana z przedmiotami ucznia. Drugim problemem jest to, że odświeżanie elementów GUI, tj. listy ocen, uwag i nazwy przedmiotu, nie jest robione wspólnie. Poprawmy nasz kod.
W pierwszej kolejności zróbmy metodę, która odświeży wszystkie wymienione elementy. Nazwij ją odswiezEkran(). Najpierw pobierz aktualny przedmiot i ustaw etykietę.
private void odswiezEkran() { //pobieram aktualny przedmiot String nazwaAktualnegoPrzedmiotu = przedmioty.get(aktualnyPrzedmiot); przedmiotLabel.setText(nazwaAktualnegoPrzedmiotu); }
W metodzie sluchajNaZmianePrzedmiotu zamień ustawianie etykiety na wywołanie metody odswiezEkran(). Pamiętaj, że dzieje się to w dwóch miejscach.
//sprawdzenie czy nie doszedl do konca listy if (aktualnyPrzedmiot >= przedmioty.size()) { aktualnyPrzedmiot = 0; } // odswiezam ekran odswiezEkran();
oraz
if (aktualnyPrzedmiot < 0) { aktualnyPrzedmiot = przedmioty.size() - 1; } // odswiezam ekran odswiezEkran(); }
W metodzie odswiezEkran należy pobrać aktualne wyniki przedmiotu u ucznia, jeśli nie ma wyników, dodajmy je. Wyniki zapisz pod istniejącym polem - aktualneWyniki.
private void odswiezEkran() { .. //pobieram aktualne wyniki aktualneWyniki = uczen.pobierzWyniki(nazwaAktualnegoPrzedmiotu); if (aktualneWyniki == null) { // jesli brak wynikow inicjalizuje je uczen.dodajWyniki(nazwaAktualnegoPrzedmiotu); aktualneWyniki = uczen.pobierzWyniki(nazwaAktualnegoPrzedmiotu); }
Teraz w tej samej metodzie uaktualnijmy listę uwag i ocen. Możemy wykorzystać kod, który obecnie jest w metodach nasłuchujących na przyciskach dodawania.
private void odswiezEkran() { ... // odświeżenie list uwag i ocen odswiezListBox(uwagiListBox, wynikiUcznia.pobierzUwagi()); odswiezListBox(ocenyListBox, wynikiUcznia.pobierzOceny()); }
Zamiast tamtego kodu umieść tam wywołanie metody odswiezEkran().
private void sluchajNaPrzyciskuUwag() { … if (wprowadzonaUwaga != null && !"".equals(wprowadzonaUwaga)) { //dodanie do listy uwag aktualneWyniki.dodajUwage(wprowadzonaUwaga); odswiezEkran(); } }
oraz
private void sluchajNaPrzyciskuOceny() { ... try { ... aktualneWyniki.dodajOcene(wprowadzonaOcena); odswiezEkran(); } } catch (NumberFormatException e) { //ignorujemy, po prostu nie dodajmy do listy } } }
Poprawmy też kod w konstruktorze. Usuń kod dodający wynik ucznia oraz pobierający te wyniki do pola aktualneWyniki. Zamiast tego na końcu konstruktora, tuż przed wywołaniem Greenfoot.start() wywołaj metodę odświeżającą ekran.
public DziennikUcznia() { ... //uczen = new Uczen("Jan", "Kowalski"); //uczen.dodajWyniki("matematyka"); aktualneWyniki = uczen.pobierzWyniki("matematyka"); ... odswiezEkran(); Greenfoot.start(); }
Uruchom teraz aplikację i spróbuj powtórzyć test. Dodaj ocenę np. do jednego przedmiotu, przejdź do następnego, dodaj tam uwagę, a następnie wróć do poprzedniego. Czy wprowadzona ocena jest widoczna?
Lista uczniów
W tym kroku dodamy do naszej aplikacji listę uczniów. Dzięki temu będziemy mogli przełączać się pomiędzy kolejnymi osobami w dzienniku.
W pierwszej kolejności dodaj 2 przyciski: następny i poprzedni uczeń, podobnie jak zrobiłeś to przy przedmiotach. Przyciski będą potrzebne nam później przy sprawdzaniu, czy zostały wciśnięte, więc niech będą polami w klasie Dziennik.
public class DziennikUcznia extends World { ... //przyciski uczniow private Button poprzedniUczen, nastepnyUczen;
Zainicjujmy je w konstruktorze i dodajmy do świata.
poprzedniUczen = new Button("Poprzedni uczen", new Point(120, 20)); nastepnyUczen = new Button("Nastepny uczen", new Point(120, 20)); addObject(poprzedniUczen, 150, 30); addObject(nastepnyUczen, 450, 30);
Ekran powinien wyglądać jak na obrazku poniżej.
Teraz potrzebujemy listy, która przechowa nam uczniów. Stwórz listę uczniów, jako pole w klasie.
//pelna lista uczniow private ArrayList<Uczen> uczniowie;
Lista przechowująca obiekty klasy Uczen.
Zainicjuj pole w konstruktorze, dodając kilka osób - będą nam potrzebne do testów.
public DziennikUcznia() { ... uczniowie = new ArrayList<Uczen>(); uczniowie.add(new Uczen("Anna", "Nowak")); uczniowie.add(new Uczen("Pawel", "Kryszalowicz")); uczniowie.add(new Uczen("Jan", "Kowalski"));
Zmień też nazwę pola uczen, na bardziej znaczącą aktualnyUczen. Dodaj też pole indeksAktualnegoUcznia, przechowujący numer aktualnie wyświetlanego ucznia.
//private Uczen uczen; private Uczen aktualnyUczen; //indeks aktualnego ucznia private int indeksAktualnegoUcznia;
Zmieniając nazwę pola musisz zmienić wszystkie jego użycia w klasie. Jeśli pole jest publiczne to także wszystkie użycia w innych klasach. Jednak nie przejmuj się, jeśli zapomnisz gdzieś poprawić nową nazwę, to kompilator Cię ostrzeże i wymusi poprawkę, takim oto komunikatem.
Przykład błędu kompilacji, po zmianie nazwy pola.
Zmień sposób inicjacji pola, niech będzie pobierany pierwszy uczeń z listy.
Kod inicjujący uczniów.
uczniowie.add(new Uczen("Pawel", "Kryszalowicz")); uczniowie.add(new Uczen("Jan", "Kowalski")); indeksAktualnegoUcznia = 0; aktualnyUczen = uczniowie.get(indeksAktualnegoUcznia);
Potrzebny jest już tylko kod, który będzie sprawdzał czy wciśnięto przyciski i zmieniał uczniów.
Stwórzmy metodę sluchajNaZmianieUcznia, aby kod był czytelny. Tak jak w poprzedniej części dotyczącej przedmiotów, dodajmy kod, który będzie aktualizował indeks aktualnego ucznia, pobierał go i odświeżał ekran.
private void sluchajNaZmianeUcznia() { if (poprzedniUczen.wasClicked()) { indeksAktualnegoUcznia = indeksAktualnegoUcznia - 1; if (indeksAktualnegoUcznia < 0) { indeksAktualnegoUcznia = uczniowie.size() - 1; } aktualnyUczen = uczniowie.get(indeksAktualnegoUcznia); odswiezEkran(); } if (nastepnyUczen.wasClicked()) { indeksAktualnegoUcznia = indeksAktualnegoUcznia + 1; if (indeksAktualnegoUcznia >= uczniowie.size()) { indeksAktualnegoUcznia = 0; } aktualnyUczen = uczniowie.get(indeksAktualnegoUcznia); odswiezEkran(); } }
Musimy jeszcze pamiętać o odświeżaniu etykiety przy zmianie ucznia - musisz przenieść zmienną lokalną do pola klasy.
public class DziennikUcznia extends World { … private Label uczenLabel;
Musimy jeszcze zmienić w konstruktorze inicjalizacji tego pola.
indeksAktualnegoUcznia = 0; aktualnyUczen = uczniowie.get(indeksAktualnegoUcznia); // etykiety nazw uczenLabel = new Label(aktualnyUczen.pobierzUcznia());
Pozostaje nam w metodzie odswiezEkran, dodać aktualizowanie tej wartości.
private void odswiezEkran() { ... // odswiezenie etykiety ucznia uczenLabel.setText(aktualnyUczen.pobierzUcznia()); }
Uruchom program i sprawdź czy zmiana ucznia działa. Czy wszystko działa prawidłowo? Jeśli zapomnisz lub pomylisz się przy sprawdzaniu poprawności indeksu możesz otrzymać komunikat jak poniżej. Numery linii mogą się różnić.
Jest to efektem błędu w poniższym kodzie, po kliknięciu kilkukrotnie na “następny uczeń” pojawi się wcześniej wspomniany błąd. Czy wiesz dlaczego do tego doszło?
if (poprzedniUczen.wasClicked()) { indeksAktualnegoUcznia = indeksAktualnegoUcznia - 1; if (indeksAktualnegoUcznia < 0) { indeksAktualnegoUcznia = uczniowie.size(); } aktualnyUczen = (Uczen) uczniowie.get(indeksAktualnegoUcznia); odswiezEkran(); }
Odpowiedzią jest, przypisanie złej wartości do pola indeksAktualnegoUcznia. Przypisanie aktualnej wielkości listy, oznacza, że przy pobieraniu odwołamy się do nieistniejącej wartości.
Menu górne - dodawanie ucznia i przedmiotu
W tym kroku dodamy nową funkcjonalność do aplikacji: dodawanie nowego ucznia i nowego przedmiotu.
Zajrzyj do projektu Gui Components. Znajduje się w nim klasa Menu, którą wykorzystamy, aby dodać nasze menu. Jej użycie jest trochę skomplikowane, ale na przykładzie wyjaśnię, jak stworzyć nasze menu.
Zacznijmy od umieszczenia na górze strony menu z jednym elementem Dziennik. Następnie dodajmy 2 pozycje rozwijane z tego menu Dodaj przedmiot, Dodaj ucznia.
W pierwszej kolejności dodajmy do świata pole z naszym menu.
private Menu menuGlowne;
Dodajmy metodę inicjujMenu. Stwórzmy w niej menu i umieśćmy na ekranie.
private void inicjujMenu() { ArrayList<String> pozycjeMenu = new ArrayList<String>(); pozycjeMenu.add("Dziennik"); menuGlowne = new Menu(pozycjeMenu); addObject(menuGlowne, 25, 5); }
Nie zapomnijmy wywołać tej metody w konstruktorze świata.
utworzElementyUwag(); inicjujMenu(); //ustawienie tla ekranu setBackground("tlo.jpg");
Skompiluj projekt. Czy menu się pojawia? Jeśli chcesz umieścić je w innym miejscu, możesz zrobić to zmieniając odpowiednie parametry w metodzie addObject.
Jak pewnie zauważyłeś, menu po kliknięciu nie rozwija się. Dodajmy 2 pozycje Dodaj ucznia oraz Dodaj przedmiot. W metodzie inicjujMenu umieść kod
private void inicjujMenu() { ... menuGlowne.addItem("Dziennik/Dodaj ucznia"); menuGlowne.addItem("Dziennik/Dodaj przedmiot"); }
Za pomocą metody addItem wywołanej na obiekcie menu, dodawane jest podrzędne menu. Trzeba najpierw podać nazwę głównego elementu, a następnie po ukośniku nazwę podmenu, które pojawi się po rozwinięciu.
Uruchom teraz aplikację. Czy rozwijanie już działa? Jeśli wszystko jest ok, to w efekcie powinieneś po uruchomieniu aplikacji zobaczyć ekran jak poniżej.
Musimy teraz dodać słuchacza na kliknięcie w menu. Przydatna tutaj będzie metoda Menu.getItemPressed(). Pozwoli ona sprawdzić jaki element menu został wciśnięty. Stwórz nową metodę, która obsłuży wciśniecie przycisku.
private void sluchajNaMenu() { String wybraneMenu = menuGlowne.getItemPressed(); if (wybraneMenu != null) { if (wybraneMenu.startsWith("Dziennik/")) { //wybranie nazwy po ukosniku / wybraneMenu = wybraneMenu.substring(wybraneMenu.indexOf('/') + 1); if (wybraneMenu.equals("Dodaj ucznia")) { // otwarcie okna dodawania ucznia } else if (wybraneMenu.equals("Dodaj przedmiot")) { // otwarcie okna dodawania przedmiotu } } } }
Powyższa metoda jest odrobinę skomplikowana. Pierwszy warunek:
if (wybraneMenu != null)
sprawdza czy kliknięto na menu. Kolejny:
if (wybraneMenu.startsWith("Dziennik/")) {
to sprawdzenie czy kliknięto na element, który znajduje się pod menu Dziennik. Kolejny krok:
wybraneMenu = wybraneMenu.substring(wybraneMenu.indexOf('/') + 1);
to pobranie drugiego członu nazwy. Najpierw pobieram miejsce ukośnika w napisie,
wybraneMenu.indexOf('/')
a następnie “wycinam” napis, zostawiając część, od tego miejsca + 1.
wybraneMenu.substring(wybraneMenu.indexOf('/') + 1)
Dodaj jeszcze wywołanie metody w metodzie act.
@Override public void act() { ... sluchajNaMenu(); }
Brakuje nam jeszcze uruchomienia rzeczywistych akcji, które pozwolą nam na dodanie nowego przedmiotu i ucznia. Zrobimy to za pomocą wyskakujących okien.
Dodaj pole typu InputWindow, które pozwoli zapytać nam o nazwę przedmiotu. Zainicjuj je przy deklaracji.
private Menu menuGlowne; private InputWindow wprowadzPrzedmiotOkno = new InputWindow("Podaj nazwe przedmiotu", 15);
Teraz powinniśmy pokazać to okno przy kliknięciu na odpowiednie menu. W metodzie sluchajNaMenu wywołaj wyświetlanie okna w miejscu gdzie wcześniej był komentarz.
} else if (wybraneMenu.equals("Dodaj przedmiot")) { // otwarcie okna dodawania przedmiotu wprowadzPrzedmiotOkno.toggleShow(""); }
Okno powinno wyglądać jak poniżej.
Teraz dodaj metode sluchajNaDodaniePrzedmiotu, umieść sprawdzanie czy dodano nowy przedmiot. Jeśli tak się stało - dodaj go do listy. Zmień także pole aktualneWyniki, aby wskazywało na nowo dodany przedmiot. Jakie pola musisz zaktualizować? Konieczne będzie także odświeżenie ekranu.
private void sluchajNaDodaniePrzedmiotu() { String nazwaPrzedmiotu = wprowadzPrzedmiotOkno.getResult(); if (nazwaPrzedmiotu != null && !nazwaPrzedmiotu.isEmpty()) { przedmioty.add(nazwaPrzedmiotu); aktualnyPrzedmiot = przedmioty.size() - 1; aktualneWyniki = aktualnyUczen.pobierzWyniki(nazwaPrzedmiotu); odswiezEkran(); } }
Pamiętaj o wywołaniu metody w metodzie act.
public void act() { ... sluchajNaDodaniePrzedmiotu(); }
Uruchom aplikację i sprawdź czy dodawanie nowego przedmiotu działa.
Druga część ćwiczenia będzie trochę trudniejsza. Stworzymy w niej nowe okno, w którym dodamy dwa pola imię i nazwisko do wprowadzania ucznia.
Nowe okno to będzie nowa klasa - aktor, która będzie dziedziczyć z klasy Window. Przykładem takiej klasy jest TextWindow, którą możesz podejrzeć w GUI Components.
Stwórz nową klasę przez kliknięcie PPM na klasę aktora i wybranie New subclass.
Swoją klasę nazwij NowyUczenWindow. Nowa klasa będzie miała metodę act i będzie dziedziczyć po klasie Actor. Zmień kod tak, aby dziedziczyła z klasy Window. Musisz też dodać odpowiedni konstruktor, który wywoła konstruktor klasy Window. Gdybyś tego nie zrobił, pojawiłby się błąd kompilacji.
Klasa okna powinna wyglądać jak poniżej.
public class NowyUczenWindow extends Window { public NowyUczenWindow() { super("Dodaj ucznia"); } public void act() { // Add your action code here. } }
Teraz dodaj dwa pola typu TextBox: imieBox, nazwiskoBox oraz dwa przyciski: Enter i Cancel.
public class NowyUczenWindow extends Window { private TextBox imieBox, nazwiskoBox; private Button enterBtn = new Button("Enter", new Point(50, 23)); private Button cancelBtn = new Button("Cancel", new Point(50, 23));
Zainicjuj je w konstruktorze. Zrób to tak samo jak w klasie Dziennik, aby były widoczne na ekranie. Pamiętaj, że metoda super(..), musi być wywołana jako pierwsza instrukcja w konstruktorze.
public NowyUczenWindow() { super("Dodaj ucznia"); imieBox = new TextBox(new Point(50, 20), ""); nazwiskoBox = new TextBox(new Point(50, 20), "");
Teraz przejdziemy do trudniejszej części, a mianowicie rozmieszczenia wszystkiego nowym oknie. W tym celu wykorzystamy klasę Container, która działa trochę jak siatka. Np. takie wywołanie
new Container(new Point(2, 3));
Mówi, że kontener jest podzielony na dwie kolumny i trzy wiersze.
Kontener, podzielony na dwie kolumny i trzy wiersze.
Dodajmy teraz w konstruktorze takie kontenery, które pozwolą na umieszczenie w nich etykiet i pól.
Container c = new Container(new Point(1, 3)); Container kontenerImie = new Container(new Point(2, 1)); kontenerImie.addComponent(new Label("Imie:")); kontenerImie.addComponent(this.imieBox); c.addComponent(kontenerImie); Container kontenerNaziwsko = new Container(new Point(2, 1)); kontenerNaziwsko.addComponent(new Label("Nazwisko:")); kontenerNazwisko.addComponent(this.nazwiskoBox); c.addComponent(kontenerNazwisko); Container btns = new Container(new Point(2, 1)); btns.addComponent(this.enterBtn); btns.addComponent(this.cancelBtn); c.addComponent(btns); addContainer(c);
W pierwszej części tworzymy kontener z jedną kolumną i trzema wierszami.
Później tworzymy kontener z dwiema kolumnami i jendym wierszem, do którego dodajemy najpierw etykietę imię i pole tekstowe.
Następnie dodajemy ten kontener do głównego kontenera.
W kolejnym kroku postępujemy podobnie: w drugim wierszu dodamy kolejny kontener z nazwiskiem.
Ostatni kontener przechowa nam dwa przyciski, które zamkną okno.
Na końcu główny kontener umieszczamy w oknie, wywołując addContainer(c).
Najpierw obsłużmy przypadek, gdy ktoś kliknie przycisk Cancel. W metodzie act dodaj nasłuchiwanie na wciśnięcie przycisku. Pamiętaj, żeby w metodzie dodać wywołanie metody super.act(). Wywołuje to metodę act z klasy nadrzędnej. Jest to konieczne do prawidłowego działania.
public void act() { super.act(); if (this.cancelBtn.wasClicked()) { this.toggleShow(); }
Teraz, na wzór poprzednich okien, potrzebujemy metody, która zwróci nam dane, jeśli wciśnięty zostanie przycisk Enter. Najpierw dodaj pole nowyUczen, które będzie przechowywać wynik okna.
public class NowyUczenWindow extends Window { ... private Uczen nowyUczen;
Teraz w metodzie act dodaj sprawdzanie czy wciśnięto przycisk Enter, jeśli tak, to niech utworzy nowy obiekt Uczen, oraz wyczyści pola tekstowe i ukryje wyświetlane okno.
if (this.enterBtn.wasClicked()) { this.nowyUczen = new Uczen(imieBox.getText(), nazwiskoBox.getText()); imieBox.setText(""); nazwiskoBox.setText(""); this.toggleShow(); }
Teraz potrzebujemy metody getResult, która zwróci obiekt ucznia, jeśli taki został utworzony. Po wywołaniu tej metody, musimy to pole wyczyścić.
public Uczen getResult() { Uczen temp = nowyUczen; nowyUczen = null; return temp; }
Musimy do zmiennej tymczasowej temp przypisać aktualną wartość ucznia, następnie czyścimy pole ucznia i zwracamy obiekt, który był wcześniej.
Skompiluj projekt, jeśli wszystko jest ok, możemy wykorzystać nasze własne okno. Przejdź do klasy DziennikUcznia.
Najpierw dodajmy nasze nowe okno, podobnie jak okno do wprowadzania przedmiotu.
private InputWindow wprowadzPrzedmiotOkno = ... private NowyUczenWindow wprowadzUczniaOkno = new NowyUczenWindow();
Teraz wyświetl to okno w metodzie sluchajNaMenu.
if (wybraneMenu.equals("Dodaj ucznia")) { // otwarcie okna dodawania ucznia wprowadzUczniaOkno.toggleShow();
Teraz zostaje nam już tylko dodanie słuchacza na oknie ucznia, który doda ucznia, jeśli ten został wybrany. Stwórz metodę sluchajNaDodanieUcznia.
private void sluchajNaDodanieUcznia() { Uczen nowyUczen = wprowadzUczniaOkno.getResult(); if (nowyUczen != null) { uczniowie.add(nowyUczen); aktualnyUczen = nowyUczen; indeksAktualnegoUcznia = uczniowie.size() - 1; odswiezEkran(); } }
Podobnie jak w innych metodach, sprawdzamy czy okno zwraca rezultat. Jeśli tak, to dodajemy ucznia na listę uczniów. Nowego ucznia ustawiamy także jako aktualnego, ustawiamy też indeksAktualnegoUcznia na ostatni element w liście, czyli na numer nowo dodanego ucznia. Na końcu odświeżamy ekran.
Skompiluj program. Sprawdź czy przycisk dodawania ucznia działa. Przetestuj aplikację, czy wszystko jest ok?
Zapis danych do pliku
W tym kroku nauczysz się jak korzystać z plików - jak zapisać dane do pliku i je odczytać. Dzięki temu, będzie można dodawać uczniów, przedmioty, wstawiać oceny, a po zamknięciu aplikacji będzie można ponownie te dane wczytać, podając nazwę pliku.
Na początku, przygotujmy aplikację do obsługi zapisu i odczytuj do pliku. W następnych krokach zrobimy właściwy odczyt i zapis pliku.
Dodajmy najpierw dwa pola okien w klasie DziennikUcznia z pytaniami o nazwę pliku. Jedno pole z nazwą eksportowanego pliku, drugie z nazwą importowanego.
private InputWindow plikEksportOkno = new InputWindow("Podaj nazwe pliku", 30); private InputWindow plikImportOkno = new InputWindow("Podaj nazwe pliku", 30);
Następnie dodajmy do menu nowe opcje wyboru. W metodzie inicjujMenu zmień kod na:
private void inicjujMenu() { ArrayList<String> pozycjeMenu = new ArrayList<String>(); pozycjeMenu.add("Dziennik"); pozycjeMenu.add("Operacje"); menuGlowne = new Menu(pozycjeMenu); addObject(menuGlowne, 50, 5); menuGlowne.addItem("Dziennik/Dodaj ucznia"); menuGlowne.addItem("Dziennik/Dodaj przedmiot"); menuGlowne.addItem("Operacje/Eksportuj"); menuGlowne.addItem("Operacje/Importuj"); }
Dodaliśmy nowe menu główne Operacje, poszerzyliśmy menu (metoda addObject), aby ładnie się wyświetlało i na końcu dodaliśmy dwa podmenu: Importuj i Eksportuj.
Uruchom aplikację i zobacz czy menu jest widoczne.
Teraz do metody sluchajNaMenu dodaj kod, który pokaże nowo dodane okna. Niech w oknach pojawią się domyślne wartości dane.txt.
if (wybraneMenu.startsWith("Dziennik/")) { ... } else if (wybraneMenu.startsWith("Operacje")) { //wybranie nazwy po ukosniku / wybraneMenu = wybraneMenu.substring(wybraneMenu.indexOf('/') + 1); if (wybraneMenu.equals("Eksportuj")) { //pokazanie okna z domyslna wartoscia plikEksportOkno.toggleShow("dane.txt"); } else if (wybraneMenu.equals("Importuj")) { //pokazanie okna z domyslna wartoscia plikImportOkno.toggleShow("dane.txt"); } }
Kod jest analogiczny do tego z poprzedniego ćwiczenia. Metoda
plikImportOkno.toggleShow("dane.txt");
wyświetla nowe okno z ustawionym tekstem "dane.txt".
Uruchom aplikację i zobacz czy wyświetlanie okien działa prawidłowo.
Teraz dodajmy kod, który wywoła się, gdy ktoś wprowadzi nazwę pliku do eksportu.
Stwórz metodę sluchajNaImportExport.
private void sluchajNaImportExport() { String nazwaPlikuExportu = plikEksportOkno.getResult(); //jesli wprowadzono nazwe if (nazwaPlikuExportu != null) { //eksportuje do pliku } }
W miejscu komentarza //eksportuje do pliku będziemy musieli wstawić kod do eksportu. Wywołaj jeszcze metodę w metodzie act.
public void act() { ... sluchajNaImportExport(); }
Teraz napiszmy kod do eksportu danych do pliku.
Stwórz nową klasę Eksporter, w której umieścimy kod do zapisu pliku. Usuń niepotrzebne metody. Na górze pliku umieść importy tak, jak w przykładzie poniżej.
import java.io.*; import java.util.*; import org.apache.commons.io.*; public class Eksporter { public Eksporter() { } } </syntaxhighlight Importy będą nam potrzebne przy używaniu klas związanych z plikami oraz list.<br /> Teraz dodajmy dwa pola '''uczniowe''' i '''przedmioty''', które będę przechowywać dane, które będziemy zapisywać w pliku. Zróbmy tak, aby te dane trzeba było podać przy tworzeniu obiektu- musimy więc stworzyć odpowiedni konstruktor. <syntaxhighlight lang="java"> public class Eksporter { private ArrayList<Uczen> uczniowie; private final ArrayList<String> przedmioty; public Eksporter(ArrayList<Uczen> uczniowie, ArrayList<String> przedmioty) { this.uczniowie = uczniowie; this.przedmioty = przedmioty; }
Wyrażenie this.uczniowe = uczniowe mówi, żeby do pola (nazwa poprzedzona słówkiem kluczowym this.) przypisać parametr uczniowie.
Teraz musimy napisać metodę, która przygotuje nam dane do zapisu. Zróbmy metodę, która będzie zwracała listę napisów. Później, każdy element z listy będzie zapisany w oddzielnym wierszu w pliku.
Musimy jeszcze ustalić jak zapisać dane, żeby były łatwe do odczytania. Zróbmy to w ten sposób:
- W pierwszej linii wypiszmy wszystkie przedmioty po przecinku
- W kolejnych liniach będziemy zapisywać uczniów jeden pod drugim wraz z ich wynikami
- w pierwszej linii ucznia zapiszmy jego imię
- w drugiej nazwisko
- następnie dla każdego przedmiotu będziemy pobierać wyniki i zapisywać w ten sposób
- najpierw nazwa przedmiotu
- później lista ocen po przecinku
- następnie lista uwag po przecinku
Zaczynajmy!
Najpierw zapiszmy nazwę przedmiotów w pierwszej linii. Stwórz metodę pobierzDane i do listy zapisz przedmioty po przecinku.
private ArrayList<String> pobierzDane() { ArrayList<String> linie = new ArrayList<String>(); //inicujuje pusta linie String liniaPrzedmiotu = ""; //zapis plikow po przecinku w pierwszej linii for (int i = 0; i < this.przedmioty.size(); i++) { String przedmiot = this.przedmioty.get(i); if (i > 0) { liniaPrzedmiotu = liniaPrzedmiotu + ","; } liniaPrzedmiotu = liniaPrzedmiotu + przedmiot; } //zapisuje linie z przedmiotami linie.add(liniaPrzedmiotu); return linie; }
Jak możesz zauważyć, najpierw tworzona jest pusta lista i pusty napis.
Następnie w pętli for pobieramy każdy przedmiot i zapisujemy na liście.
Jeśli przedmiot nie jest pierwszym if (i > 0) { to dodajmy przecinek przed nowym przedmiotem.
Na końcu tak stworzony napis dodajemy do listy linie. Zwracamy tę listę.
Teraz dodajmy metodę publiczną, która zapisze zwracaną listę do pliku.
public void eksportuj(String nazwaPliku) { //pobranie listy wierszy do zapisania ArrayList<String> zawartosc = pobierzDane(); try { //zapis do pliku FileUtils.writeLines(new File(nazwaPliku), zawartosc); } catch (IOException e) { //zlapanie wyjatku System.out.println("Zapis do pliku" + nazwaPliku + " nieudany!"); e.printStackTrace(); } }
Metoda jest publiczna, po to, aby można było wywołać ją z innych klas.
W pierwszym kroku metoda pobiera listę wierszy do zapisania.
Następnie w znanym nam już bloku try{ } catch() łapie wyjątek IOException, który jest wyjątkiem występującym, gdy zapis pliku się nie powiedzie. W takim wypadku, na konsoli za pomocą System.out.println wyświetlamy komunikat o błędzie.
Z kolei e.printStackTrace() wyświetla szczegóły wyjątku.
Najważniejsze dla nas jest jednak wywołanie:
FileUtils.writeLines(new File(nazwaPliku), zawartosc);
które zapisuje w nowym pliku o podanej w parametrze nazwie (new File(nazwaPliku)), zawartość, czyli listę wierszy.
Metoda jest gotowa, wywołajmy ją z klasy DziennikUcznia, w metodzie sluchajNaImportExport.
if (nazwaPlikuExportu != null) { //eksportuje do pliku Eksporter exporter = new Eksporter(uczniowie, przedmioty); exporter.eksportuj(nazwaPlikuExportu); }
Najpierw tworzymy obiekt eksportera z listą uczniów i przedmiotów. Następnie wywołujemy eksport podając nazwę wprowadzonego pliku.
Skompiluj program. Dodaj jakiś przedmiot i uruchom eksport. Podaj domyślną nazwę pliku.
Kliknij Enter. Teraz przejdź do katalogu w którym tworzyłeś projekt. Powinien pojawić się nowy plik o nazwie dane.txt.
Otwórz go. Powinieneś ujrzeć taki wpis:
Świetnie. Teraz możemy zapisać resztę danych.
Przejdź do klasy Exporter. Musimy zmodyfikować metodę pobierzDane, tak aby wpisała resztę pozycji.
Dodajmy pętlę, która przejdzie po wszystkich uczniach:
for (int i = 0; i < uczniowie.size(); i++) { Uczen uczen = uczniowie.get(i); } return linie;
Teraz po pobraniu ucznia dodajmy dwie linie z imieniem i nazwiskiem.
Uczen uczen = uczniowie.get(i); // linia z imieniem linie.add(uczen.pobierzImie()); // linia z nazwiskiem linie.add(uczen.pobierzNazwisko());
Teraz skompiluj ponownie aplikację. Wyeksportuj ponownie dane, w pliku powinny pojawić się teraz imiona i nazwiska uczniów.
Teraz pozostało nam już tylko dodanie wyników z poszczególnych przedmiotów.
W metodzie pobierzDane w środku pętli uczniów, pod dodaniem nazwiska ucznia do listy, dodaj nową pętlą chodzącą po przedmiotach.
// linia z nazwiskiem linie.add(uczen.pobierzNazwisko()); // dla kazdego przedmiotu dodaje wpis ucznia for (int j = 0; j < przedmioty.size(); j++) { String przedmiot = przedmioty.get(j); }
Następnie zapisz w kolejnej linii nazwę przedmiotu.
String przedmiot = przedmioty.get(j); //nazwa przedmiotu linie.add(przedmiot);
Teraz dodajmy metodę pomocniczą, która zapisze listę napisów po przecinku. Dzięki temu nie będziemy musieli przy zapisie uwag i ocen powtarzać tych samych czynności. Stwórz metodę listaDoLinii.
private String listaDoLinii(ArrayList<String> lista) { //inicujuje pusta linie String linia = ""; //zapis plikow po przecinku w pierwszej linii for (int i = 0; i < lista.size(); i++) { String wartosc = lista.get(i); if (i > 0) { linia = linia + ","; } linia = linia + wartosc; } return linia; }
Metoda działa podobnie jak pętla do zapisu przedmiotów. W parametrze przyjmuje listę napisów. Następnie tworzy pustą linię, do której w pętli dodaje każdy element z listy. Pomiędzy elementami wstawia przecinek. Na końcu zwraca stworzoną linię.
Wykorzystajmy to teraz przy zapisie wyników ucznia. Poniżej linii dodawania przedmiotu dodaj pobieranie wyników z przedmiotu
linie.add(przedmiot); // pobieram przedmiot WynikiUcznia wynikiUcznia = uczen.pobierzWyniki(przedmiot); if (wynikiUcznia == null) { //jesli uczen nie ma wynikow z danego przedmiotu to zapisuje 2 puste linie linie.add(""); linie.add(""); } else { //zapisuje linie ocen String liniaOcen = listaDoLinii(wynikiUcznia.pobierzOceny()); linie.add(liniaOcen); //zapisuje linie uwag String liniaUwag = listaDoLinii(wynikiUcznia.pobierzUwagi()); linie.add(liniaUwag); }
Najpierw pobieramy wyniki.
Następnie sprawdzamy, czy uczeń posiada jakieś oceny z danego przedmiotu
if (wynikiUcznia == null) {
Jeśli nie ma żadnych ocen, dodajemy po prostu dwie puste linie.
Jeśli jednak posiada oceny } else {
to tworzymy wiersze z linią ocen i uwag.
Najpierw za pomocą nowej metody listę zamieniamy na napis listaDoLinii(wynikiUcznia.pobierzOceny())
Następnie linię dodajemy do listy
linie.add(liniaOcen);
To już jest wszystko. Zapisaliśmy wszystkie dane, które są nam potrzebne aby odtworzyć dziennik ucznia.
Skompiluj program, dodaj kilka uwag i kilka ocen. Wyeksportuj plik i zobacz czy wszystko działa poprawnie. Plik powinien wyglądać mniej więcej tak, jak poniżej.
Świetnie mamy już eksport. Teraz musimy dodać funkcjonalność odczytu pliku.
Zacznijmy od stworzenia klasy Importer. Podobnie jak Eksporter będzie przechowywać listę uczniów i przedmiotów. Nie zapomnij o importach.
import java.io.*; import java.util.*; import org.apache.commons.io.*; public class Importer { private ArrayList<Uczen> uczniowie; private ArrayList<String> przedmioty; }
Dodajmy teraz metodę importuj, która z pliku odczyta linie i stworzy odpowiednie obiekty. Parametrem metody będzie nazwa pliku. Zacznijmy od wczytania linii z pliku. Robimy to podobnie do metody zapisującej, z tą różnicą, że nie podajemy linii, które chcemy zapisać, a odczytujemy je i przypisujemy do zmiennej lokalnej.
private void importuj(String nazwaPlik) { //odczyt pliku do linii try { List<String> linie = FileUtils.readLines(new File(nazwaPlik)); } catch (IOException e) { System.out.println("Nie udalo sie odczytac pliku " + nazwaPlik); e.printStackTrace(); } }
Podobnie jak przy eksporcie, operując na plikach, musimy łapać wyjątek IOException. Odczyt zwraca obiekt List, a nie ArrayList, ale praca z nim jest dokładnie taka sama.
Następnym krokiem będzie odczytanie listy przedmiotów. Zróbmy w pierwszej kolejności metodę pomocniczą, która linię podzieli po przecinkach (rozdzielają one kolejne wpisy). Dodajmy metodę liniaDoListy.
private ArrayList<String> liniaDoListy(String linia) { //przepisanie tablicy na liste ArrayList<String> lista = new ArrayList<String>(); //jesli linia jest pusta to nie dodajemy if (linia.length() > 0) { //rozdzielenie listy po przecinku String[] wartosci = linia.split(","); for (String wartosc : wartosci) { lista.add(wartosc); } } return lista; }
Na początku tworzona jest pusta lista. Następnie sprawdzamy czy linia jest nie jest pusta. Jeśli są w niej wartości to rozdzielamy je po przecinku linia.split(","). W efekcie otrzymujemy tablicę wartości. Kolejny krok to już tylko przejście po tablicy i dodanie kolejnych elementów do listy. Na końcu zwracamy listę.
Tę metodę wykorzystamy przy odczytywaniu wartości. Zacznijmy od pobierania przedmiotów.
List<String> linie = FileUtils.readLines(new File(nazwaPlik)); // pierwsza linia to przedmioty String liniaPrzedmiotow = linie.get(0); przedmioty = liniaDoListy(liniaPrzedmiotow);
Wiemy, że w pierwszej linni znajdują się przedmioty, dlatego możemy ją pobrać po indeksie 0. Następnie wywołujemy metodę, która rozdzieli napis po przecinkach i zwróci listę. Od razu przypisujemy tę listę do pola przedmioty.
Następnie musimy wczytać uczniów. Zacznijmy od zainicjalizowania listy uczniów. Następnie w pętli czytajmy kolejne liniie.
//inicujuje liste uczniow uczniowie = new ArrayList<Uczen>(); // startuje od drugiej linii, czyli indeksu 1 wczytujac kolejno: imie, nazwisko, przedmiot1, oceny, uwagi (powtarzam dla kazdego przedmiotu), nastepnie kolejny uczen // uwaga! ostatnia linia jest pusta, wiec musze wczytac o 1 linie mniej, stad line.size() -1 for (int i = 1; i < linie.size() - 1; ) { }
Zauważ, że w trzecia część pętli for jest pusta. Wartość i będziemy dla wygody zmieniać samemu.
Odczytajmy teraz imię i nazwisko, stwórzmy nowego ucznia i dodajmy go do listy.
//najpierw jest imie String imie = linie.get(i); String nazwisko = linie.get(i + 1); // tworze ucznia i dodaje do listy Uczen uczen = new Uczen(imie, nazwisko); uczniowie.add(uczen); // zwiekszam licznik o 2 odczytane linie i = i + 2;
Wczytujemy kolejne dwie linie i oraz i+1. Tworzymy nowy obiekt Ucznia z tymi wartościami i dodajemy go do listy. Na końcu zwiększamy i o 2, bo odczytaliśmy 2 linie.
Teraz zmieńmy trochę klasę Uczen. W tej chwili metoda dodajWynik nie zwraca wartości. Zmieńmy ją, aby zwracała nam tworzone wyniki. Przejdź do klasy uczeń i zmień metodę dodajWynik, aby wyglądała jak poniżej.
public WynikiUcznia dodajWyniki(String przedmiot) { //dodaje nowy przedmiot na mape, tworzy nowy obiekt, aby moc korzystac z przedmiotu WynikiUcznia wynikiUcznia = new WynikiUcznia(); przedmioty.put(przedmiot, wynikiUcznia); return wynikiUcznia; }
Przejdź teraz do klasy WynikiUcznia i dodaj tam dwie metody dodajUwagi i dodajOceny, które pozwolą na dodanie całej listy.
public void dodajUwagi(ArrayList<String> uwagi) { this.uwagi.addAll(uwagi); } public void dodajOceny(ArrayList<String> oceny) { this.oceny.addAll(oceny); }
Wróćmy teraz do klasy Importer i metody import. Dodajmy pętlę po liście wczytanych przedmiotów, dzięki temu będziemy wiedzieli ile wyników mamy odczytać.
// dla kazdego przedmiotu odczytuje odpowiednio nazwe, oceny i uwagi, tak jak wczesniej zapisalismy for (int j = 0; j < przedmioty.size(); j++) { }
Zwróć uwagę, że mamy tutaj zmienną j. Jednak w pętli będziemy korzystać ze zmiennej i. Wczytajmy najpierw nazwę przedmiotu i dodajmy wyniki. Przypiszmy zwróconą wartość do zmiennej wynikiUcznia.
for (int j = 0; j < przedmioty.size(); j++) { String nazwa = linie.get(i); //dodaje przedmiot to ucznia i podstawam zmienna pod wyniki WynikiUcznia wynikiUcznia = uczen.dodajWyniki(nazwa);
Teraz odczytaj kolejną linię - jest to linia ocen. Przekształć ją na listę i dodaj oceny do wyników.
String liniaOcen = linie.get(i + 1); ArrayList<String> oceny = liniaDoListy(liniaOcen); //dodaje liste ocen wynikiUcznia.dodajOceny(oceny)
Następnie zrób to samo z uwagami.
String liniaUwag = linie.get(i + 2); ArrayList<String> uwagi = liniaDoListy(liniaUwag); wynikiUcznia.dodajUwagi(uwagi);
Na końcu program zwiększy i o 3 odczytane linie.
i = i + 3;
Mamy już metodę, która odczyta dane. Teraz wywołajmy ją w konstruktorze, aby po stworzeniu obiektu plik został odczytany i pola były zainicjowane. Dodaj konstruktor z parametrem - nazwaPliku i wywołaniem metody.
public Importer(String nazwaPlik) { //laduje dane importuj(nazwaPlik); }
Zostało już nam tylko dodać dwie metody zwracające wczytane przedmioty i uczniów. Pamiętaj, że muszą być publiczne.
public ArrayList<Uczen> pobierzUczniow() { return uczniowie; } public ArrayList<String> pobierzPrzedmioty() { return przedmioty; }
Świetnie, zostało już nam tylko użycie tej klasy w klasie DziennikUcznia, po wywołaniu okna importu i podaniu pliku. Przejdź do klasy DziennikUcznia i metody sluchajNaImportExport. Dodaj tam sprawdzanie czy podano nazwę pliku w oknie importu. Jeśli tak, stwórz nowy importer.
String nazwaPlikuImportu = plikImportOkno.getResult(); //jesli wprowadzono nazwe if (nazwaPlikuImportu != null) { // importuje Importer importer = new Importer(nazwaPlikuImportu); ... }
Świetnie, teraz pozostaje nam zaktualizowanie pól dziennika. Najpierw zaktualizujmy przedmioty. Zarówno listę, jak i indeks aktualnie wyświetlanego.
//przypisuje wartosci przedmiotu przedmioty = importer.pobierzPrzedmioty(); // resetuje aktualnie widoczne zmienne aktualnyPrzedmiot = 0;
Następnie, pobierzmy uczniów.
//aktualizuje uczniow uczniowie = importer.pobierzUczniow(); //resetuje wartosci, aby prawidlowo wyswietlaly sie na ekranie indeksAktualnegoUcznia = 0; aktualnyUczen = uczniowie.get(0); String nazwaAktualnegoPrzedmiotu = przedmioty.get(aktualnyPrzedmiot); aktualneWyniki = aktualnyUczen.pobierzWyniki(nazwaAktualnegoPrzedmiotu);
Aktualizujemy tutaj listę uczniów, indeks aktualnego ucznia, a także aktualnego ucznia i aktualne wyniki.
Na końcu wywołajmy odświeżenie ekranu.
//na koniec odswiezam ekran odswiezEkran();
Skompiluj projekt i uruchom aplikację. Dodaj oceny, uwagi, przedmioty, uczniów. Na koniec wyeksportuj plik.
Następnie zamknij aplikację i uruchom ponownie. Jak zwykle wszystkie dane są puste. Wywołaj import, podając tę samą nazwę pliku, do którego eksportowałeś i kliknij Enter.
W tym momencie aplikacja powinna się odświeżyć i powinieneś zobaczyć dane, które zapisałeś.
Szyfrowanie pliku
W poprzednim rozdziale zapisywaliśmy dane do pliku. Wadą rozwiązania, jest to, że każdy może otworzyć plik i zobaczyć jego zawartość.
Dodajmy opcję szyfrowania pliku. W Greenfoot dodaliśmy klasę pl.org.coderdojo.ceasar.ROT13Encoder
która dostarcza nam opcję szyfrowania według prostego algorytmu.
Klasa ROT13Encoder jest realizacją tak zwanego Szyfru Cezara - całość opiera się zamianie każdej litery w szyfrowanym słowie inną, oddaloną o stałą liczbę pozycji. W tym wypadku jest to 13 pozycji, przez co litera A zamienia się w N, litera B w O itd. Dodatkowo, implementacja tej klasy nie ogranicza się do liter - szyfrowane są też znaki przestankowe oraz cyfry.
Klasa ROT13Encoder dziedziczy z klasy AbstractCeasarEncoder. Klasa AbstractCeasarEncoder jest dość specjalna - kiedy ją otworzysz, zauważysz pewnie, że niektóre metody… istnieją tylko jako nagłówki! O co chodzi? Zwróć uwagę na słowo, które stoi przed nazwą klasy:
public abstract class AbstractCeasarEncoder {
Jest to tak zwana klasa abstrakcyjna. Składa się ona zarówno z “pełnych” metod, które zostaną odziedziczone i można z nich bez problemów korzystać w klasach potomnych, jak również i metody abstrakcyjne. Takie metody posiadają również słowo abstract w deklaracji, jak w naszym wypadku dla klasy AbstractCeasarEncoder:
public abstract char encode(char toEncode); public abstract String encode(String toEncode); public abstract char decode(char toDecode); public abstract String decode(String toDecode);
Słowo abstract przy nazwie klasy oznacza, że gdyby klasa mogła mówić, to powiedziałaby nam coś takiego: “Hej! Jestem klasą abstrakcyjną! Oprócz metod, z których możesz korzystać i które mogą odziedziczyć inne klasy, mam też metody abstrakcyjne. Nie wiem, jak działają, bo mnie to nie obchodzi. Wiem natomiast, że klasy dziedziczące po mnie muszą je posiadać i muszą to być metody pełne, już bez słowa abstract!”. Natomiast każda metoda, przy której znajduje się słowo abstract mówi nam coś takiego: “Jestem metodą abstrakcyjną! Nie wiem, jaki kod mam miedzy klamrami, bo mnie to nie obchodzi. Wiem natomiast, jak się nazywam, jakiego typu dane zwracam oraz jakie dane przyjmuję na wejście. Metoda, która będzie mnie tworzyła w klasie potomnej MUSI już mieć kod i nie może mieć słowa abstract”. Może trochę pyszałkowate (bo są ale nie wiedzą nic), ale tego typu klasy i metody są programistom potrzebne.
Skoro już znamy zasadę działania klas abstrakcyjnych, przyjrzyjmy się zatem polom i pełnym metodom w klasie AbstractCeasarEncoder. Na początek pola i ich opisy:
typ zmiennej | nazwa | rola |
---|---|---|
private int | shiftBy | Mówi, o ile miejsc należy przesunać się aby zaszyfrować lub odszyfrować znak. |
protected int | asciiMaxValue | Określa rozmiar tablicy, w której będą przechowywane znaki wykorzystywane do szyfrowania iodszyfrowania. |
protected static char[] | asciiTable | Tabela, w której przechowywane będą znaki - domyślnie nie zainicjalizowana. |
Konstruktor klasy jest jeden i bezparametrowy:
public AbstractCeasarEncoder() { if (asciiTable == null) { initializeASCIITable(); } }
Jak widać, jeśli tabela nie jest utworzona, wywołana zostanie metoda initializeASCIITable(), która wygląda w ten sposób:
protected void initializeASCIITable () { asciiTable = new char[asciiMaxValue]; for (int index = 0; index < asciiMaxValue; index++) { asciiTable[index] = (char)index; } }
Rola tej metody jest również prosta - na początku zostanie utworzona tabela znaków, o rozmiarze zadeklarowanym w zmiennej asciiMaxValue. Następnie, w pętli for, która iteruje zmienną index od zera do momentu osiągnięcia równości ze zmienną asciiMaxValue, zapisany zostanie w komórce tabeli w pozycji oznaczonej numerem index znak, który jest określony za pomocą aktualnej wartości zmiennej index w pętli.
Następnie metody getShiftBy() i setShiftBy()
public int getShiftBy() { return shiftBy; } public void setShiftBy(int shiftBy) { this.shiftBy = shiftBy; }
które służą odpowiednio do pobierania i ustawiania wartości zmiennej shiftBy. Ostatnią z metod jest getCharAtPosition(), której zadaniem jest pobranie znaku z określonego miejsca w tablicy.
protected char getCharAtPosition(int index) { return asciiTable[index]; }
Pozostałe metody - decode() oraz encode() są abstrakcyjnymi metodami i zostaną zaimplementowane w klasie-potomku.
Kolej omówić klasę ROT13Encoder. Jak wielokrotnie było wspominane, dziedziczy ona z klasy AbstractCeasarEncoder. Posiada zatem zaimplementowane metody decode() oraz encode().
@Override public char encode(char toEncode) { int charIndex = (int)toEncode; int charIndexAfterEncoding = charIndex + getShiftBy(); if (charIndexAfterEncoding >= asciiMaxValue) { charIndexAfterEncoding = charIndexAfterEncoding % asciiMaxValue; } return getCharAtPosition(charIndexAfterEncoding); } @Override public String encode(String toEncode) { char[] chars = toEncode.toCharArray(); StringBuilder stringBuild = new StringBuilder(); for (int i = 0 ; i < chars.length; i++) { char encoded = encode(chars[i]); stringBuild.append(encoded); } return stringBuild.toString(); } @Override public char decode(char toDecode) { int charIndex = (int)toDecode; int charIndexAfterEncoding = charIndex - getShiftBy(); if (charIndexAfterEncoding < 0) { charIndexAfterEncoding = asciiMaxValue + charIndexAfterEncoding; } return getCharAtPosition(charIndexAfterEncoding); } @Override public String decode(String toDecode) { char[] chars = toDecode.toCharArray(); StringBuilder stringBuild = new StringBuilder(); for (int i = 0 ; i < chars.length; i++) { char decoded = decode(chars[i]); stringBuild.append(decoded); } return stringBuild.toString(); }
Od razu widać jedną rzecz - metody decode() oraz encode() dostępne są zarówno dla obiektów typu String oraz pojedynczych liter. W wypadku metody encode dla ciągu znaków sytuacja jest prosta - na wstępie ciąg znaków, który ma zostać zaszyfrowany, jest rozbijany za pomocą metody toCharArray() na tablicę znaków. Potem inicjalizowany jest obiekt klasy StringBuilder - służy on do połączenia liter w jeden ciąg znakowy. Następnie w pętli, która iteruje po wszystkich kolejnych znakach w tablicy wywoływana jest metoda encode() dla znaku. Na koniec każdej iteracji do stringbuildera trafia zaszyfrowany znak. Po wykonaniu pętli, dzięki metodzie toString() stringbuildera, zwracany jest zaszyfrowany ciąg znaków. W wypadku metody encode() dla pojedynczego znaku wykonywany jest algorytm szyfrujący. Na początku znak zamieniany jest w liczbę, reprezentującą go w tabeli. Następnie do tej wartości dodawana jest liczba będąca reprezentacją, o ile należy przesunąć się w tabeli - getShiftBy(). Indeks znaku, po zaszyfrowaniu, w tablicy przechowywany jest w zmiennej charIndexAfterEncoding. Następnie sprawdzane jest czy wartość indeksu charIndexAfterEncoding nie przekracza pojemności tabeli. Jeśli tak jest (popatrzmy na zawartość instrukcji if), to w takim wypadku pobierana jest reszta z dzielenia zmiennej charIndexAfterEncoding przez zmienną reprezentującą maksymalną wielkość tabeli znaków. Dzięki temu osiągamy cykliczność - ostatnie znaki z tabeli zostaną zastąpione pierwszymi w kolejności z tabeli znaków. Po wszystkim za pomocą metody getCharAtPosition() zwracany jest znak-szyfr spod wyznaczonej pozycji.
Podobnie jest w wypadku metod decode(). Ciąg znaków metoda decode() rozbija za pomocą metody toCharArray() na tablicę znaków. Potem inicjalizowany jest obiekt klasy StringBuilder - służy on do połączenia liter w jeden ciąg znakowy. Następnie w pętli, która iteruje po wszystkich kolejnych znakach w tablicy wywoływana jest metoda decode() dla znaku. Na koniec każdej iteracji do stringbuildera trafia odszyfrowany znak. W wypadku metody decode() dla pojedynczego znaku na początku znak jest konwertowany na liczbę reprezentującą jego pozycję w tabeli. następnie odejmowana od tego jest wartość zmiennej przesunięcia pobranej za pomocą metody getShiftBy(). Jeśli wynik odejmowania wyniesie mniej niż zero - to ta ujemna liczba jest dodawana do wielkości tablicy - dzięki temu znowu mamy cykliczność. Po wszystkim za pomocą metody getCharAtPosition() zwracany jest znak spod wyznaczonej pozycji.
Ostatnią metodą obecną w tej klasie jest konstruktor. Służy on do wywołania konstruktora klasy-rodzica (za pomocą metody super()) oraz ustawienia zmiennej shiftBy za pomocą metody setShiftBy() na wartość 13 (stąd inna nazwa szyfru Cezara: ROT-13).
public ROT13Encoder() { super(); setShiftBy(13); }
Wiemy już jak działa samo szyfrowanie, dlatego teraz dodajmy opcję eksportu i importu z szyfrowaniem do naszej aplikacji. Zacznijmy od zmiany klasy
Eksporter.
Dodajmy do importów wpis:
import pl.org.coderdojo.ceasar.ROT13Encoder; import org.apache.commons.io.*;
Zmieńmy metodę eksportuj, aby przyjmowała parametr typu boolean, czyliWartość logiczną true/false - czy szyfrować dane czy nie.
public void eksportuj(String nazwaPliku, boolean szyfruj) {
Dodajmy jeszcze metodę, która będzie szyfrować dane. Niech metoda przyjmuje listę napisów i zwraca również listę, ale już zaszyfrowanych napisów. Zainicjujmy drugą listę, do której dodamy zaszyfrowane wiersze.
private ArrayList<String> szyfruj(ArrayList<String> lista) { ArrayList<String> szyfrowaneDane = new ArrayList<String>(); return szyfrowaneDane; }
Samo szyfrowanie będzie polegało da wywołaniu metody encode, na każdym z wierszy i wstawieniu tego wiersza do nowej listy.
ROT13Encoder szyfrant = new ROT13Encoder(); for (int i = 0; i < lista.size(); i++) { String linia = lista.get(i); String zaszyfrowanaLinia = szyfrant.encode(linia); szyfrowaneDane.add(zaszyfrowanaLinia); } return szyfrowaneDane;
Na początku tworzymy obiekt szyfranta, następnie idziemy wiersz po wierszu i każdy wiersz szyfrujemy, dodając go jednocześnie na zwracaną listę.
Teraz pozostało nam wywołać tę metodę, jeśli dane mają być szyfrowane w metodzie eksportuj.
public void eksportuj(String nazwaPliku, boolean szyfruj) { //pobranie listy wierszy do zapisania ArrayList<String> zawartosc = pobierzDane(); if (szyfruj) { zawartosc = szyfruj(zawartosc); }
Sprawdzamy tutaj flagę szyfruj i jeśli jest aktywna (wartość true), po prostu wwywołujemy metodę szyfruj, która zmieni dane, które zostaną zapisane.
Teraz jeszcze w klasie DziennikUcznia zmieńmy metodę eksportującą - wywołajmy ją z flagą true.
private void sluchajNaImportExport() { ... Eksporter exporter = new Eksporter(uczniowie, przedmioty); exporter.eksportuj(nazwaPlikuExportu, true);
Skompiluj projekt i wyeksportuj dane. Otwórz plik. Czy potrafisz powiedzieć co zostało tam zapisane?
Zaszyfrowany plik. Nie znając szyfru trudno go odczytać Człowiekowi bez komputera zajęłoby to też dużo czasu.
Spróbujmy to zrobić w importerze, aby można było ponownie pobrać wyeksportowane dane. W klasie Importer dodajmy na górze import do naszej klasy szyfrującej.
import pl.org.coderdojo.ceasar.ROT13Encoder;
Następnie w konstruktorze dodajmy informację czy dane są szyfrowane. Zmieńmy też wywołanie metody importuj.
public Importer(String nazwaPlik, boolean szyfrowane) { //laduje dane importuj(nazwaPlik, szyfrowane); }
Zmieńmy teraz metodę importuj. Niech w parametrze przyjmuje boolean z informacją czy dane są szyfrowane.
private void importuj(String nazwaPlik, boolean szyfrowane) {
Dodajmy teraz metodę odszyfruj.
private ArrayList<String> odszyfruj(List<String> linie) { ArrayList<String> odszyfrowane = new ArrayList<String>(); ROT13Encoder szyfrant = new ROT13Encoder(); for (String linia : linie) { odszyfrowane.add(szyfrant.decode(linia)); } return odszyfrowane; }
Metoda przyjmuje listę linii i zwraca odszyfrowane dane. Tworzymy w niej nowy obiekt szyfranta i każdą kolejną linijkę odszyfrowujemy za pomocą metody decode.
szyfrant.decode(linia)
Dodajmy teraz tylko wywołanie metody odszyfrywującej dane w metodzie importuj.
private void importuj(String nazwaPlik, boolean szyfrowane) { //odczyt pliku do linii try { List<String> linie = FileUtils.readLines(new File(nazwaPlik)); if (szyfrowane) { linie = odszyfruj(linie); }
Teraz zostaje nam tylko poprawić kod w klasie DziennikUcznia w metodzie sluchajNaImportExport. Zmieńmy w niej sposób tworzenia importera, dodając flagę true, oznaczającą, że szyfrujemy dane.
if (nazwaPlikuImportu != null) { // importuje Importer importer = new Importer(nazwaPlikuImportu, true); //przypisuje wartosci przedmiotu przedmioty = importer.pobierzPrzedmioty();
To już wszystko. Skompiluj projekt i zaimportuj zaszyfrowany plik. Zobacz czy wszystko działa jak wcześniej.
Wysyłanie maila z raportem
W tym rozdziale wyślemy mail z aplikacji. Będzie nam do tego potrzebne konto mailowe. Założymy je razem na gmail.com. Najpierw jednak przygotujmy naszą aplikację, aby pozwalała na podanie maila. Będziemy wysyłać ten sam raport, który zapisujemy w pliku.
W wypadku języka Java, jego twórcy zadbali także o funkcjonalność wysyłania i odbierania emaili. W tym celu stworzona została biblioteka Java Mail. Została ona wykorzystana w klasie MailSender. Przyjrzyjmy się zatem, jakie pola i metody posiada ta klasa.
Poniżej znajduje się tabela z opisem poszczególnych pól klasy MailSender.
typ zmiennej | nazwa | rola |
---|---|---|
private Properties | mailProperties | Obiekt przechowujący podstawowe parametry służące do łączenia się z serwerem pocztowym. |
private String | subject | Określa temat emaila. |
private String | content | Określa zawartość emaila |
private String | fromMail | Określa adres, z którego zostanie wysłany email. |
private ArrayList<String> | toMails | Lista przechowująca adresy email, pod które ma zostać wysłany mail. |
private File | attachment | Określa plik, który ma zostać wysłany jako załącznik do emaila. |
private Multipart | mainMessageBody | Przechowywane jest w niej ciało (czyli treść i załącznik) maila |
private MimeMessage | mainMessage | Reprezentuje wiadomość email. |
Klasa ta posiada też cztery konstruktory - każdy z nich rozszerza zakres inicjalizowanych zmiennych wewnętrznych:
public MailSender(Properties props) { this.setMailProperties(props); } public MailSender(Properties props, String subject, String content, String fromMail) { this.setMailProperties(props); this.setSubject(subject); this.setContent(content); this.setFromMail(fromMail); } public MailSender(Properties props, String subject, String content, String fromMail, File attachment) { this.setMailProperties(props); this.setSubject(subject); this.setContent(content); this.setFromMail(fromMail); this.setAttachment(attachment); } public MailSender(Properties props, String subject, String content, String fromMail, File attachment, ArrayList<String> toMailList) { this.setMailProperties(props); this.setSubject(subject); this.setContent(content); this.setFromMail(fromMail); this.setAttachment(attachment); this.setToMails(toMailList); }
Pierwszy z nich tylko ustawia obiekt opisujący parametry niezbędne do nawiązania połączenia z serwerem pocztowym. Drugi dodaje do tego tytuł i zawartość wiadomości oraz adres email nadawcy. Trzeci konstruktor ustawia także plik załącznika. Ostatni zaś - potrafi również ustawić listę adresów email odbiorców.
Dodatkowo klasa MailSender potrafi również stworzyć (a w zasadzie dodać do już istniejącego obiektu - maszyna wirtualna Javy, która wykonuje programy posiada już utworzony taki obiekt) obiekt klasy Properties. Trzymane w nim będą ustawienia niezbędne do ustanowienia połączenia naszego programu z serwerem do wysyłania poczty. Dostępne są dwie metody getPropertiesForMail() - metoda z dwoma parametrami wejściowymi służy do konfiguracji ustawień pod serwery Google gMail. Druga metoda, z większą ilością parametrów, przeznaczona jest dla innych serwerów poczty wychodzącej.
public static Properties getPropertiesForMail (String login, String pass) { Properties props = getPropertiesForMail(login, pass, "smtp.gmail.com", "465", "true"); props.put("mail.smtp.auth", true); props.put("mail.smtp.password", pass); props.put("mail.smtp.starttls.enable", "true"); return props; } public static Properties getPropertiesForMail (String login, String pass, String host, String port, String enableSSL) { Properties props = System.getProperties(); props.put("mail.smtp.host", host); props.put("mail.smtp.user", login); props.put("mail.smtp.port", port); props.put("mail.smtp.ssl.enable", enableSSL); props.put("mail.password", pass); return props; }
Jak widać, wersja metody getPropertiesForMail() z większą ilością parametrów najpierw za pomocą metody System.getProperties() pobiera obiekt typu Properties. Następnie za pomocą metody put() dodawane są wpisy do obiektu typu Properties. Są to kolejno:
- mail.smtp.host - adres serweru email, który będzie odpowiedzialny za wysyłanie naszego emaila,
- mail.smtp.user - nazwa użytkownika, za pomocą której serwer email sprawdzi, czy możemy wysłać z niego email,
- mail.smtp.port - tak zwany port, po którym zostanie przesłany nasz mail do serwera,
- mail.smtp.ssl.enable - informuje o tym, czy chcemy włączyć szyfrowanie naszego połączenia z serwerem email,
- mail.password - hasło użytkownika, za pomocą którego serwer email sprawdzi, czy możemy wysłać z niego email,
Natomiast metoda getPropertiesForMail() z dwoma parametrami czyli loginem i hasłem użytkownika jest wykorzystywana tylko w wypadku wysyłania emaili przez serwery Google gMail. Jak widać, na początku wywołuje ona wersję tej metody z większą ilością parametrów, przy czym kilka wartości jest zapisanych na stałe - są to adres serweru email, port oraz włączenie szyfrowania połączenia. Dodatkowo, ze względu na wymagania Google’a, należy ustawić też kilka innych parametrów w obiekcie typu Properties:
- mail.smtp.auth - zezwolenie na tak zwaną autoryzację użytkownika (oznacza to, że serwer email sprawdzi, czy użytkownik ma prawo do wysłania maila za pomocą naszej aplikacji),
- mail.smtp.password - hasło użytkownika, za pomocą której serwer email sprawdzi, czy możemy wysłać z niego email,
- mail.smtp.starttls.enable - dodatkowe szyfrowanie przed wysłaniem danych do logowania.
Oprócz metod związanych z ustawianiem bądź pobieraniem wartości zmiennych, klasa MailSender zawiera też metodę sendMail(). Służy ona do stworzenia i wysłania wiadomości email na podstawie danych, które zostały do niej wcześniej podane.
public void sendMail() { try { Session sendSession = Session.getInstance(this.getMailProperties(),null); mainMessage = new MimeMessage(sendSession); mainMessage.setFrom(getFromMail()); mainMessage.setSubject(getSubject()); for (String mail : getToMails()) { mainMessage.addRecipient(Message.RecipientType.TO, new InternetAddress(mail)); } MimeBodyPart contentPart = new MimeBodyPart(); contentPart.setText(this.getContent()); mainMessageBody = new MimeMultipart(); mainMessageBody.addBodyPart(contentPart); if (this.getAttachment() != null) { MimeBodyPart attachmentPart = new MimeBodyPart(); attachmentPart.attachFile(getAttachment()); mainMessageBody.addBodyPart(attachmentPart); } mainMessage.setContent(mainMessageBody); mainMessage.setSentDate(Calendar.getInstance().getTime()); Transport.send(mainMessage, this.getMailProperties().getProperty("mail.smtp.user"), this.getMailProperties().getProperty("mail.password")); } catch (Exception ex) { ex.printStackTrace(); } }
Na samym początku metoda sendMail() tworzy obiekt klasy Session - służył on będzie do nawiązania połączenia z serwerem pocztowym Google - zwrócić należy uwagę, iż metoda Session.getInstance() przyjmuje obiekt klasy Properties. Następnie tworzony jest obiekt naszego emaila (można to potraktować jako “kopertę”, do której włożymy nasz list, zaadresujemy go i na końcu wyślemy) za pomocą konstruktora new MimeMessage(), który przyjmuje wcześniej utworzony obiekt Session. Dalej ustawiane są: adres email nadawczy, temat emaila oraz - za pomocą pętli for, która iteruje po liście adresów email - adresy osób, do których mail ma zostać wysłany. Dalej tworzymy obiekt będący zawartością naszego emaila (czyli treść). Dzieje się to za sprawą wywołania konstruktora klasy MimeBodyPart. Jak sama nazwa klasy wskazuje - jest to część emaila. Może zdarzyć się tak, że będzie trzeba razem z treścią wysłać do kogoś jakiś plik, który przecież też jest częścią tego maila. Dlatego, aby połączyć te części, wykorzystana została klasa MimeMultipart. Służy ona do scalania tekstu wiadomości oraz jednego (bądź też większej ilości załączników) w jeden email (czyli jest jakby “zszywaczem”, za pomocą którego zszywamy wiele stron listu). Za pomocą metody addBodyPart() dodajemy do obiektu MimeMultipart naszą treść emaila. Następnie, za pomocą pętli if sprawdzamy, czy mamy też jakiś załącznik do wysłania (czyli czy metoda getAttachement() nie zwróci nam wartości null) - jeśli mamy plik, to postępujemy podobnie jak w wypadku tekstu emaila. Kiedy mamy już wszystko “zszyte”, wkładamy to do naszej “koperty” (czyli obiektu MimeMessage) i za poomocą metody send() z klasy Transport wysyłamy go. I gotowe!
Wiedząc już, jakie są podstawowe funkcjonalności klasy do wysyłania maili, zmodyfikujmy klasę Eksporter, gdzie dodamy metodę, która wygeneruje raport z ocenami.
public String generujRaport() { ArrayList<String> zawartosc = pobierzDane(); StringBuilder sb = new StringBuilder(); for (String linia : zawartosc) { sb.append(linia).append("\r\n"); } return sb.toString(); }
Metoda ta najpierw pobiera listę z liniami, czyli dane do zapisania.
Następnie tworzy obiekt StringBuilder, który pozwala nam łączyć napisy.
Następnie pętla for idzie linijka po linijce po liście wierszy.
Linijka sb.append(linia).append("\r\n") dodaje do napisu, który budujemy najpierw linię, a później sekwencję "\r\n", która oznacza znak nowej linii.
W ostatnim kroku metoda zwraca zbudowany napis, przez wywołanie metody toString().
Teraz przejdźmy do klasy DziennikUcznia. Na górze dodajmy kolejny import, który jest nam potrzebny.
import pl.org.coderdojo.mail.Mailer;
Dodajmy też nowe okno z pytaniem o adres mail. Nowe okno będzie typu EmailWindow.
private EmailWindow adresEmailOkno = new EmailWindow("Podaj adres email do wyslania raportu", 50);
W metodzie inicujMenu dodajmy kolejny element do menu Operacje, o nazwie Wyslij raport.
menuGlowne.addItem("Operacje/Eksportuj"); menuGlowne.addItem("Operacje/Importuj"); menuGlowne.addItem("Operacje/Wyslij raport");
Teraz dodajmy kod, który otworzy okno po kliknięciu na raport. W metodzie sluchajNaMenu dodaj wywołanie okna, tak jak poniżej
} else if (wybraneMenu.equals("Importuj")) { ... } else if (wybraneMenu.equals("Wyslij raport")) { adresEmailOkno.toggleShow("twoj.email@gmail.com"); }
Zamiast twoj.email@gmail.com możesz wprowadzić swój mail, na którym będziesz testować działanie aplikacji. Na ten email, będzie wysyłana wiadomość.
Dodajmy teraz kod, który po wprowadzeniu adres mail, wyśle raport.
Dodaj nową metodę sluchajNaRaport.
private void sluchajNaRaport() { String adresEmail = adresEmailOkno.getResult(); //jesli wprowadzono mail if (adresEmail != null) { //pobiera raport Eksporter exporter = new Eksporter(uczniowie, przedmioty); String raport = exporter.generujRaport(); Mailer mailer = new Mailer("test@gmail.com", "haslo"); mailer.wyslijEmail(adresEmail, "Raport ocen", raport); } }
W pierwszych dwóch linijkach pobieramy adres email i sprawdzamy czy został wprowadzony.
W kolejnych dwóch linijkach tworzymy obiekt eksportera i wywołujemy na nim generowanie raportu. Otrzymujemy raport w postaci napisu.
Następny krok to użycie klasy Mailer. Wywołując ją, podajemy adres email i hasło skrzynki mailowej z której wyślemy wiadomość. Uwaga, powyższy kod działa tylko z kontem gmail.
Za chwilę zmodyfikujemy login i hasło, musimy tylko założyć testowe konto.
Następny krok to użycie wcześniej skonfigurowanego mailer-a, do wysłania raportu na wskazany adres email.
Nie zapomnij wywołać metody w metodzie act.
public void act() { ... sluchajNaRaport(); }
Zakładanie konta gmail.
Wejdź na adres gmail.com
Wybierz Create account / Utwórz konto
Wprowadź dane, nie muszą być prawdziwe. Zapamiętaj użytkownika oraz hasło. Wybierz następny krok.
W następnym kroku kliknij Zgadzam się
Konto zostanie założone
Kliknij Przejdź do Gmail Teraz musimy jeszcze pozwolić, aby nasza plikacja mogła uzyć serwera smtp. Przejdź pod adres [https://www.google.com/settings/security/lesssecureapps] i kliknij Włącz, jeśli jest wyłączone.
Teraz masz skonfigurowane konto google z którego możesz wysłać email. Przejdź do metody sluchajNaRaporti zmień ustawienia Mailer-a na własne.
Mailer mailer = new Mailer("test@gmail.com", "haslo");
Skompiluj aplikację. Wyślij raport (pamiętaj o klikaniu w oknie na Enter, wciśnięcie klawisza może powodować błędy).
Powinieneś w swoje skrzynce mailowe zobaczyć wiadomość z raportem.
W skrzynce wychodzącej konta testowego, także pojawi się ta wiadomość.