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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import javafx.application.Application; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.scene.Scene; import javafx.scene.control.TextField; import javafx.scene.layout.StackPane; import javafx.stage.Stage; public class Sluchacze extends Application { @Override public void start(Stage primaryStage) { TextField pole1 = new TextField("Jedynie słuszny tekst"); pole1.textProperty().addListener(new InvalidationListener() { @Override public void invalidated(Observable observable) { System.out.println("Nie wolno zmieniać pola!"); } }); StackPane root = new StackPane(); root.getChildren().add(pole1); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("Sluchacz"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } } |
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
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package sluchacze; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class Sluchacze extends Application { @Override public void start(Stage primaryStage) { TextField pole1 = new TextField("Jedynie słuszny tekst"); Label wpisanyTekst = new Label("................"); pole1.textProperty().addListener((observable, oldValue, newValue) ->{ wpisanyTekst.setText(newValue); } ); VBox root = new VBox(); root.getChildren().add(pole1); root.getChildren().add(wpisanyTekst); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("Sluchacz"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package laczenie; import javafx.application.Application; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Slider; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.stage.Stage; public class Laczenie extends Application { @Override public void start(Stage primaryStage) { // Tworzymy kontrolkę typu Slider Slider suwak = new Slider(); TextField wartosc = new TextField(); // łączymy właściwość text kontrolki wartosc z wartoscia value // kontrolki suwak. Poniewaz textProperty przyjmuje String-i // a value jest wartością liczbową, wywołujmey metode asString() wartosc.textProperty().bind(suwak.valueProperty().asString()); // Tworzymy kontener typu GridPane, GridPane root = new GridPane(); // Ustawiamy odstęp między konternerem a krawędziami okna root.setPadding(new Insets(5)); // Ustawiamy poziome i pionowe odstępy między komórkami (rzędami i kolumnami) root.setHgap(10); root.setVgap(10); // Dodajemy suwak do komórki o współrzędnych 0, 0 // (pierwsza kolumna, pierwszy rząd) root.add(suwak, 0,0); // Dodajemy suwak do komórki o współrzędnych 0, 0 // (druga kolumna, pierwszy rząd) root.add(wartosc, 1,0); Scene scene = new Scene(root, 500, 250); primaryStage.setTitle("Laczenie"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } |
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
// łą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()
– dodawaniesubstract()
– odejmowaniemultiply()
– mnożeniedivide()
– dzielenienegate()
– 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
package hardyweinbergfx; import javafx.application.Application; import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.Slider; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.stage.Stage; import javafx.util.StringConverter; import javafx.util.converter.NumberStringConverter; public class HardyWeinbergFX extends Application { @Override public void start(Stage primaryStage) { // Wywołujemy metodę, która tworzy i zwraca obiekt typu Slider Slider suwak_A = zrobSuwak(); // Wywołujemy metodę, która tworzy i zwraca obiekt typu TextField TextField wartosc_A = zrobTextField(); Label label_A = new Label ("Frekwencja allelu A"); Slider suwak_a = zrobSuwak(); TextField wartosc_a = zrobTextField(); Label label_a = new Label ("Frekwencja allelu a"); // obiekt pozwala na "tłumaczenie" Stringów na liczby i odwrotnie StringConverter<Number> tlumacz = new NumberStringConverter(); // łączymy dwustronnie wartości suwaków i pól tekstowych Bindings.bindBidirectional(wartosc_A.textProperty(), suwak_A.valueProperty(), tlumacz); Bindings.bindBidirectional(wartosc_a.textProperty(), suwak_a.valueProperty(), tlumacz); // kiedy klikniemy na suwak odłączamy wiązanie z nim // oraz łączymy drugi suwak z wartością przeliczoną z wartości // pierwszego suwaka 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()); }); // Jak wyżej, ale w drugą stronę suwak_A.setOnMousePressed((event) -> { suwak_A.valueProperty().unbind(); suwak_a.valueProperty().bind(suwak_A.valueProperty().subtract(1.0).negate()); }); // Jak wyżej ale dla sytuacji gdy zaczynamy pracę z polem tekstowym wartosc_a.setOnKeyPressed((event) -> { suwak_a.valueProperty().unbind(); suwak_A.valueProperty().bind(suwak_a.valueProperty().subtract(1.0).negate()); }); // Jak wyżej, dla drugiego pola tekstowego wartosc_A.setOnKeyPressed((event) -> { suwak_A.valueProperty().unbind(); suwak_a.valueProperty().bind(suwak_A.valueProperty().subtract(1.0).negate()); }); // Tworzymy obiekty przechowujące liczby - wartości frekwencji genotypów, // których wartość jest na bieżąco wyliczana z wartości frekwencji alleli DoubleBinding pAA = suwak_A.valueProperty().multiply(suwak_A.valueProperty()); DoubleBinding pAa = suwak_A.valueProperty().multiply(suwak_a.valueProperty()).multiply(2.0); DoubleBinding paa = suwak_a.valueProperty().multiply(suwak_a.valueProperty()); // Suma wszystkich frekwencji - powinna zawsze wynosić 1 DoubleBinding suma = pAA.add(pAa).add(paa); Label pAATxtLabel = new Label ("P AA"); // Wywołanie metody, która zwraca Label Label pAALabel = zrobLabel(pAA); Label pAaTxtLabel = new Label ("P Aa"); Label pAaLabel = zrobLabel(pAa); Label paaTxtLabel = new Label ("P aa"); Label paaLabel = zrobLabel(paa); Label sumaTxtLabel = new Label ("Suma"); Label sumaLabel = zrobLabel(suma); GridPane root = new GridPane(); root.setPadding(new Insets(5)); root.setHgap(10); root.setVgap(10); root.add(label_A, 0, 1); root.add(suwak_A, 1, 1); root.add(wartosc_A, 2, 1); root.add(label_a, 0, 2); root.add(suwak_a, 1, 2); root.add(wartosc_a, 2, 2); root.add(pAATxtLabel, 3, 1); root.add(pAALabel, 4, 1); root.add(pAaTxtLabel, 3, 2); root.add(pAaLabel, 4, 2); root.add(paaTxtLabel, 3, 3); root.add(paaLabel, 4, 3); root.add(sumaTxtLabel, 3, 4); root.add(sumaLabel, 4, 4); Scene scene = new Scene(root, 800, 200); primaryStage.setTitle("Hardy-Weinberg FX"); primaryStage.setScene(scene); primaryStage.show(); } // Metoda tworzy i zwraca obiekt typu Slider public static Slider zrobSuwak() { Slider suwak = new Slider(0, 1, 0.5); suwak.setMajorTickUnit(0.1); suwak.setMinorTickCount(9); suwak.setShowTickMarks(true); suwak.setShowTickLabels(true); suwak.setMinWidth(500); suwak.setBlockIncrement(0.1); suwak.setSnapToTicks(true); return suwak; } // Metoda tworzy i zwraca obiekt typu TextField public static TextField zrobTextField () { TextField tf = new TextField(); // minimalna i maksymalna szerokość, ponieważ jest taka sama // nie będzie się zmieniać tf.setMaxWidth(43); tf.setMinWidth(43); return tf; } public static Label zrobLabel(DoubleBinding v) { Label l = new Label(); l.setMinWidth(50); l.setMaxWidth(50); l.textProperty().bind(v.asString()); return l; } public static void main(String[] args) { launch(args); } } |
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ń?
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.