W moim drugim tutorialu poświęconym technologi / bibliotece JavaFX chciałbym przede wszystkim pokazać jak dodawać do aplikacji różne kontrolki i obsługiwać zdarzenia, które wywołują. Oprócz tego przedstawię parę sposobów zarządzania kompozycją interfejsu oraz stylizacji jej elementów. Pokażę również jak grupować kontrolki i dodawać do nich ikony.
Aby kurs był uporządkowany i łatwo było w nim znaleźć tylko wycinek całego tematu, podzieliłem go na sektory opisane krótko poniżej.
Tematy zawarte w tym kursie:
- Tworzenie szkieletu aplikacji JavaFX.
- Konstruowanie kompozycji. Tworzenie i osadzanie paneli kompozycji (layoutu).
- Dodawanie kontrolek: radio-buttony, check-boxy i choice-box. Grupowanie kontrolek.
- Stylizacja komponentów programu.
- Obsługa zdarzeń.
1. Tworzenie szkieletu aplikacji JavaFX
Oto on:
public class BasicControls extends Application
{
private Scene scene; // scena
private BorderPane rootPane; // główny kontener
private VBox rightContainer, leftContainer; // kontenery boczne
private Pane centerContainer; // kontener centralny
private RadioButton rB1, rB2, rB3;
private CheckBox cB1, cB2, cB3;
private ChoiceBox<Object> choiceBox;
private final ToggleGroup toggleGroup = new ToggleGroup();
private Image icon, icon_un;
private ImageView iconView;
//
=============================================================================
private void prepareScene(Stage primaryStage)
{
}
//
=============================================================================
public void start(Stage primaryStage)
{
prepareScene(primaryStage);
primaryStage.setTitle("Basic
controls example");
primaryStage.setScene(scene);
primaryStage.show();
}
//
=============================================================================
public static void main(String[] args)
{
launch(args);
}
//
=============================================================================
}
Wewnątrz klasy, na samej górze pozwoliłem sobie zadeklarować większość jej atrybutów. Oczywiście podczas pisania struktury klasy, zazwyczaj nie jesteśmy w stanie już na starcie przewidzieć wszystkich potrzebnych później zmiennych i deklarujemy je na bieżąco. Ja zrobiłem inaczej aby nie musieć co chwila o tym pisać :) .
Niżej wstawiamy na razie pustą metodę private void prepareScene(Stage primaryStage), która posłuży jako konstruktor i przygotuje scenę. Dzięki temu nie będziemy musieli wszystkiego ładować do metody start(...), co pozwoli zachować większą kontrolę nad kodem.
Dalej, wspomniana już metoda public void start(Stage primaryStage) - obowiązkowa, jeżeli chcemy w ogóle uruchomić później nasz program.
Na końcu metoda public static void main(String[] args) - nieobowiązkowa, ale na wszelki wypadek wstawiamy.
2. Konstruowanie kompozycji. Tworzenie i osadzanie paneli kompozycji (layoutu).
Podczas projektowania interfejsu użytkownika, niezwykle wygodne jest korzystanie z tzw. "Built-in Layout Panes" czyli wbudowanych paneli kompozycji. Oszczędzają one programiście wiele pracy i pozwalają zachować płynność i dynamikę interfejsu, dlatego też z nich skorzystamy.
Zanim jednak napiszemy odpowiedni kod, powinniśmy najpierw zastanowić się jaki układ elementów chcemy uzyskać. W tej aplikacji umieścimy: radio-buttony, check-boxy i choice-boxa. Powiedzmy, że chcielibyśmy, aby te pierwsze znajdowały się jeden pod drugim i trzymały prawego marginesu, check-boxy podobnie, tyle że z lewej strony, natomiast ostatni z elementów umieścimy gdzieś po środku.
Teraz, kiedy mamy już konkretną wizję, możemy skorzystać z konkretnych komponentów. Aby uzyskać pożądany efekt wypełniamy nieco metodę prepareScene(Stage primaryStage), która wygląda teraz tak:
private void prepareScene(Stage primaryStage)
{
String cssPath;
primaryStage.setMinWidth(450);
rootPane = new BorderPane();
rightContainer = new VBox(10);
rightContainer.setId("rightContainer");
leftContainer = new VBox(10);
leftContainer.setId("leftContainer");
centerContainer = new Pane();
centerContainer.setId("centerContainer");
// ==============================
cssPath = this.getClass().getResource("application.css").toExternalForm();
rootPane.setLeft(leftContainer);
rootPane.setRight(rightContainer);
rootPane.setCenter(centerContainer);
scene = new Scene(rootPane, 700, 550);
scene.getStylesheets().add(cssPath);
}rootPane = new BorderPane(); - nasz nadrzędny panel - rootPane staje się instancją klasy BorderPane. Klasa ta jest odpowiedzialna za odpowiedni rozkład elementów interfejsu. Konkretnie umożliwia ona podział kompozycji na następujące obszary: lewo, prawo, środek, góra i dół. Czyli dokładnie to czego potrzebowaliśmy. Nie należy martwić się tym, że np. nie byliśmy zainteresowani zagospodarowaniem górnego obszaru, bo jeżeli tego nie zrobimy, to po prostu go nie będzie.
Zauważmy, że zarówno po lewej jak i prawej stronie rootPane'a chcieliśmy, aby elementy tam umieszczone układały się wierszami, jeden pod drugim. Taką funkcjonalność daje nam następny panel kompozycji: VBox.
Stąd w kolejnych linijkach inicjalizujemy lewy i prawy kontener jako własnie VBox:
rightContainer = new VBox(10);
rightContainer.setId("rightContainer");
leftContainer = new VBox(10);
leftContainer.setId("leftContainer");
Gdybyśmy chcieli teraz uruchomić program to naszym oczom nie ukaże się nic interesującego, ponieważ aby zobaczyć jak działają panele kompozycji, musimy coś w nich umieścić i zrobimy to w kolejnym punkcie.
3. Dodawanie kontrolek: radio-buttony, check-boxy i choice-box. Grupowanie kontrolek.
Kolejnym krokiem będzie dodanie kontrolek do naszej aplikacji. Jednej z nich przypiszemy również ikonkę. W tym celu zaraz po linijce String cssPath; dopisujemy następujące, tak aby początek naszej metody wyglądał w ten sposób:
private void prepareScene(Stage primaryStage)
{
String cssPath;
icon = new Image(getClass().getResourceAsStream("ok.png"));
icon_un = new Image(getClass().getResourceAsStream("ok_unselected.png"));
iconView = new ImageView(icon_un);
Następnie, przed komentarzem oddzielającym: // ============================== umieszczamy kod dodający nasze kontrolki:
// ========= RadioButtons =========
rB1 = new RadioButton("1 radio button");
rB1.setToggleGroup(toggleGroup);
rB2 = new RadioButton("2 radio button");
rB2.setToggleGroup(toggleGroup);
rB3 = new RadioButton();
rB3.setGraphic(iconView);
rB3.setToggleGroup(toggleGroup);
rightContainer.getChildren().addAll(rB1, rB2, rB3);
// ========= CheckBoxes =========
cB1 = new CheckBox("1 check-box");
cB2 = new CheckBox("2 check-box");
cB3 = new CheckBox("3 check-box");
leftContainer.getChildren().addAll(cB1, cB2, cB3);
// ========= Choice Box =========
choiceBox = new ChoiceBox<>(FXCollections.observableArrayList("lemon", "apple", "orange",
new Separator(), "chair"));
choiceBox.setValue("lemon");
// Sztywne
ustawienie Lokalizacji
choiceBox.setLayoutY(100);
choiceBox.setLayoutX(50);
centerContainer.getChildren().add(choiceBox);
rootPane.setCenter(centerContainer);
Za pomocą metody setGraphic ustawiamy grafikę dla kontrolki, a na koniec dodajemy wszystkie radio-buttony do stworzonego wcześniej prawego kontenera.
Przy check-boxach mamy podobną sytuację, nic nowego się tam nie dzieje. Trochę dziwnie i odpychająco wygląda natomiast konstruktor choice-boxa, jednak po krótkiej analizie nie będzie on już taki straszny. Operatory <> sugerują, że jest to konstruktor generyczny, co oznacza, że nasze "pudło wyboru" może przechowywać różne obiekty. Jako argument przyjmuje strukturę observableArrayList z klasy FXCollections. Jest to po prostu generyczna, dynamiczna stryktura danych, która pozwala nam przechować różne elementy w klasie ChoiceBox. Prefiks observable informuje, że konstrukcja może dynamicznie reagować na wprowadzane zmiany. Jako parametry podałem łańcuchy znakowe odpowiadające kilku owocom oraz jeden oznaczający krzesło. Jako, że ten ostatni nie pasuje do poprzednich, umieściłem obiekt klasy Separator, który wizualnie oddzieli te obiekty. Dalej, za pomocą setValue ustawiamy wartość na "lemon" (czy cokolwiek co wpisaliście), dzięki czemu, po uruchomieniu programu, coś (lemon) będzie już wybrane. Następnie na sztywno ustawiamy lokalizację kontrolki i dodajemy ją do centralnego kontenera, którego z kolei wstawiamy do rootPane'a.
Uruchommy teraz naszą aplikację. Oczywiście z zawodem stwierdzimy, że wygląda jak Fiat 126p przy najnowszym Lamborgini, czyli co najmniej biednie. Możemy już jednak zaobserwować jak działają panele kompozycji. Zmieniając rozmiar okna, zauważymy, że elementy które miały być po lewej, trzymają się lewej strony, przyciski radiowe przylegają do prawej, a "pudło wyboru" trzyma się jednego punktu wewnątrz centralnego kontenera. U mnie wygląda to tak:
Jak już poklikamy w kontrolki i sprawdzimy czy wszystko działa, zabierzemy się do podrasowania layoutu.
4. Stylizacja komponentów programu.
Czas podmalować nieco naszą aplikację. W tym celu otwieramy arkusz css (jeżeli IDE nie stworzyło go automatycznie należy to zrobić ręcznie) i wpisujemy:
#rightContainer
{
-fx-background-color: rgb(54,54,54);
-fx-padding: 10 20 10 20;
}
#leftContainer
{
-fx-background-color: rgb(54,54,54);
-fx-padding: 10 20 10 20;
}
#centerContainer
{
-fx-background-color: linear-gradient(#A0C0F0,
#D0D0D0);
}
.radio-button
{
-fx-text-fill: rgb(211,211,211);
}
.check-box
{
-fx-text-fill: rgb(211,211,211);
}
Znakiem '#' poprzedziliśmy wszystkie elementy o danym ID, natomiast '.' obiekty należące do danej rodziny / klasy.
Właściwości:
-fx-background-color odpowiada za kolorowanie tła.
-fx-padding wstawia wewnętrzne marginesy.
-fx-text-fill ustawia kolor tekstu.
Po tych niewielu linijkach wstawionych do arkusza, aplikacja powinna prezentować się już znacznie lepiej:
5. Obsługa zdarzeń.
Kiedy nasz program już jakoś wygląda, pozostało nam tylko dodać do niego obsługę zdarzeń. Nie miałem pomysłu na to, co ciekawego mógłby robić, więc ograniczyłem się do tego, by po jakiejś akcji został wyświetlony komunikat informujący co było jej źródłem (mam nadzieję, że w kolejnych kursach uda mi się przedstawić bardziej funkcjonalne aplikacje). Dodatkowo sprawimy aby radio-button z ikonką zmieniał swój obrazek kiedy jest zaznaczony.
Ponieważ chcemy wyświetlić komunikat, musimy go mieć gdzie wyświetlić. W tym celu na samej górze w miejscu gdzie deklarowaliśmy wszystkie zmienne dopiszmy:
private Label eventLabel;
private Pane centerContainer, bottomContainer; // kontener centralny i dolny
Dalej w metodzie prepareScene(...), po ustawieniach dla choice-boxa wstawiamy fragment tworzący naszą strefę komunikatów:
// ========= eventLabel ==========
eventLabel = new Label();
eventLabel.setStyle("-fx-text-fill: #0AF;");
bottomContainer = new Pane();
bottomContainer.getChildren().add(eventLabel);
bottomContainer.setStyle("-fx-background-color:
linear-gradient(rgb(54,54,54) 25%, rgb(30,30,30))");
rootPane.setBottom(bottomContainer);
Kolejnym krokiem będzie implementacja interfejsów: EventHandler<> oraz ChangeListener<>, które będą bezpośrednio odpowiedzialne za wychwytywanie i obsługę zdarzeń.
Tak więc dostawiamy klauzulę informującą o tym:
public class BasicControlsExample extends Application implements EventHandler<Event>, ChangeListener<Toggle>
Oraz dopisujemy chwilowo puste szkielety metod, które musimy sami zaimplementować:
//
=============================================================================
public void handle(Event event)
{
}
//
=============================================================================
public void changed(ObservableValue<? extends Toggle> ov, Toggle oldRB, Toggle currentRB)
{
}
//
=============================================================================
Zacznijmy od tego ostatniego. Oczywiście argumenty metody wyglądają przerażająco, ale spokojnie, da się to okiełznać.
ObservableValue<? extends Toggle> ov - Ogólnie mówiąc jest to zmienna referencyjna generycznego interfejsu ObservableValue (omg !) - obserwator, który wywołuje metodę changed(...) gdy jego wartość ulegnie zmianie. Mówiąc po ludzku, jeżeli cokolwiek zmieni się w grupie radio-buttonów - toggleGroup, wówczas zostanie wywołana metoda changed(...).
oldRB - wcześniej zaznaczona kontrolka,
currentRB - aktualnie zaznaczona kontrolka.
My będziemy korzystać tylko z currentRB. Wypełniamy tę piekielną metodę:
public void changed(ObservableValue<? extends Toggle> ov, Toggle oldRB, Toggle currentRB)
{
RadioButton rB = (RadioButton) currentRB;
eventLabel.setText(rB.getText());
if (rB.getGraphic() != null)
{
iconView.setImage(icon);
eventLabel.setText("OK");
}
else
iconView.setImage(icon_un);
}
Możemy teraz uruchomić nasz program i sprawdzić co się będzie działo podczas klikania radio-buttonów. Nic się nie będzie działo, a to dlatego, że nie przypisaliśmy im żadnego słuchacza zdarzeń. Czyli nasza klasa implementująca changeListenera, nie nasłuchuje co się dzieje z przyciskami i w rezultacie metoda change(...) nigdy nie jest wywoływana. Dzięki zgrupowaniu nie musimy jednak przypisywać słuchacza do każdego przycisku osobno, ale wystarczy dodać go do toggleGroup. W związku z tym, w miejscu gdzie ustawialiśmy radio-buttony (może być na początku), wpisujemy:
toggleGroup.selectedToggleProperty().addListener(this);
Oczywiście za pomocą słówka this, jako słuchacza ustawiamy naszą klasę BasicControls, ponieważ to właśnie ona implementuje potrzebny interfejs i udostępnia niezbędną metodę. Teraz po odpaleniu aplikacji Wszystko powinno działać tak jak chcieliśmy:
Zanim przejdziemy do implementacji metody handle(...), przypiszmy od razu wszystkim pozostałym kontrolkom 'asystenta zdarzeń'. W sekcji check-boxów wstawiamy:
cB1.addEventHandler(ActionEvent.ACTION, this);
cB2.addEventHandler(ActionEvent.ACTION, this);
cB3.addEventHandler(ActionEvent.ACTION, this);
choiceBox.addEventHandler(ActionEvent.ACTION, this);
Jak widać EventHandler'a ustawiamy setterem o mało zaskakującej nazwie: setEventHandler(...). Pierwszym parametrem tej metody jest typ zdarzeń, który chcemy obsługiwać, dzięki któremu możemy ograniczyć się tylko do wyłapywania konkretnych akcji, lub też możemy chcieć nasłuchiwać wszystkiego co bezpośrednio wpływa na kontrolkę. Drugi parametr to oczywiście klasa implementująca interfejs EventHandler. Gdybyśmy jako pierwszy argument podali np. ActionEvent.ANY, wówczas metoda handle byłaby wywoływana również wtedy, gdy kursor tylko najechałby na przycisk. Nam w zupełności wystarczy typ ActionEvent.ACTION, aby nasłuchiwać czy jakaś kontrolka została wciśnięta lub odznaczona.
Do zrobienia została nam już tylko implementacja metody handle(...). Oto ona:
public void handle(Event event)
{
Object source = event.getSource();
if (source == cB1)
{
if (cB1.isSelected())
eventLabel.setText("check-box 1 - checked");
else
eventLabel.setText("check-box 1 - clear");
}
else if (source == cB2)
{
if (cB2.isSelected())
eventLabel.setText("check-box 2 - checked");
else
eventLabel.setText("check-box 2 - clear");
}
else if (source == cB3)
{
if (cB3.isSelected())
eventLabel.setText("check-box 3 - checked");
else
eventLabel.setText("check-box 3 - clear");
}
else if (source == choiceBox)
eventLabel.setText(choiceBox.getValue().toString());
}
if (source == cB1) - jeżeli źródłem zdarzenia był przycisk cB1, wówczas:
if (cB1.isSelected()) - jeżeli jest zaznaczony:
eventLabel.setText("check-box 1 - checked"); - ustaw tekst w etykiecie zdarzeń (informujący o wciśnięciu).
W przeciwnym wypadku:
eventLabel.setText("check-box 1 - clear"); - ustaw tekst w etykiecie zdarzeń (informujący o zwolnieniu).
Kolejne instrukcje działają analogicznie. Warto zwrócić uwagę, że przy większych projektach, lepiej by było zamiast pisać "check-box X", napisać cBX.getText(), lub ustawiać jakieś id. i je sczytywać. Wówczas, kiedy zechcemy zmienić tekst przypisany do jakiegoś przycisku, nie będziemy musieli robić tego w paru miejscach w kodzie, a tylko raz.
Jeżeli wszystko zrobiliśmy dobrze to nasz program kursowy jest już całkowicie sprawny :) .
Źródło:
Cały kod aplikacji, arkusz css + użyte ikonki: źródło
Linki:
Strona główna firmy Oracle dot. JavaFX: JavaFX
Jak pracować z róznymi panelami kompozycji: Layouts Panes
Używanie kontrolek interfejsu: Using UI Controls
Obsługa zdarzeń: Events Handling
Dzięki wielkie za tutorial. Bardzo mi się przydał jako jedna z pierwszych lekcji JavaFX.
OdpowiedzUsuń24 yr old Librarian Taddeusz Doppler, hailing from Bow Island enjoys watching movies like "Awakening, The" and Astronomy. Took a trip to Himeji-jo and drives a 911. ta strona
OdpowiedzUsuń40 year old Clinical Specialist Ellary Govan, hailing from Victoriaville enjoys watching movies like Red Lights and Sculpting. Took a trip to Pearling and drives a Ferrari F2004. Oficjalna strona
OdpowiedzUsuń