Celem laboratorium jest zapoznanie się z mechanizmem wątków oraz obsługą zasobów w kontekście programowania graficznego interfejsu użytkownika (GUI).
Treść laboratorium powstała we współpracy z Norbertem Morawskim.
Najważniejsze zadania:
- Dodanie obsługi tekstur.
- Dodanie wątku symulacyjnego.
- Guzik uruchamiający symulację.
- Mechanizm wątków służy do realizacji zadań, które powinny być realizowane współbieżnie, a jeśli system posiada wiele procesorów, to również równolegle. Wykonujące wątki nawzajem nie blokują swego wykonania. Dzięki temu możliwe jest np. reagowanie na zdarzenia GUI (np. kliknięcie na guzik) oraz wykonywanie obliczeń, które sterują tym co wyświetla się w GUI.
- Przykładami takich operacji może być np. dostęp do zasobu sieciowego albo w naszym przypadku sztucznie generowane opóźnienie pomiędzy ruchami.
- Wątek UI jest to główny wątek aplikacji w graficznym interfejsem użytkownika. Tylko ten wątek może modyfikować zawartość sceny w JavaFX.
- Aby stworzyć wątek możemy skorzystać z klasy
Thread
i interfejsuRunnable
.class SimulationEngine implements Runnable { @Override public void run() { System.out.println("Thread started."); } } SimulationEngine engine = new SimulationEngine(); Thread engineThread = new Thread(engine); engineThread.start();
- Zasoby w projekcie, takie jak obrazy graficzne zwykle umieszczane są w katalogu
src/main/resources
. - Odczytanie zasobu możliwe jest np. za pomocą strumienia
java.io.FileInputStream
. - Dane binarne zawierające obraz można wczytać do obiektu
javafx.scene.image.Image
. - Wyświetlenie obrazka odbywa się za pomocą obiektu
javafx.scene.image.ImageView
. - Zasoby takie jak
ImageView
są pamięciochłonne, dlatego ważne jest aby nie były one tworzone bez potrzeby. W modelu pamięciowym JavaFX elementy, które nie należą do drzewa węzłów są usuwane przez śmieciarza (GC), ale ich tworzenie samo w sobie jest czasochłonne, co może istotnie spowalniać działanie aplikacji. - Przykładowy kod służący do utworzenia obrazka, który można dodać do sceny JavaFX:
Uwaga: w profesjonalnych aplikacjach zasoby znajdujące się w
Image image = new Image(new FileInputStream("src/main/resources/up.png")); ImageView imageView = new ImageView(image); imageView.setFitWidth(100); imageView.setFitHeight(100);
src/main/resources
odczytujemy nieco inaczej, korzystając z tzw. class loadera:getClass().getResourceAsStream("up.png")
. Nie musimy wówczas podawać ścieżki do obrazka, co uniezależnia nas od tego, czy jest on uruchamiany za pośrednictwem IntelliJ czy jako zbudowana, produkcyjna aplikacja. Żeby to zadziałało, zasób musi być umieszczony w takim samym pakiecie jak klasa, z której jest wczytywany. Przykładowo: jeśli ładujemy obrazek z klasyagh.ics.oop.gui.App
to zasób powinien się on znajdować wsrc/main/resources/agh/ics/oop/gui
.
- Stwórz albo wykorzystaj gotowe 4 tekstury z informacją o orientacji dla zwierzaka (folder
resources
) - Stwórz albo wykorzystaj teksturę dla trawy.
- Dodaj utworzone tekstury do folderu
src/main/resources
- Utwórz klasę
GuiElementBox
, która pozwoli na dodanie obrazka do siatki:- utwórz instancję klasy
Image
, - zainicjuj za jej pomocą obiekt
ImageView
, - ustal jego rozmiary na 20 x 20,
- utwórz etykietę informującą o pozycji zwierzaka,
- uwtórz obiekt vertical box (
VBox
) do którego dodasz oba obiekty (obrazek i etykietę), - wyśrodkuj elementy wewnątrz kontenera.
- utwórz instancję klasy
- Dodaj do interfejsu
IMapElement
metody pozwalające na pobranie nazwy zasobu odzwierciedlającego wygląd danego elementu (czyli np.src/main/resources/up.png
, jeśli zwierzę zwrócone jest na północ). Zaimplementuj je w klasach implementujących ten interfejs. - Wykorzystaj powyższe metody w konstruktorze klasy
GuiElementBox
, który powinien przyjmować instancjęIMapElement
i wyświetlać reprezentację elementu. Upewnij się, że elementy te nie są niepotrzebnie tworzone wielokrotnie. - Zamień reprezentację tekstową na graficzną w klasie
App
. - Docelowy wygląd:
- Skorzystaj ze wzroca Observer, aby informować o zmianach położenia zwierzą moduł GUI. Zastanów się, czy lepiej połączyć za jego pomocą zwierzęta i GUI, czy może silnik symulacyjny.
- W klasie
App
obsłuż aktualizację stanu mapy. Wyczyść siatkę wywołującgrid.getChildren().clear()
i wyrenderuj ją od nowa wyświetlając aktualne pozycje roślin i zwierząt. Wykonanie na wątku UI można osiągnąć przy użyciu wywołaniaPlatform.runLater(() -> { ... })
.- Dla zaawansowanych: Spróbuj zopytmalizować aktualizacje siatki aby nie była ona tworzona od nowa za każdym razem.
- Dodaj pole
moveDelay
które będzie służyć do opóźniania sekwencji ruchów zwierząt (aby widzieć zmiany na żywo).- Zastanów się kiedy ustawić wartość tego pola.
- Opóźnienie pomiędzy ruchami dodaj za pomocą
Thread.sleep(300)
(usypia wątek na 300 ms). Umieśćsleep()
w blokutry-catch
i wypisz stosowny komunikat w razie przerwania symulacji. - Zaimplementuj interfejs
Runnable
przezSimulationEngine
. - W metodzie
init()
GUI stwórz nowy wątek używającSimulationEngine
jako parametru. Uruchom wątek metodąstart()
. Pamiętaj o ustawieniumoveDelay
na np. 300 [ms].
- Dodaj do interfejsu pole tekstowe i przycisk start. Skorzystaj z klas
HBox
,VBox
,Button
iTextField
. - Utwórz setter dla pola
directions
wSimulationEngine
tak, aby dało się je dynamicznie zmieniać przy naciśnięciu przycisku. Utwórz konstruktor który nie ustawia tego pola. - Usuń
engineThread.start()
z metodyinit()
. - Dodaj obsługę kliknięcia Start (użyj
setOnAction
). Odczytaj wartość pola tekstowego (getText()
) i użyj jego zawartości w parserze. Ustaw nową sekwencję ruchów i uruchom za każdym razem nową instancjęThread
.