如何通过 JavaFX 中的单选按钮限制最大可选复选框?

How to put constrains on maximum selectable CheckBoxes via RadioButtons in JavaFX?

请看下图,你就会明白我的应用程序的布局。

我希望能够动态选择多少 CheckBox(启用下拉菜单)是可选的(固定数量)。我想用这 3 个 RadioButton.

来实现

在垂直模式下,必须选择所有 4 个 CheckBox(不少于)。在混合模式下,只有 2 CheckBox 必须可用(不多也不少)。在水平模式下,仅需选择 1 CheckBox(不能更多)。重要的是用户能够选择特定的组合 ComboBoxes(例如:我们处于混合模式,选择 1 和 2 不同于选择 1 和 3)。

解决方案

public class ConfigurationEditDialogController {

// Data Acquisition Tab

private ObservableList<String> options =
        FXCollections.observableArrayList(
                "ciao",
                "hello",
                "halo"
        );

@FXML
private PrefixSelectionComboBox<String> testBus1ComboBox = new PrefixSelectionComboBox<>();
@FXML
private PrefixSelectionComboBox<String> testBus2ComboBox = new PrefixSelectionComboBox<>();
@FXML
private PrefixSelectionComboBox<String> testBus3ComboBox = new PrefixSelectionComboBox<>();
@FXML
private PrefixSelectionComboBox<String> testBus4ComboBox = new PrefixSelectionComboBox<>();
@FXML
private CheckBox checkbox1;
@FXML
private CheckBox checkbox2;
@FXML
private CheckBox checkbox3;
@FXML
private CheckBox checkbox4;

private ObservableSet<CheckBox> selectedCheckBoxes = FXCollections.observableSet();
private ObservableSet<CheckBox> unselectedCheckBoxes = FXCollections.observableSet();

private IntegerBinding numCheckBoxesSelected = Bindings.size(selectedCheckBoxes);
private int maxNumSelected =  2;

@FXML
private RadioButton verticalMode;
@FXML
private RadioButton horizontalMode;
@FXML
private RadioButton hybridMode;


private Stage dialogStage;
private Configuration configuration;
private boolean okClicked = false;

/**
 * Initializes the controller class. This method is automatically called
 * after the fxml file has been loaded.
 */
@FXML
private void initialize() {
    testBus1ComboBox.setItems(options);
    testBus2ComboBox.setItems(options);
    testBus3ComboBox.setItems(options);
    testBus4ComboBox.setItems(options);
    configureCheckBox(checkbox1);
    configureCheckBox(checkbox2);
    configureCheckBox(checkbox3);
    configureCheckBox(checkbox4);

    numCheckBoxesSelected.addListener((obs, oldSelectedCount, newSelectedCount) -> {
        if (newSelectedCount.intValue() >= maxNumSelected) {
            unselectedCheckBoxes.forEach(cb -> cb.setDisable(true));
        } else {
            unselectedCheckBoxes.forEach(cb -> cb.setDisable(false));
        }
    });


    testBus1ComboBox.disableProperty().bind(checkbox1.selectedProperty().not());
    testBus2ComboBox.disableProperty().bind(checkbox2.selectedProperty().not());
    testBus3ComboBox.disableProperty().bind(checkbox3.selectedProperty().not());
    testBus4ComboBox.disableProperty().bind(checkbox4.selectedProperty().not());

}


private void configureCheckBox(CheckBox checkBox) {

    if (checkBox.isSelected()) {
        selectedCheckBoxes.add(checkBox);
    } else {
        unselectedCheckBoxes.add(checkBox);
    }

    checkBox.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
        if (isNowSelected) {
            unselectedCheckBoxes.remove(checkBox);
            selectedCheckBoxes.add(checkBox);
        } else {
            selectedCheckBoxes.remove(checkBox);
            unselectedCheckBoxes.add(checkBox);
        }

    });

}

FXML 文件的选项卡 我想在此选项卡中实现 fabian 解决方案,但是不需要像我那样使用 fxml。

