Twitter: @grzegg
Kategoria: java, Tagi: - - - - .

Java [37] – JavaFX: Listenery i binding czyli detekcja zmian w kontrolkach i synchronizacja wartości

Ważnym elementem programowania w JavieFX jest obsługa zdarzeń związanych na ze zmianami wartości właściwości kontrolek. Często wiąże się to z synchronizacją tych wartości między różnymi kontrolkami.

Nasłuchiwanie zmian

Zaczniemy od „nasłuchiwania”, czyli detekcji zmian wartości właściwości kontrolek i wywoływania kodu w reakcji na takie zdarzenia. Możliwe są tu dwa podejścia związane z dwoma podstawowymi typami „słuchaczy”/”obserwatorów” (ang. listeners) czyli obiektów, których zadaniem jest właśnie obsługa tego typu zdarzeń.

InvalidationListener

Rolą InvalidationListener-a jest po prostu sprawdzanie czy właściwość obiektu, do której go przypisaliśmy się nie zmienia. Zobaczmy to na przykładzie bardzo prostej aplikacji, posiadające tylko jedno pole tekstowe o z góry zdefiniowanej zawartości:

Po uruchomieniu ujrzymy naszą nieskomplikowaną aplikację:


Teraz spróbuj zmienić tekst wpisany w pole. Już kiedy usuniesz pierwsze litery, w terminalu zostanie wypisany komunikat: Nie wolno zmieniać pola!.


Zobaczmy teraz jak to działa. Fragment, który nas szczególnie interesuje wygląda tak:


pole1.textProperty().addListener(new InvalidationListener() {
   @Override
   public void invalidated(Observable observable) {
   System.out.println("Nie wolno zmieniać pola!");
}
});

Rejestrujemy w nim InvalidationListener, który „nasłuchuje” zmiany właściwości textProperty kontrolki pole1. Jeśli wartość tej właściwości ulegnie zmianie, zostanie wykonany kod, który wpisaliśmy używając powyżej klasy anonimowej. Można tej techniki użyć w sytuacji gdy w przypadku zmiany konkretnej wartości, powinny zostać wykonane jakieś działania.

Spróbujmy jeszcze przerobić powyższy kod używając wyrażenia lambda.


       pole1.textProperty().addListener((observable) -> {
                System.out.println("Nie wolno zmieniać pola!");
            }
       );

Jeśli chcesz wydrukować zawartość zmienionego pola, należy w ciele wyrażenia lambda dodać na przykład taką linię:


System.out.println("Nowa wartość to: " + ((ObservableValue) observable).getValue());

i dodać odpowiedni import: import javafx.beans.value.ObservableValue;

Zauważ, że należało tu zastosować rzutowanie na typ ObservableValue.


ChangeListener

Innym sposobem obserwacji zmian właściwości kontrolek jest użycie klasy o nazwie ChangeListener, która pozwala nie tylko na pobranie nowej wartości, ale także „pamięta” o starej. Zmodyfikuj aplikację w ten sposób:


pole1.textProperty().addListener(new ChangeListener() {
      @Override
      public void changed(ObservableValue observable,
            Object oldValue, Object newValue) {
            System.out.println("Zmiana wartości!");
            System.out.println("Stara wartość: " + oldValue);
            System.out.println("Nowa wartość: " + newValue);
       }
});

Teraz możemy pobrać i użyć (i to w prostszy sposób) wartości sprzed i po zmianie. Na przykład jeśli będę usuwał po literze tekst, otrzymam takie komunikaty:


Jeszcze wersja powyższego kodu z użyciem wyrażenia lambda:


pole1.textProperty().addListener((observable, oldValue, newValue) ->{
                System.out.println("Zmiana wartości!");
                System.out.println("Stara wartość: " + oldValue);
                System.out.println("Nowa wartość: " + newValue);
            }
       );

Opisanej techniki można użyć także do zmiany wartości jednej kontrolki w zależności od zmian innej kontrolki. Zmodyfikujmy nieco naszą aplikację, dodając kontrolkę typu Label, zmieniając kontener StackPane na VBox, modyfikując zawartość metody ChangeListener-a oraz rzecz jasna aktualizując importy.

