Controller…
choć wdrożyliśmy się już w technologię JavaFX to nie użyliśmy jeszcze najważniejszej jego części – Kontrolerów. Póki co wszystko staraliśmy się zrobić poprzez Maina, jednak jak łatwo można było zauważyć poziom skomplikowania i nieczystości kodu rósł proporcjonalnie do każdego nowo dodanego przycisku lub etykiety.
Czas z tym skończyć – nadchodzi era Controllera! 😀
Punkt startowy
W poprzedniej lekcji skończyliśmy na utworzeniu szablonu kalkulatora przy użyciu narzędzia SceneBuilder, udało się nam nawet uruchomić owy interfejs graficzny. Oczywiście nie jest on w ogóle użyteczny, dlatego czas to zmienić. W tej lekcji stworzymy Controller, dzięki któremu “oprogramujemy” nasz interfejs kalkulatora – do dzieła!
W tej lekcji posłużę się interfejsem bardzo podobnym do tego z poprzedniej lekcji – różnica jest jedna, w poprzedniej lekcji zapomniałem o dodaniu przycisku “równa się” – dlatego w nowej wersji zamieniłem kropkę na właśnie ten przycisk.
W celu pracy na tym samym interfejsem co ja użyj mojego projektu:
git clone https://github.com/1024kb-pl/KJOP_JavaFX_Templates
Co do pliku .fxml – nazwę kontrolera możesz zmienić w tej linii:
<SplitPane dividerPositions="0.22110552763819097" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" orientation="VERTICAL" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
fx:controller określa nazwę kontrolera – dodatkowo podaje się pakiet, w którym się znajduje.
SplitPane jest akurat w moim interfejsie, u Ciebie może to być inny komponent – chodzi tylko o klucz fx:controller.
<Button fx:id="button_plus" mnemonicParsing="false" onAction="#clickButtonAdd" prefHeight="47.0" prefWidth="139.0" text="+" GridPane.halignment="CENTER" GridPane.valignment="CENTER" />
Metody z kontrollera do akcji przypisujemy w taki sposób, że podajemy z hashem nazwę metody z kontrolera przy kluczu onAction.
Przypominam, że te wszystkie rzeczy wcześniej zostały skonfigurowane w SceneBuilderze.
Kalkulator
Zanim przejdziemy do implementacji kontrolera naszego interfejsu zbudujmy prostą klasę odpowiedzialną za działanie kalkulatora.
Będzie do niej mogli wrzucić dwie liczby – zapisywane będą one w polach, po wywołaniu odpowiedniej metody na liczbach będzie wykonywana odpowiednia operacja np. dodawania.
package sample; public class Calculator { private final static Calculator instance = new Calculator(); private int firstNumber = 0; private int secondNumber = 0; private Calculator() { } public static Calculator getInstance() { return instance; } public void setFirstNumber(int firstNumber) { this.firstNumber=firstNumber; } public void setSecondNumber(int secondNumber) { this.secondNumber = secondNumber; } public int add() { return firstNumber + secondNumber; } public int subtract() { return firstNumber - secondNumber; } public int multiply() { return firstNumber * secondNumber; } public int divide() { return firstNumber / secondNumber; } }
Ze względu, że zamieniłem kropkę na równa się to będziemy pracować na liczba całkowitych – nawet wynik z dzielenia. Nie chodzi mi tu o stworzenie idealnego kalkulatora tylko o pokazanie Ci pewnej zależności podczas używania kontrolerów.
Operations
Aby łatwiej było nam operować kalkulatorem stworzyłem sobie również typ wyliczeniowy trzymający możliwe operacje matematyczne wbudowane w nasz kalkulator.
package sample; public enum Operations { ADD, SUBTRACT, DIVIDE, MULTIPLY; }
Controller
Czas przejść do implementancji kontrolera – czyli głównej gwiazdy tej imprezy. 😉
Na poczatku będziemy potrzebować kalkulatora oraz typu operacji, która została wywołana:
public class Controller { Calculator calculator = Calculator.getInstance(); Operations operation; }
Następne trzy pola będą miały za zadanie: przechowywać wpisane liczby oraz trzymać kolejność wpisywanych liczb – czyli, do której liczby będziemy dopisywać wartości.
public class Controller { Calculator calculator = Calculator.getInstance(); Operations operation; boolean isTypingFirstNumber = true; String promptFirstNumber = ""; String promptSecondNumber = ""; }
Kluczowym momentem kontrolera jest pole Label – czyli pole, do którego będziemy zapisywać wartość liczb i wynik.
@FXML Label label_result;
O co chodzi z tym @FXML? – możliwe, że zapytasz. Już tłumaczę.
Ze względu, że jestesmy “podłączeni” naszym kontrolerem do szablonu (czyli pliku) .fxml to możemy się dostawać do jego komponentów – w tym przypadku Label, lecz możemy również się dostawać np. do Buttonów.
Małpa oznacza adnotację, jest to specjalny interfejs, który jest wykorzystywany w refleksjach. Ze względu, że jest to bardziej skomplikowany temat to wystarczy Ci wiedzieć, że dobrze utworzona adnotacja potrafi zrobić wiele za nas.
Co adnotacja FXML robi za nas? “Wstrzykuje” ona za nas label o nazwie label_result czyli ten:
<Label fx:id="label_result" layoutX="8.0" layoutY="7.0" prefHeight="72.0" prefWidth="581.0" style="-fx-background-color: #efefef;" textAlignment="RIGHT">
Spójrz, że nazwa pola zgadza się z fx:id!
Podsumowując – dzięki adnotacji @FXML mamy dostęp do komponentu z naszego interfejsu bez zbędnego jego wyszukiwania lub tworzenia, wystarczy, że obie nazwy się zgadzają i możemy już na nim działać.
Skoro mamy już wszystkie pola – czyli w takiej formie:
public class Controller { Calculator calculator = Calculator.getInstance(); Operations operation; boolean isTypingFirstNumber = true; String promptFirstNumber = ""; String promptSecondNumber = ""; @FXML Label label_result; }
Zajmijmy się teraz wspólną częścią dla wielu metod.
Wyświetlanie odpowiedniej liczby
Ze względu stanu isTypingFirstNumber musimy wyświetlać odpowiednią liczbę w odpowiednim czasie w result_label.
private void displayRightNumberInResultLabel() { label_result.setText(isTypingFirstNumber ? promptFirstNumber : promptSecondNumber); }
Dzięki tej metodzie w result_label znajdzie się zawsze ta liczba, której jest “kolej”.
? i : są częścią operatora warunkowego trójargumentowego, który ma postać:
warunek ? wykonaj_gdy_prawda : wykonaj_gdy_falsz;
Jest to po prostu skrócona wersja zwykłego if – else. Nasza metoda równie dobrze może wyglądać tak:
private void displayRightNumberInResultLabel() { String number; if (isTypingFirstNumber) { number = promptFirstNumber; } else { number = promptSecondNumber; } label_result.setText(number); }
Nie uważasz, że ten zapis jest brzydszy od tego? 😉
private void displayRightNumberInResultLabel() { label_result.setText(isTypingFirstNumber ? promptFirstNumber : promptSecondNumber); }
Wpisywanie do odpowiedniej liczby
Klikając po przyciskach będziemy musieli dopisywać cyfry do odpowiedniej liczby – w tym celu stworzymy metodę, która dopisze podaną cyfrę do odpowiedniego Stringa.
private void promptNumberToRightNumber(int number) { if (isTypingFirstNumber) { promptFirstNumber = promptFirstNumber.concat(String.valueOf(number)); } else { promptSecondNumber = promptSecondNumber.concat(String.valueOf(number)); } displayRightNumberInResultLabel(); }
Na koniec oczywiście trzeba pamiętać o ponownym wyświetleniu liczby! 😉
Dodatkowe metody
Przydadzą się również nam dodatkowe metody takie jak:
Wyświetlanie pierwszej liczby w result_label:
private void displayFirstNumber() { label_result.setText(promptFirstNumber); }
Sprawdzenie czy druga liczba została wprowadzona:
private boolean isSecondNumberExist() { return promptSecondNumber.length() > 0; }
Czy operacja została wybrana:
private boolean isOperationDefined() { return operation != null; }
Zapisywanie wybranych cyfr
Na początku obsłużmy wszystkie przyciski z cyframi – wykorzystamy do tego wcześniej zdefiniowaną metodę promptNumberToRightNumber, która wpiszę do odpowiedniego pola naszą cyfrę.
public void clickButtonZero() { promptNumberToRightNumber(0); } public void clickButtonOne() { promptNumberToRightNumber(1); } public void clickButtonTwo() { promptNumberToRightNumber(2); } public void clickButtonThree() { promptNumberToRightNumber(3); } public void clickButtonFour() { promptNumberToRightNumber(4); } public void clickButtonFive() { promptNumberToRightNumber(5); } public void clickButtonSix() { promptNumberToRightNumber(6); } public void clickButtonSeven() { promptNumberToRightNumber(7); } public void clickButtonEight() { promptNumberToRightNumber(8); } public void clickButtonNine() { promptNumberToRightNumber(9); }
Wywołanie obliczeń
Potrzebujemy również metody, która wywoła odpowiednią metodę calculatora na podstawie wybranej operacji – posłuży nam do tego taka implementacja:
private int callCalculatorAction() { switch(operation) { case ADD: return calculator.add(); case DIVIDE: return calculator.divide(); case SUBTRACT: return calculator.subtract(); case MULTIPLY: return calculator.multiply(); default: return 0; } }
Przełączanie liczb
Po wciśnięciu przycisku operacji trzeba zmienić wajchę w naszym programie – czyli zapisywać do drugiej liczby, czyli zmienić stan isTypingFirstNumber na false. 😉
private void switchToSecondNumber() { isTypingFirstNumber = false; displayFirstNumber(); }
Operacje
Trzeba również obsłużyć przyciski operacji – będą po prostu przypisywać odpowiednią wartość do pola operation:
public void clickButtonAdd() { operation = Operations.ADD; switchToSecondNumber(); } public void clickButtonMinus() { operation = Operations.SUBTRACT; switchToSecondNumber(); } public void clickButtonMultiply() { operation = Operations.MULTIPLY; switchToSecondNumber(); } public void clickButtonDive() { operation = Operations.DIVIDE; switchToSecondNumber(); }
Czyszczenie kalkulatora
Potrzebujemy jeszcze wyczyszczenia całej “pamięci” kalkulatora – czyli obu liczb, przywrócić stan isTypingNumber na true oraz ustawić operację na null.
public void clickButtonClear() { label_result.setText(""); promptFirstNumber=""; promptSecondNumber=""; isTypingFirstNumber=true; operation = null; }
Obliczanie
Na sam koniec musimy oczywiście w jakiś sposób obliczyć wynik, czym byłby kalkulator bez możliwości obliczania. 😉
public void clickButtonResult() { if (isSecondNumberExist() && isOperationDefined()) { } }
Na początek musimy sprawdzić czy druga liczba została wprowadzona i czy operacja została wybrana.
Następnie ustawiamy pola w Calculatorze na podstawie wczytanych Stringów i obliczamy wynik:
public void clickButtonResult() { if (isSecondNumberExist() && isOperationDefined()) { calculator.setFirstNumber(Integer.valueOf(promptFirstNumber)); calculator.setSecondNumber(Integer.valueOf(promptSecondNumber)); int result = callCalculatorAction(); } }
Integer.valueOf – zamienia inny typ danych na integer, w naszym przypadku zamienia ze Stringa.
Na koniec wpisujemy wynik do pierwszej liczby (aby można było nadal wykonywać operację na swoim wyniku), czyścimy drugą liczbę i wypisujemy wynik do result_label.
public void clickButtonResult() { if (isSecondNumberExist() && isOperationDefined()) { calculator.setFirstNumber(Integer.valueOf(promptFirstNumber)); calculator.setSecondNumber(Integer.valueOf(promptSecondNumber)); int result = callCalculatorAction(); isTypingFirstNumber = false; promptFirstNumber = String.valueOf(result); promptSecondNumber=""; displayFirstNumber(); } }
I w końcu po tylu trudach tworzenia kalkulatora możemy go wypróbować poprzez interfejs graficzny. Mam nadzieję, że u Ciebie też działa. 😛
Podsumowanie
W tej lekcji pokazałem Ci jak wykorzystać kontroler w interfejsie graficznym JavaFx – jak widać zaoszczędził nam on wiele pracy oraz zbędnego kodu, ja kto było w pierwszej lekcji JavaFx.
Dzięki adnotacji @FXML pozbyliśmy się wyszukiwania komponentu interfejsu, został on automatycznie “wstrzyknięty” jako obiekt naszej klasy – później wystarczyło ustawiać tylko na nim tekst. 😉
Co prawda tworzenie oddzielnej klasy dla kalkulatora może jest trochę sztuką dla sztuki – ale za to jakiej… Zauważ, że dzięki temu oddzieliliśmy logikę biznesową od logiki aplikacji – dzięki czemu mamy różne warstwy aplikacji.
Czemu jest to takie ważne? Wyobraź sobie, że klient chce również mieć kalkulator w konsoli – co robisz? Podpinasz klasę Calculator do interfejsu konsoli i tam obsługujesz np. klawiaturę zamiast pisać od nowa implementację kalkulatora.
Logika biznesowa to nic innego jak ostatnia warstwa, z która ma kontakt bezpośredni użytkownik, klient – zazwyczaj jest to interfejs aplikacji, ale też mogą nią być Restowe End Pointy.
Za logikę aplikacji uważamy wszystko to co się dzieje pod spodem – czyli cała walidacja danych, operacje na danych, zapisywanie, odczyt itd.
Kod calej aplikacji możesz znaleźć tutaj.
Jeśli chcesz poćwiczyć kontrolery w JavaFx to zaimplementuj kontroler dla interfejsu z poprzedniego zadania domowego – czyli interfejsu kalkulatora jednostek miar i wag.