<Tab closable="false" text="Data Acquisition">
           <content>
                <GridPane prefHeight="254.0" prefWidth="404.0">
                    <columnConstraints>
                        <ColumnConstraints hgrow="SOMETIMES" maxWidth="218.0" minWidth="10.0" prefWidth="111.0" />
                        <ColumnConstraints hgrow="SOMETIMES" maxWidth="519.0" minWidth="10.0" prefWidth="490.0" />
                    <ColumnConstraints hgrow="SOMETIMES" maxWidth="316.0" minWidth="10.0" prefWidth="71.0" />
                    </columnConstraints>
                    <rowConstraints>
                    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                    </rowConstraints>
                    <children>
                        <Label text="Test Bus 1" GridPane.rowIndex="2" />
                        <Label text="Test Bus 2" GridPane.rowIndex="3" />
                        <Label text="Test Bus 3" GridPane.rowIndex="4" />
                        <Label text="Test Bus 4" GridPane.rowIndex="5" />
                    <PrefixSelectionComboBox fx:id="testBus1ComboBox" prefHeight="31.0" prefWidth="300.0" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="2" />
                    <PrefixSelectionComboBox fx:id="testBus2ComboBox" prefWidth="300.0" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="3" />
                    <PrefixSelectionComboBox fx:id="testBus3ComboBox" prefWidth="300.0" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="4" />
                    <PrefixSelectionComboBox fx:id="testBus4ComboBox" prefWidth="300.0" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="5" />
                    <CheckBox fx:id="checkbox1" mnemonicParsing="false" GridPane.columnIndex="2" GridPane.rowIndex="2" />
                    <CheckBox fx:id="checkbox2" mnemonicParsing="false" GridPane.columnIndex="2" GridPane.rowIndex="3" />
                    <CheckBox fx:id="checkbox3" mnemonicParsing="false" GridPane.columnIndex="2" GridPane.rowIndex="4" />
                    <CheckBox fx:id="checkbox4" mnemonicParsing="false" GridPane.columnIndex="2" GridPane.rowIndex="5" />
                    <Label text="Sample Mode" GridPane.rowIndex="1" />
                    <RadioButton fx:id="verticalMode" mnemonicParsing="false" selected="true" text="Vertical " GridPane.columnIndex="1" GridPane.rowIndex="1">
                       <toggleGroup>
                          <ToggleGroup fx:id="SampleModeGroup" />
                       </toggleGroup>
                    </RadioButton>
                    <RadioButton fx:id="hybridMode" mnemonicParsing="false" text="Hybrid" toggleGroup="$SampleModeGroup" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="1" />
                    <RadioButton fx:id="horizontalMode" mnemonicParsing="false" text="Horizontal" toggleGroup="$SampleModeGroup" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="1" />
                    </children>
                </GridPane>
           </content>
        </Tab>

我只能手动设置哪个数字是使用 private int maxNumSelected = 2; 允许的最大值(但不是最小值)。但是我想通过 RadioButton.

来操纵它们

您可以在所有RadioButton上设置一个侦听器,并且选择一个侦听器,禁用或启用每个ComboBox/CheckBox节点的容器。

这是一个演示这一点的示例应用程序。我用纯 Java(没有 FXML)构建了 UI,只是为了将所有内容都放在一个帖子中。重要的部分是添加到 RadioButtons.

的三个侦听器
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {

        // Root layout
        VBox root = new VBox(5);
        root.setPadding(new Insets(10));
        root.setAlignment(Pos.CENTER);

        // Radio buttons
        HBox hbRadios = new HBox(10);
        hbRadios.setAlignment(Pos.CENTER);
        ToggleGroup tglRadioSelections = new ToggleGroup();
        RadioButton rdoVertical = new RadioButton("Vertical");
        RadioButton rdoHybrid = new RadioButton("Hybrid");
        RadioButton rdoHorizontal = new RadioButton("Horizontal");
        tglRadioSelections.getToggles().addAll(rdoVertical, rdoHybrid, rdoHorizontal);
        hbRadios.getChildren().addAll(rdoVertical, rdoHybrid, rdoHorizontal);

        // ComboBoxes and CheckBoxes
        VBox vbSelections = new VBox(10);
        ComboBox cbo1 = new ComboBox();
        ComboBox cbo2 = new ComboBox();
        ComboBox cbo3 = new ComboBox();
        ComboBox cbo4 = new ComboBox();
        CheckBox chk1 = new CheckBox();
        CheckBox chk2 = new CheckBox();
        CheckBox chk3 = new CheckBox();
        CheckBox chk4 = new CheckBox();

        // Create the containers for each selection group
        HBox hbSelection1 = new HBox(10);
        hbSelection1.getChildren().addAll(cbo1, chk1);
        HBox hbSelection2 = new HBox(10);
        hbSelection2.getChildren().addAll(cbo2, chk2);
        HBox hbSelection3 = new HBox(10);
        hbSelection3.getChildren().addAll(cbo3, chk3);
        HBox hbSelection4 = new HBox(10);
        hbSelection4.getChildren().addAll(cbo4, chk4);

        // Add listeners for each radio button to enable appropriate selections
        rdoVertical.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
                hbSelection1.setDisable(!newValue);
                hbSelection2.setDisable(!newValue);
                hbSelection3.setDisable(!newValue);
                hbSelection4.setDisable(!newValue);
        });
        rdoHybrid.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
            hbSelection1.setDisable(!newValue);
            hbSelection2.setDisable(!newValue);
            hbSelection3.setDisable(!newValue);
            hbSelection4.setDisable(newValue);
        });
        rdoHorizontal.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
            hbSelection1.setDisable(!newValue);
            hbSelection2.setDisable(newValue);
            hbSelection3.setDisable(newValue);
            hbSelection4.setDisable(newValue);
        });

        // Build the scene
        vbSelections.getChildren().addAll(hbSelection1, hbSelection2, hbSelection3, hbSelection4);
        root.getChildren().addAll(hbRadios, vbSelections);
        stage.setScene(new Scene(root));

        stage.show();
    }
}