Po zmianie nasza aplikacja zaraz po uruchomieniu wygląda tak:


Jeśli zaczniemy zmieniać zawartość pola tekstowego, od razu będzie się zmieniać zawartość kontrolki Label.


Można też zmienić zawartość kontrolki na inną wartość, na przykład umieścić tam liczbę znaków we wpisanym tekście:


wpisanyTekst.setText((Integer.toString(pole1.getText().length())));


Wiązanie właściwości

Jeśli chcemy bezpośrednio powiązać właściwości dwu kontrolek, to można to zrobić jak pokazałem powyżej za pomocą „listenerów”. Ale w tym celu zwykle stosuje się inną technikę: wiązanie (ang. binding). Przy czym zależność ta może być jednokierunkowa albo dwukierunkowa

Wiązanie jednokierunkowe

Najpierw pokażę, jak można powiązać właściwości dwu kontrolek w jedną stronę, to znaczy w ten sposób, aby właściwość kontrolki A zależała od właściwości kontrolki B ale nie odwrotnie.
Zobaczymy jak to się robi na przykładzie kolejnego programu, a przy okazji poznamy nową kontrolkę i spotkamy się z kontenerem GridPane.

Utwórz nowy projekt i umieść kod:

Uruchomiona aplikacja wygląda nieskomplikowanie:


Po lewej stronie widać suwak (Slider), który jest kontrolką pozwalająca wybrać wartość liczbową zazwyczaj za pomocą myszy choć klawiatury też, o czym za chwilę. Po prawej mamy pole tekstowe. Obie kontrolki zostały umieszczone w kontenerze typu GridPane, który ma charakter siatki, a każde pole w tej siatce ma swoje współrzędne, w związku z czym można elementy interfejsu umieszczać dość równo i precyzyjnie.

Pole tekstowe zawiera aktualną wartość właściwości value suwaka. Jeśli teraz będziemy przesuwać suwak, będą się także zmieniać wartości w polu tekstowym:


Zauważ jednak, że nie można zmienić zawartości pola tekstowego wpisując tam liczbę.

Suwak działa, ale nie wygląda zbyt dobrze. Nie ma na nim żadnej skali, domyślny zakres (0-100) niekoniecznie jest tym, co chcemy a zwracane wartości stanowczo mogłyby być zaokrąglane.

Zatem zmodyfikujmy nieco nasz program, w metodzie start, powyżej linii w której tworzymy TextField umieść zmodyfikowany kod:

double min = -10.0;
double max = 10.0;
double domyslna = 0.0;

// Tworzymy kontrolkę typu Slider, ustawiamy zakres i wartość domyślną
Slider suwak = new Slider(min, max, domyslna);

// Większe podziałki na suwaku - co 1
suwak.setMajorTickUnit(1.0);
// Liczba mniejszych podziałek pomiędzy większymi
suwak.setMinorTickCount(1);
// Pokaż podziałki
suwak.setShowTickMarks(true);
// Pokaż opis podziałki
suwak.setShowTickLabels(true);
// Rozszerzmy suwak do minimum 400 pikseli
suwak.setMinWidth(400);
// Jeśli chcemy przesuwać suwak za pomocą klawiatury, to przesuwaj co 1
suwak.setBlockIncrement(1.0);
// Przyciąganie do znaczników podziałki - zaokrągla liczby
suwak.setSnapToTicks(true);

Teraz suwak wygląda lepiej, przyjmuje wartości od -10 do 10 a na dodatek je zaokrągla:


Wiązanie dwukierunkowe

W naszej aplikacji zawartość pola tekstowego zależy od wartości właściwości value suwaka. Ale ta zależność działa na razie tylko w jedną stronę. Teraz zmienimy ją tak, żeby można było wpisać liczbę w pole tekstowe i zmienić wartość suwaka.

Sprawa byłaby prosta, gdyby obie łączone właściwości były tego samego typu, na przykład obie przechowywały liczby albo łańcuchy znaków. Moglibyśmy wtedy zrobić coś w tym stylu:


kontrolka1.valueProperty().bindBidirectional(kontrolka2B.valueProperty());

