#javafx
Вопрос:
В моем приложении я хочу применить поведение a ToggleGroup
к группе TitledPane
s. Для этого я реализовал это:
ToggleAdapter.java
package sample;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.scene.control.TitledPane;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
public class ToggleAdapter implements Toggle {
final private TitledPane titledPane;
final private ObjectProperty<ToggleGroup> toggleGroupProperty = new SimpleObjectProperty<>();
private ToggleAdapter(TitledPane titledPane) {
this.titledPane = titledPane;
}
@Override
public ToggleGroup getToggleGroup() {
return toggleGroupProperty.get();
}
@Override
public void setToggleGroup(ToggleGroup toggleGroup) {
toggleGroupProperty.set(toggleGroup);
}
@Override
public ObjectProperty<ToggleGroup> toggleGroupProperty() {
return toggleGroupProperty;
}
@Override
public boolean isSelected() {
return titledPane.isExpanded();
}
@Override
public void setSelected(boolean selected) {
titledPane.setExpanded(selected);
}
@Override
public BooleanProperty selectedProperty() {
return titledPane.expandedProperty();
}
@Override
public Object getUserData() {
return titledPane.getUserData();
}
@Override
public void setUserData(Object value) {
titledPane.setUserData(value);
}
@Override
public ObservableMap<Object, Object> getProperties() {
return FXCollections.emptyObservableMap();
}
public static Toggle asToggle(final TitledPane titledPane) {
return new ToggleAdapter(titledPane);
}
}
образец.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.VBox?>
<VBox spacing="7.0" xmlns="http://javafx.com/javafx/15.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
<children>
<TitledPane fx:id="titledPane1" text="Title 1">
<content>
<TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
</content>
</TitledPane>
<TextField />
<TitledPane fx:id="titledPane2" expanded="false" text="Title 2">
<content>
<TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
</content>
</TitledPane>
<TitledPane fx:id="titledPane3" expanded="false" text="Title 3">
<content>
<TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
</content>
</TitledPane>
</children>
<padding>
<Insets bottom="14.0" left="14.0" right="14.0" top="14.0" />
</padding>
</VBox>
Controller.java
package sample;
import javafx.fxml.FXML;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleGroup;
public class Controller {
@FXML private TitledPane titledPane1;
@FXML private TitledPane titledPane2;
@FXML private TitledPane titledPane3;
@FXML
private void initialize() {
final var toggleGroup = new ToggleGroup();
final var toggle1 = ToggleAdapter.asToggle(titledPane1);
toggle1.setToggleGroup(toggleGroup);
final var toggle2 = ToggleAdapter.asToggle(titledPane2);
toggle2.setToggleGroup(toggleGroup);
final var toggle3 = ToggleAdapter.asToggle(titledPane3);
toggle3.setToggleGroup(toggleGroup);
toggleGroup.selectToggle(toggle1);
}
}
Main.java
package sample;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
final Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Мой наивный подход не работает, но я понятия не имею, почему бы и нет. Есть какие-нибудь идеи?
ПРАВКА: Я знаю об Acordion
этом , но это не подходит, потому что я не могу поместить все три TitledPane
s в один родительский контейнер.
Комментарии:
1. Вот реализация для
ToggleButton
. Может быть, вы сможете почерпнуть из этого какие-нибудь идеи. github.com/openjdk/jfx/blob/master/modules/javafx.controls/src/…2. Не изобретайте велосипед заново. Элемент управления уже реализован в стандартном API
Accordion
.3. Привет @kleopatra, я только что изменил пример.
Ответ №1:
Во-первых, вы изобретаете велосипед заново. Не делайте этого; уже Accordion
есть элемент управления, который реализует именно то , что вы пытаетесь здесь сделать.
Все, что вам нужно, это:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Accordion?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TitledPane?>
<Accordion expandedPane="${titledPane1}" xmlns="http://javafx.com/javafx/15.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
<panes>
<TitledPane fx:id="titledPane1" text="Title 1" expanded="true">
<content>
<TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
</content>
</TitledPane>
<TitledPane fx:id="titledPane2" expanded="false" text="Title 2">
<content>
<TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
</content>
</TitledPane>
<TitledPane fx:id="titledPane3" expanded="false" text="Title 3">
<content>
<TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
</content>
</TitledPane>
</panes>
<padding>
<Insets bottom="14.0" left="14.0" right="14.0" top="14.0" />
</padding>
</Accordion>
и
public class Controller {
@FXML private Accordion accordion ;
@FXML private TitledPane titledPane1;
@FXML private TitledPane titledPane2;
@FXML private TitledPane titledPane3;
@FXML
private void initialize() {
accordion.setExpandedPane(titledPane1);
}
}
Это ToggleAdapter
не требуется.
Причина, по которой ваш код не работает, заключается в том, что вы предполагаете, я думаю, что ToggleGroup
он наблюдает за selected
состоянием каждого из своих переключателей и обновляет состояние другого переключателя, когда один из них выбран. Это не так; на самом деле реализация переключения несет ответственность за сохранение единого выбора в своей группе переключений, если она того пожелает. Вы можете сделать это, добавив прослушиватель в выбранное состояние в ToggleAdapter
(но опять же, чтобы подчеркнуть, всегда неправильно заново изобретать функциональность, определенную в стандартном API).
private ToggleAdapter(TitledPane titledPane) {
this.titledPane = titledPane;
selectedProperty().addListener(obs -> {
ToggleGroup tg = getToggleGroup();
if (tg != null) {
if (isSelected()) {
tg.selectToggle(this);
} else if (tg.getSelectedToggle() == this) {
tg.selectToggle(null);
}
}
});
}
Комментарии:
1. Как упоминалось в моем вопросе: я действительно хочу применить поведение a
ToggleGroup
к спискуTitledPane
.Accordion
является aControl
и работает только в том случае, если всеTitlePane
s, о которых идет речь, являются детьми аккордеона.2. @Hannes До сих пор не ясно, почему
Accordion
(или, на худой конец, несколькоAccordion
s) не обеспечивают ту функциональность, которую вы ищете. Но если у вас есть какой-то другой вариант использования, разве модификацияToggleAdapter
не решит проблему?3. просто побочный комментарий: на самом деле реализация переключения отвечает за поддержание единого выбора , который является истинным и реализован, но мне кажется странным (как в полностью перевернутом 😉 для класса, который должен управлять единственным выбором внутри группы.
4. @клеопатра Согласилась, это странно. Я уверен, что в то время это имело смысл для разработчиков API, но я даже не могу предположить, о чем они думали.
5. Я считаю
Accordion
, что это не решает мою проблему, потомуTitledPane
что s в моем реальном случае использования находится в разныхParent
s, которые расположены в разных местах пользовательского интерфейса. Поэтому я не могу поместить всеTitledPane
буквы s в одного родителя, так как это необходимо для правильной работы аккордеона. В своем MRE я попытался устранить сложность пользовательского интерфейса и сузить его до чисто технических требований.
Ответ №2:
Изменение реализации ToggleAdapter
на это фактически решает проблему:
package sample;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.scene.control.TitledPane;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import java.util.Optional;
public class ToggleAdapter implements Toggle {
final private TitledPane titledPane;
final private ObjectProperty<ToggleGroup> toggleGroupProperty = new ObjectPropertyBase<>() {
private ToggleGroup old;
@Override
protected void invalidated() {
if (old != null) {
old.getToggles().remove(ToggleAdapter.this);
}
old = get();
if (get() != null amp;amp; get().getToggles().contains(ToggleAdapter.this) == false) {
get().getToggles().add(ToggleAdapter.this);
}
}
@Override
public Object getBean() {
return ToggleAdapter.this;
}
@Override
public String getName() {
return "toggleGroup";
}
};
@Override
public ToggleGroup getToggleGroup() {
return toggleGroupProperty.get();
}
@Override
public void setToggleGroup(ToggleGroup toggleGroup) {
toggleGroupProperty.set(toggleGroup);
}
@Override
public ObjectProperty<ToggleGroup> toggleGroupProperty() {
return toggleGroupProperty;
}
@Override
public boolean isSelected() {
return titledPane.isExpanded();
}
@Override
public void setSelected(boolean selected) {
titledPane.setExpanded(selected);
}
@Override
public BooleanProperty selectedProperty() {
return titledPane.expandedProperty();
}
@Override
public Object getUserData() {
return titledPane.getUserData();
}
@Override
public void setUserData(Object value) {
titledPane.setUserData(value);
}
@Override
public ObservableMap<Object, Object> getProperties() {
return FXCollections.emptyObservableMap();
}
public static Toggle asToggle(final TitledPane titledPane) {
return new ToggleAdapter(titledPane);
}
public ToggleAdapter(TitledPane titledPane) {
this.titledPane = titledPane;
selectedProperty().addListener(obs -> {
Optional.ofNullable(getToggleGroup()).ifPresent(toggleGroup -> {
if (isSelected()) {
toggleGroup.selectToggle(this);
} else if (toggleGroup.getSelectedToggle() == this) {
toggleGroup.selectToggle(null);
}
});
});
}
}
Спасибо, James_D за идею о том, как изменить реализацию.
Комментарии:
1. неправильно не реализовывать сломанный инвариант, о котором я упоминал в ответе Джеймса
2. @клеопатра, ты имеешь в виду это? github.com/openjdk/jfx/blob/… Я не совсем уверен, с каким делом он справляется, тбх.
3. скопированный комментарий: как бы ни была плоха ToggleGroup, я думаю, что любое решение должно поддерживать инвариант
assertTrue(tg.getToggles().contains(toggle))
для всех управляемых переключений, которые могут быть обработаны в свойстве ToggleGroup адаптера (посмотрите на ToggleButton, как уже отметил @Sedrick 🙂4. Я не понимаю, когда может возникнуть этот инвариант и в каком случае с ним необходимо справиться. Тем более, что я не вижу, как свойство ToggleGroup относится к нему.
5. речь идет не о вас или обо мне — речь идет о будущих читателях: поэтому ответы должны быть как можно более хорошими … дерьмовый оригинал (в ядре javafx) не является оправданием для воспроизведения его дерьмовости *выкл