有几点需要注意。您将需要命名容器 enable/disable 适当的选择区域。这里它们被称为 hbSelection#,这是我们添加复选框和组合框的地方。

在监听器中,您会看到我基本上只是根据 newValue 设置每个 HBox 的禁用 属性,如果选择 RadioButton 则为 true false如果不是。

可能有更有效的方法来处理这个问题,但这绝对是一种方法。

我不建议对 CheckBoxes/ComboBoxes 和不同地方的案例进行硬编码。使用相同的值来创建 CheckBoxes/ComboBoxes 和修改允许的 selected CheckBoxes 数量。但是,这会阻止您单独在 fxml 中创建外观。

除非你想让用户出现一些相当混乱的行为,否则你需要允许用户 select 少于所需的 CheckBox 数量,因为你无法真正分辨出哪个 CheckBox 到 select 当取消 select 一个。您可以 disable/enable 提交表单的按钮或一些类似的控件...

private static HBox createModesRadios(IntegerProperty count, Mode... modes) {
    ToggleGroup group = new ToggleGroup();
    HBox result = new HBox(10);
    for (Mode mode : modes) {
        RadioButton radio = new RadioButton(mode.getText());
        radio.setToggleGroup(group);
        radio.setUserData(mode);
        result.getChildren().add(radio);
    }
    if (modes.length > 0) {
        group.selectToggle((Toggle) result.getChildren().get(0));
        count.bind(Bindings.createIntegerBinding(() -> ((Mode) group.getSelectedToggle().getUserData()).getCount(), group.selectedToggleProperty()));
    } else {
        count.set(0);
    }
    return result;
}

private static void updateCheckBoxes(CheckBox[] checkBoxes, int requiredCount, int unmodifiedIndex) {
    if (unmodifiedIndex >= 0 && checkBoxes[unmodifiedIndex].isSelected()) {
        requiredCount--;
    }
    int i;
    for (i = 0; i < checkBoxes.length && requiredCount > 0; i++) {
        if (i != unmodifiedIndex && checkBoxes[i].isSelected()) {
            requiredCount--;
        }
    }

    for (; i < checkBoxes.length; i++) {
        if (i != unmodifiedIndex) {
            checkBoxes[i].setSelected(false);
        }
    }
}

@Override
public void start(Stage primaryStage) {
    Mode[] modes = new Mode[]{
        new Mode("Vertical", 4),
        new Mode("Hybrid", 2),
        new Mode("Horizontal", 1)
    };

    ToggleGroup group = new ToggleGroup();
    IntegerProperty elementCount = new SimpleIntegerProperty();

    HBox radioBox = createModesRadios(elementCount, modes);
    GridPane grid = new GridPane();
    VBox root = new VBox(10, radioBox);

    int count = Stream.of(modes).mapToInt(Mode::getCount).max().orElse(0);
    ObservableMap<Integer, String> elements = FXCollections.observableHashMap();

    ObservableList<String> options = FXCollections.observableArrayList(
            "ciao",
            "hello",
            "halo");

    CheckBox[] checkBoxes = new CheckBox[count];

    elementCount.addListener((o, oldValue, newValue) -> {
        // uncheck checkboxes, if too many are checked
        updateCheckBoxes(checkBoxes, newValue.intValue(), -1);
    });

    for (int i = 0; i < count; i++) {
        final Integer index = i;
        CheckBox checkBox = new CheckBox();
        checkBoxes[i] = checkBox;

        ComboBox<String> comboBox = new ComboBox<>(options);
        comboBox.valueProperty().addListener((o, oldValue, newValue) -> {
            // modify value in map on value change
            elements.put(index, newValue);
        });
        comboBox.setDisable(true);

        checkBox.selectedProperty().addListener((o, oldValue, newValue) -> {
            comboBox.setDisable(!newValue);
            if (newValue) {
                // put the current element in the map
                elements.put(index, comboBox.getValue());

                // uncheck checkboxes that exceede the required count keeping the current one unmodified
                updateCheckBoxes(checkBoxes, elementCount.get(), index);
            } else {
                elements.remove(index);
            }

        });
        grid.addRow(i, comboBox, checkBox);
    }

    Button submit = new Button("submit");
    submit.setOnAction(evt -> System.out.println(elements));

    // enable submit button iff the number of elements is correct
    submit.disableProperty().bind(elementCount.isNotEqualTo(Bindings.size(elements)));

    root.getChildren().addAll(grid, submit);

    final Scene scene = new Scene(root, 400, 400);

    primaryStage.setScene(scene);
    primaryStage.show();
}
public class Mode {

    private final String text;
    private final int count;

    public Mode(String text, int count) {
        this.text = text;
        this.count = count;
    }

    public String getText() {
        return text;
    }

    public int getCount() {
        return count;
    }

}