JavaFX, czyli pierwszy widok dla lifetimera
Postanowiłem stworzyć prosty minutnik w celu zaprezentowania proces tworzenia GUI w technologii JavaFX.
Od czego zacząć?
Zaczynając pracę z JavaFX 8 wystarczy mieć JDK 1.8. Biblioteki są zawarte w narzędziach deweloperskich Javy.
Klasa uruchomieniowa
Klasa główna powinna rozszerzać klasę javafx.application.Application, która pozwala w łatwy sposób uruchomić samodzielną aplikację. W funkcji main wywołujemy metodę launch(String... args) z argumentami aplikacji. Klasa Application jest klasą abstrakcyjną z jedną metodą do zaimplementowania. W funkcji abstrakcyjnej start(Stage stage) piszemy początkowy kod naszej aplikacji.
U mnie klasa wygląda następująco:
package pl.devpragmatic.lifetimerjavafxgui; | |
import java.io.IOException; | |
import javafx.application.Application; | |
import javafx.scene.image.Image; | |
import javafx.stage.Stage; | |
import pl.devpragmatic.lifetimerjavafxgui.scene.WatchstopConfigScene; | |
/** | |
* | |
* @author devpragmatic | |
*/ | |
public class GuiApplication extends Application{ | |
@Override | |
public void start(Stage stage) throws IOException { | |
stage.getIcons().add(new Image(getClass().getResource("/images/icon.jpg").toString())); | |
WatchstopConfigScene configScene = new WatchstopConfigScene(); | |
configScene.start(stage); | |
} | |
public static void main(String[] args) { | |
launch(args); | |
} | |
} |
Ustawiam ikonę do okna aplikacji. Następnie wywołuję klasę, która reprezentuje mi scene konfiguracji minutnika.
Teraz przedstawię wygląd pliku fxml:
<?xml version="1.0" encoding="UTF-8"?> | |
<?import java.lang.*?> | |
<?import java.net.*?> | |
<?import java.util.*?> | |
<?import javafx.scene.*?> | |
<?import javafx.scene.control.*?> | |
<?import javafx.scene.layout.*?> | |
<?import pl.devpragmatic.lifetimerjavafxgui.control.*?> | |
<StackPane id="root" prefHeight="200.0" styleClass="mainFxmlClass" xmlns:fx="http://javafx.com/fxml/1" fx:controller="pl.devpragmatic.lifetimerjavafxgui.controller.WatchstopConfigController"> | |
<HBox alignment="center"> | |
<stylesheets> | |
<URL value="@/styles/Styles.css"/> | |
</stylesheets> | |
<children> | |
<Label minWidth="15" >D:</Label> | |
<NumberTextField fx:id="daysField" maxLength="10"></NumberTextField> | |
<Label minWidth="15">H:</Label> | |
<NumberTextField fx:id="hoursField" maxLength="2" maxValue="23" ></NumberTextField> | |
<Label minWidth="15">M:</Label> | |
<NumberTextField fx:id="minutesField" maxLength="2" maxValue="59" ></NumberTextField> | |
<Label minWidth="15">S:</Label> | |
<NumberTextField fx:id="secondsField" maxLength="2" maxValue="59" ></NumberTextField> | |
<Button minWidth="50" onAction="#start">Start</Button> | |
</children> | |
</HBox> | |
</StackPane> |
Przedstawię co dokładnie zaimplementowałem.
Kod przedstawia importy klas. W pliku fxml potrzebujemy zaimportować także klasy z paczki java.lang.*, w zwykłych klasach Javy ten import nie występuje.
Tutaj tworzę główny kontener, który nazywam root. Przypisuje mu klasę stylu (czyli klasę z CSS) mainFxmlClass. Sugeruje jaką powinnień mieć szerokość początkową. Ustawiam przestrzeń nazw dla fx z adresu "http://javafx.com/fxml/1" oraz klasę kontrolera "pl.devpragmatic.lifetimerjavafxgui.controller.WatchstopConfigController". StackPane jest kontenerem, który umieszcza elementy na sobie. Dokładniejsze zastosowanie można zauważyć w następnym pliku fxml.
Tutaj podajemy link do pliku stylizującego css.
W znaczniku children podajemy węzły podrzędne rodzica. W naszym przypadku będą w rodzicu HBox umieszczone 4 kontrolki Label, 4 NumberTextField oraz jeden przycisk. HBox to kontener, który porządkuje węzły podrzędne poziomo (czyt. Horizontal Box).
Mam nadzieję, że rozjaśniłem troszkę strukturę pliku fxml. Po drodzę wystąpiły dwie obce klasy. Jedna to kontroler, który przedstawię i opiszę poniżej:
package pl.devpragmatic.lifetimerjavafxgui.controller; | |
import java.io.IOException; | |
import java.net.URL; | |
import java.util.ResourceBundle; | |
import javafx.event.ActionEvent; | |
import javafx.fxml.FXML; | |
import javafx.fxml.Initializable; | |
import javafx.scene.Node; | |
import javafx.stage.Stage; | |
import pl.devpragmatic.lifetimer.domain.Time; | |
import pl.devpragmatic.lifetimerjavafxgui.control.NumberTextField; | |
import pl.devpragmatic.lifetimerjavafxgui.scene.WatchstopScene; | |
/** | |
* FXML Controller class | |
* | |
* @author devpragmatic | |
*/ | |
public class WatchstopConfigController implements Initializable { | |
@FXML | |
private NumberTextField daysField; | |
@FXML | |
private NumberTextField hoursField; | |
@FXML | |
private NumberTextField minutesField; | |
@FXML | |
private NumberTextField secondsField; | |
/** | |
* Initializes the controller class. | |
* @param location | |
* The location used to resolve relative paths for the root object, or | |
* <tt>null</tt> if the location is not known. | |
* | |
* @param resources | |
* The resources used to localize the root object, or <tt>null</tt> if | |
* the root object was not localized. | |
*/ | |
@Override | |
public void initialize(URL location, ResourceBundle resources) { | |
} | |
@FXML | |
public void start(ActionEvent event) throws IOException | |
{ | |
Time time = new Time(); | |
time.setTime(daysField.getInteger(), hoursField.getInteger(), minutesField.getInteger(), secondsField.getInteger()); | |
WatchstopScene watchstop = new WatchstopScene(time); | |
final Node source = (Node) event.getSource(); | |
final Stage stage = (Stage) source.getScene().getWindow(); | |
watchstop.start(stage); | |
} | |
} |
Implementuje ona interfejs inicjalizujący kontroler. U mnie nie występuje tam żaden kod. Elementy GUI otrzymuje wykorzystując DI (Dependency Injection). Uzyskuje ten efekt za pomocą adnotacji @FXML przy prywatnych polach klasy. Jednocześnie implementuje tutaj metode kontrolera, która jest wywoływana po naciśnięciu przycisku. Oznaczam ją także adnotacją @FXML, aby kompilator pliku fxml wiedział na co ma zwracać uwagę przypisując funkcję guzikowi. Kontroler służy tutaj do zebrania danych ze sceny aplikacji i przesłania jej do drugiej w celu uruchomienia jej z tymi parametrami.
Druga niestandardowa klasa to NumberTextField:
package pl.devpragmatic.lifetimerjavafxgui.control; | |
import javafx.scene.control.TextField; | |
/** | |
* | |
* @author devpragmatic | |
*/ | |
public class NumberTextField extends TextField | |
{ | |
private int maxLength = 19; | |
private long maxValue = Long.MAX_VALUE; | |
public int getMaxLength() { | |
return maxLength; | |
} | |
public void setMaxLength(int maxLength) { | |
this.maxLength = maxLength; | |
} | |
public long getMaxValue() { | |
return maxValue; | |
} | |
public void setMaxValue(long maxValue) { | |
this.maxValue = maxValue; | |
} | |
/** | |
* Veryfing maxlength and number only text. Replace corrected text using super method. | |
* | |
* @param start The starting index in the range, inclusive. This must be >= 0 and < the end. | |
* @param end The ending index in the range, exclusive. This is one-past the last character to | |
* delete (consistent with the String manipulation methods). This must be > the start, | |
* and <= the length of the text. | |
* @param text The text that is to replace the range. This must not be null. | |
*/ | |
@Override | |
public void replaceText(int start, int end, String text) { | |
if (validate(text)){ | |
String changedText = changeText(text, start, end); | |
if (text.equals("") || getText().length() < maxLength || end < getText().length() || (start != end && end == getText().length())) { | |
if(text.equals(changedText)) | |
super.replaceText(start, end, changedText); | |
else{ | |
super.replaceText(0, getText().length(), changedText); | |
} | |
} | |
} | |
} | |
/** | |
* Veryfing maxlength and number only text. Replace corrected text using super method. | |
* @param text selection text to change | |
*/ | |
@Override | |
public void replaceSelection(String text) { | |
if (validate(text)) { | |
if (text.equals("")) { | |
super.replaceSelection(changeText(text, -1, -1)); | |
} else if (getText().length() < maxLength) { | |
if (text.length() > maxLength - getText().length()) { | |
text = text.substring(0, maxLength- getText().length()); | |
} | |
super.replaceSelection(changeText(text, -1, -1)); | |
} | |
} | |
} | |
private boolean validate(String text) { | |
if(text.length() <= maxLength) | |
return text.matches("[0-9]*"); | |
return false; | |
} | |
private String changeText(String text, int start, int end) { | |
if(getText().length() > 0){ | |
String valueString; | |
if(start == -1 || end == -1){ | |
valueString = text; | |
}else if((start == end && getLength() == end) || (start != end && getLength() == end - 1)){ | |
valueString = getText() + text; | |
}else{ | |
valueString = new StringBuilder(getText()).replace(start, end, text).toString(); | |
} | |
if(valueString.length() > 0){ | |
long value = Long.valueOf(valueString); | |
if(value > maxValue){ | |
return String.valueOf(maxValue); | |
}else{ | |
return text; | |
} | |
} | |
} | |
return text; | |
} | |
/** | |
* Return text as Integer | |
* @return text as Integer | |
*/ | |
public Integer getInteger(){ | |
if(getText().length() < 1){ | |
return 0; | |
} | |
return Integer.valueOf(getText()); | |
} | |
} |
Jest to rozszerzenie kontrolki TextField o dodatkową walidację, która pozwala na wpisywanie tylko cyfr o zadeklarowanej długości oraz z ograniczeniem górnym. Przydaje mi się to w celu uzyskania od użytkownika wpisania odpowiednich danych czasowych.
Ukończyłem pierwszą scenę. Wygląda ona jak na obrazku poniżej.
W widoku można zobaczyć jak w praktyce działa HBox. Elementy są umieszczone obok siebie w jednej lini.
Już w trakcie opisywania poprzedniej sceny wyszło, że posiadam kolejną która służy za minutnik.
<?xml version="1.0" encoding="UTF-8"?> | |
<?import java.lang.*?> | |
<?import java.net.*?> | |
<?import java.util.*?> | |
<?import javafx.scene.*?> | |
<?import javafx.scene.control.*?> | |
<?import javafx.scene.layout.*?> | |
<?import javafx.scene.text.*?> | |
<StackPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="pl.devpragmatic.lifetimerjavafxgui.controller.WatchstopController"> | |
<stylesheets> | |
<URL value="@/styles/Styles.css"/> | |
</stylesheets> | |
<children> | |
<ProgressBar fx:id="progressBar" progress="0.0"> </ProgressBar> | |
<Text fx:id="timeLabel"></Text> | |
</children> | |
</StackPane> |
Tutaj sytuacja wygląda bardzo prosto. Jest umieszczony ProgressBar a na nim tekst. Bardziej skomplikowanie wygląda jednak uruchomienie tego mechanizmu.
package pl.devpragmatic.lifetimerjavafxgui.scene; | |
import java.io.IOException; | |
import javafx.fxml.FXMLLoader; | |
import javafx.scene.Parent; | |
import javafx.scene.Scene; | |
import javafx.stage.Stage; | |
import pl.devpragmatic.lifetimer.domain.Time; | |
import pl.devpragmatic.lifetimerjavafxgui.controller.WatchstopController; | |
/** | |
* | |
* @author devpragmatic | |
*/ | |
public class WatchstopScene implements SceneManager{ | |
private Time time = new Time(); | |
public WatchstopScene(Time time) { | |
this.time = time != null ? time : new Time(); | |
} | |
@Override | |
public void start(Stage stage) throws IOException{ | |
stage.hide(); | |
FXMLLoader fXMLLoader = new FXMLLoader(getClass().getResource("/fxml/Watchstop.fxml")); | |
Parent root = fXMLLoader.load(); | |
WatchstopController controller = fXMLLoader.getController(); | |
Scene scene = new Scene(root); | |
stage.setTitle(time.getAsString()); | |
stage.setScene(scene); | |
stage.setMaxWidth(300); | |
stage.setMaxHeight(75); | |
stage.show(); | |
controller.runWatchstop(time, stage); | |
} | |
} |
Tutaj wczytywanie elementów GUI podzielone jest troszkę na etapy. Nie robię tego automatycznie. Powód tego jest taki, że potrzebuje dostać się do kontrolera widoku, który będzie służył, za mechanizm zmieniania danych w widoku. Kontroler ma możliwość operowania na komponentach GUI. W trakcie jego wczytywania elementy są wstrzykiwane do niego, dlatego to on posłuży mi do wcześniej wspomnianego mechanizmu.
Pobieranie kontrolera robię za pomocą FXMLLoadera w linijce:
Następnie uruchamiam licznik wywołując metode:
Przedstawie teraz jak wygląda mechanizm licznika. Napisałem wcześniej, że jest on umieszczony w kontrolerze.
package pl.devpragmatic.lifetimerjavafxgui.controller; | |
import java.net.URL; | |
import java.util.ResourceBundle; | |
import javafx.animation.KeyFrame; | |
import javafx.animation.Timeline; | |
import javafx.event.Event; | |
import javafx.event.EventHandler; | |
import javafx.fxml.FXML; | |
import javafx.fxml.Initializable; | |
import javafx.scene.control.ProgressBar; | |
import javafx.scene.text.Text; | |
import javafx.stage.Stage; | |
import javafx.util.Duration; | |
import pl.devpragmatic.lifetimer.domain.Time; | |
/** | |
* FXML Controller class | |
* | |
* @author devpragmatic | |
*/ | |
public class WatchstopController implements Initializable { | |
private final Time timeSecond; | |
@FXML | |
private ProgressBar progressBar; | |
@FXML | |
private Text timeLabel; | |
public WatchstopController(){ | |
timeSecond = new Time(); | |
timeSecond.setTime(0, 0, 0, 1); | |
} | |
@Override | |
public void initialize(URL location, ResourceBundle resources) { | |
} | |
/** | |
* Running the watchstop | |
* @param time time to count down | |
* @param stage scene stage | |
*/ | |
public void runWatchstop(final Time time, final Stage stage) { | |
final Timeline timeline = new Timeline(); | |
timeline.setCycleCount(Timeline.INDEFINITE); | |
final Time tmp = new Time(); | |
tmp.addTime(time); | |
progressBar.prefWidthProperty().bind(stage.widthProperty().subtract(5)); | |
progressBar.prefHeightProperty().bind(stage.heightProperty().subtract(5)); | |
timeLabel.setText(tmp.getAsString()); | |
timeline.getKeyFrames().add( | |
new KeyFrame(Duration.seconds(1), | |
new EventHandler() { | |
@Override | |
public void handle(Event event) { | |
tmp.subTime(timeSecond); | |
progressBar.setProgress((time.getAsSeconds()-tmp.getAsSeconds())/(double) time.getAsSeconds()); | |
if(progressBar.getProgress() > 0.99){ | |
progressBar.setStyle("-fx-accent: red;"); | |
}else if(progressBar.getProgress() > 0.75){ | |
progressBar.setStyle("-fx-accent: yellow;"); | |
}else{ | |
progressBar.setStyle("-fx-accent: green;"); | |
} | |
stage.setTitle(tmp.getAsString()); | |
timeLabel.setText(tmp.getAsString()); | |
if(tmp.getAsSeconds() == 0L){ | |
timeline.stop(); | |
progressBar.setStyle("-fx-accent: red;"); | |
} | |
} | |
})); | |
timeline.playFromStart(); | |
} | |
} |
Wyjaśnię co się tutaj dzieje.
Tworzę tutaj obiekt Timeline (linia czasu). Pozwala ona na stworzenie animacji w GUI, której proces będzie realizowany co jakiś określony czas. U mnie tą animacją będzie zmieniający się czas i stan progresBara co sekunde. Następnie ustawiam, że ilość cykli tego obiektu jest niezdefiniowana. Pozwala to na uzyskanie efektu wykonywania animacji bez przerwy, aż wywołana została metoda stop(). Animację także można zatrzymać także metodą pause().
W tym fragmencie powiązuje szerokość progressBar'a do szerokości okna oraz analogicznie jego wysokość.
Dodaje tutaj do listy obserwatorów zmian na lini czasu. Zastosowany jest tutaj popularny wzorzec w GUI obserwator. Do listy dodaje swój KeyFrame, który ma być wywoływany co okres jednej sekundy.
Tutaj obliczany jest aktualny czas do wyświetlenia oraz stosunek czasu, który już minął, w celu ustawienia odpowiedniego stanu procesu dla processBar'a. Jednocześnie w zależności od tego stanu zmienia on swój kolor. Na końcu jest sprawdzany warunek stopu obiektu timeLine.
Po ustaleniu co ma się wykonywać co sekunde, czas na odpalenie naszego licznika.
Minutnik wygląda następujaco:
Tak oto kończę swój wpis. Mam nadzieję, że przybliżyłem troszkę mechanikę tworzenia aplikacji samodzielnych z GUI. Zapraszam do zapoznania się z moim poprzednim wpisem w którym wyjaśniam czym jest JavaFx. A jakby kogoś interesował programik (to jeżeli ma JRE 8) to zapraszam do pobrania tutaj.
Comments
Post a Comment