Proste, krótkie i zrozumiałe. Problem jednak w tym, że suwak zwraca liczbę zmiennoprzecinkową a pole tekstowe przechowuje tekst. Powyżej, kiedy zależność była w jednym kierunku ratowaliśmy się, wywołując metodę asString(), ale jeśli to ma działać w dwie strony to nie tylko liczba musi być konwertowana na String ale także łańcuch znaków na liczbę. Na szczęście rozwiązanie problemu nie jest takie skomplikowane jakby się wydawało:

Usuń:

wartosc.textProperty().bind(suwak.valueProperty().asString());

Dopisz:


// Tworzymy obiekt typu String Converter, który będzie "konwertował"
// liczby na String-i i odwrotnie
StringConverter tlumacz = new NumberStringConverter();
// łączymy dwustronnie właściwość text kontrolki wartosc
// z wartoscia value kontrolki suwak, przekazujemy także obiekt tlumacz
// aby konwertować wzajemnie liczby i łańcuchy znaków
Bindings.bindBidirectional(wartosc.textProperty(), suwak.valueProperty(), tlumacz);

Przykład

Przyszedł czas na bardziej złożony przykład. Stworzymy aplikację, która będzie obliczała frekwencję genotypów w zależności od frekwencji alleli wg. zależności opisanych w prawie Hardy’ego-Weinberga. Nie będę tu objaśniał dokładnie tego prawa, jeśli nie wiesz lub nie pamiętasz o co chodzi, warto zajrzeć do odpowiednich książek, do prezentacji na mojej stronie albo do Wikipedii. W skrócie tylko przypomnę, że wg. prawa Hardy’ego-Weinberga, jeśli populacja spełnia określone założenia to frekwencje (proporcje) genotypów zależą tylko od frekwencji alleli i wynoszą odpowiednio:

  • PAA = pA2
  • PAa = 2 * pA * pa
  • Paa = pa2

gdzie: pA – frekwencja allelu A, pa – frekwencja allelu a, PAA – frekwencja genotypu AA, PAa – frekwencja genotypu Aa, Paa – frekwencja genotypu aa

Przy czym należy pamiętać, że frekwencje alleli sumują się do 1, podobnie zresztą jak wszystkie frekwencje genotypów:

  • pA + pa= 1
  • PAA + PAa + Paa = 1

Nasza aplikacja będzie na końcu wyglądać tak:


Jak widać znajdują się tam:

  • dwa suwaki, każdy do ustawienia frekwencji dla jednego z alleli.
  • dwa pola tekstowe wyświetlające aktualną wartość frekwencji dla obu alleli ale także umożliwiające wprowadzenie tych wartości
  • etykiety (Label) wyświetlające obliczone wartości frekwencji alleli oraz ich sumę
  • etykiety służące opisowi poszczególnych pól
  • niewidoczny kontener (PanelGrid) pozwalający to wszystko uporządkować.

Aplikacja będzie działać tak:

Użytkownik wprowadza wartość frekwencji jednego z alleli za pomocą suwaka lub pola tekstowego. Następnie będą się wykonywać następujące procesy:

  • będzie się uaktualniała wartość frekwencji na drugiej z kontrolek dla danego allelu (jeśli ustawimy wartość na suwaku, to zostanie uaktualniona wartość pola tekstowego i odwrotnie)
  • zostanie wyliczona i wprowadzona wartość frekwencji drugiego allelu i ta wartość zostanie ustawiona w odpowiednich kontrolkach
  • dla wartości alleli zostaną wyliczone frekwencje genotypów a następnie suma tych frekwencji i wartości te zostaną wprowadzone do odpowiednich etykiet

Jak widać będziemy to mieli do czynienia z dość licznymi wiązaniami, niektóre z nich będą dwustronne inne jednostronne.
Przy okazji poznamy kilka nowych tricków związanych z bindingiem. Pierwsza rzecz, to obliczenia. Nie można definiować łączenia na przykład tak:


suwak_a.valueProperty().bind(1.0 - suwak_A.valueProperty());

To byłoby zbyt proste, prawda? Do prowadzenia obliczeń wykorzystamy odpowiednie metody wystarczające do wykonania prostych operacji arytmetycznych a także zmiany znaku liczby:

  • add() – dodawanie
  • substract() – odejmowanie
  • multiply() – mnożenie
  • divide() – dzielenie
  • negate() – zmiana znaku na przeciwny

Tak więc operacja obliczenia frekwencji jednego allelu w zależności od frekwencji drugiego (pa= 1 – pA) będzie wyglądać tak:


suwak_a.valueProperty().bind(suwak_A.valueProperty().subtract(1.0).negate());

Metoda negate() na końcu została użyta dlatego, że od frekwencji allelu A odjęliśmy 1 otrzymując liczbę ujemną.

Jest jeszcze kolejny problem. Jeśli w ten sposób połączymy dwa suwaki obustronnie to program nie będzie działać. Dlatego na raz będzie możliwe łączenie tylko w jedną stronę, jeśli zmieniamy wartość na suwaku dla allelu A to powinna się wyliczyć i zaktualizować wartość dla suwaka dla allelu a, obliczenia i aktualizacja w drugą stronę powinna się wydarzyć tylko wtedy, gdy zmienimy wartość dla allelu a. Dlatego stworzymy mechanizm „przełączania” łączenia.
Zrobimy to przez przypisanie akcji do kontrolki. Dotychczas wykorzystywaliśmy przy takich okazjach jedynie metodę onAction() powiązaną z przyciskiem, ale JavaFX umożliwia obsługę dla różnych kontrolek wielu róznych zdarzeń jak najechanie kursorem, kliknięcie, opuszczenie kursora itd.

Teraz użyjemy dla suwaka wydarzenia kliknięcia myszą na kontrolce (setOnMousePressed()). Jeśli klikniemy na suwaku (w celu zmiany wartości) zostanie wywołany kod związany z tym wydarzeniem . Najpierw zostanie zerwane łączenie dla tego suwaka a następnie zostanie ustanowione łączenie wartości drugiego suwaka z wartością, którą ustawimy w klikniętym suwaku.


suwak_a.setOnMousePressed((event) -> {
      // usuwanie łączenia dla suwak_a
      suwak_a.valueProperty().unbind();
      // łączenie suwak_A z suwak_a
      suwak_A.valueProperty().bind(suwak_a.valueProperty().subtract(1.0).negate());
});

Podobny kod napiszemy dla drugiego suwaka a także dla pól tekstowych, przy czym dla nich będziemy obsługiwać wydarzenie związane z wciśnięciem klawisza (setOnKeyPressed()).

Inną nowością w niniejszym programie jest użycie klasy DoubleBinding, dzięki której tworzymy obiekty przechowujące liczby zmiennoprzecinkowe dla których możemy tworzyć łączenia z innymi obiektami. Jest to dość wygodne np. w przypadku wartości używanych w obliczeniach.

Ponieważ niektóre z kontrolek (suwaki, etykietki, pola tekstowe) występują w kilku egzemplarzach mających podobne parametry (np. szerokość), aby nie mnożyć kodu, stworzyłem odpowiednie metody, tworzące i zwracające kontrolki o zestandaryzowanych parametrach.

Oto kompletny kod aplikacji, przeanalizuj go:

2 komentarze Java [37] – JavaFX: Listenery i binding czyli detekcja zmian w kontrolkach i synchronizacja wartości

  • Radek

    Czy te metody:

    suwak_A.valueProperty().unbind();
    suwak_a.valueProperty().unbind();

    Odłączają te łączania:

    Bindings.bindBidirectional(wartosc_A.textProperty(), suwak_A.valueProperty(), tlumacz);
    Bindings.bindBidirectional(wartosc_a.textProperty(), suwak_a.valueProperty(), tlumacz);
    ???

    Jeżeli tak to np. przesuwając „suwak a” nie powinna zmieniać się wartość pola tekstowego, z którym jest połączony.

    Mógłby Pan wyjaśnić dokładniej jak działa tu system przełączania łączeń?

  • Waldi

    suwak_A.valueProperty().unbind();

    Odłącza uzależnienie suwaka suwak_A od innych kontrolek. Dzięki temu jak ustawiasz wartość suwaka suwak_A ręcznie nie powstaje kolizja z powodu zaciągnięcia wartości od innych kontrolek – nie zostaje podwójnie nadana wartość suwakowi suwak_A w tym samym czasie.

Leave a Reply