dynamically populate ChoiceBox from editable colum

2019-03-06 04:10发布

问题:

Basically the question title says it all. I have a column of strings (called type) in a tableview and a corresponding column of numbers (called size), each row representing an object CargoItem, which has the two properties type and size. Both columns are editable. The TableView is associated with the corresponding observable list of CargoItem, called cargoList.

A ChoiceBox is initially set to the observable list. I have a listener attached to the ChoiceBox, so it can update a TextField with the corresponding size attribute. This works OK: if you type a new value into a cell in the size column, the corresponding value in the TextField will update (once the ChoiceBox item is selected).

At the moment if I edit a cell in the type column, the items in the ChoiceBox won't update. I have a diagnostic button which will verify that the observable list is updating properly. I think I need to put a listener into the cells of the type column, but not sure how.

Following is a minimal working example, with 4 files: Table_test02.fxml, CargoItem.java, ControllerTest.java, and TestApp.java

Table_test02.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.text.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<AnchorPane fx:id="root" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="252.0" prefWidth="232.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="so_question01.ControllerTest">
   <children>
      <TableView fx:id="cargoTable" editable="true" layoutX="9.0" layoutY="30.0" prefHeight="107.0" prefWidth="213.0">
         <columnResizePolicy>
            <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
         </columnResizePolicy>
         <columns>
            <TableColumn fx:id="cargoTableTypeCol" maxWidth="130.0" minWidth="60.0" prefWidth="75.0" sortable="false" text="Type" />
            <TableColumn fx:id="cargoTableSizeCol" maxWidth="118.0" minWidth="60.0" prefWidth="67.0" sortable="false" text="Size" />
         </columns>
      </TableView>
      <Label layoutX="10.0" layoutY="11.0" text="Cargo">
         <font>
            <Font name="System Bold" size="12.0" />
         </font>
      </Label>
      <ChoiceBox fx:id="externalChoiceBox" layoutX="9.0" layoutY="162.0" prefHeight="25.0" prefWidth="99.0" />
      <TextField fx:id="externalTextField" layoutX="123.0" layoutY="162.0" prefHeight="25.0" prefWidth="99.0" />
      <Button fx:id="diagnosticButton" layoutX="83.0" layoutY="210.0" mnemonicParsing="false" text="Diagnostic" textFill="#862828">
         <font>
            <Font name="Arial Black" size="11.0" />
         </font>
      </Button>
      <Label layoutX="10.0" layoutY="143.0" text="Type">
         <font>
            <Font name="System Bold" size="12.0" />
         </font>
      </Label>
      <Label layoutX="123.0" layoutY="144.0" text="Size">
         <font>
            <Font name="System Bold" size="12.0" />
         </font>
      </Label>
   </children>
</AnchorPane>

CargoItem.java:

package so_question01;

import javafx.beans.property.*;

public class CargoItem {

   private final StringProperty type;
   private final FloatProperty size;

   public CargoItem() {
      this.type = new SimpleStringProperty("");
      this.size = new SimpleFloatProperty(0f);
   }

   public CargoItem(String type, float size) {
      this.type = new SimpleStringProperty(type);
      this.size = new SimpleFloatProperty(size);
   }

   public void setType(String type) {
      this.type.set(type);
   }

   public void setSize(float size) {
      this.size.set(size);
   }

   public String getType() {
      if (!(type.get().equals("") || type.get() == null )) {
         return type.get();
      }
      return "";
   }

   public float getSize() {
      return size.get();
   }

   public StringProperty typeProperty() {
      return type;
   }

   public FloatProperty sizeProperty() {
      return size;
   }

   public void clear() {
      this.type.set("");
      this.size.set(0);
   }

   @Override
   public String toString() {  
      return type.get();     
   }

   public String display() {
      return "\nCARGO ITEM:"
              + "\n\ttype: " + type.get()
              + "\n\tsize: " + size.get();
   }
}

ControllerTest.java:

package so_question01;

import java.text.DecimalFormat;
import javafx.collections.ListChangeListener.*;
import javafx.collections.ObservableList;
import javafx.collections.FXCollections;
import javafx.collections.*;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.beans.value.ObservableValue;
import javafx.util.StringConverter;
import javafx.beans.value.*;

public class ControllerTest {

    ObservableList<CargoItem> cargoList = FXCollections.observableArrayList();
   private TestApp mainApp;   // Reference to the main application.

   @FXML private ChoiceBox externalChoiceBox;
   @FXML private TextField externalTextField;
   @FXML private Button diagnosticButton;
    @FXML   private TableView<CargoItem> cargoTable;
    @FXML   private TableColumn<CargoItem, Float> cargoTableSizeCol;
    @FXML   private TableColumn<CargoItem, String> cargoTableTypeCol;

    public ControllerTest() {  }

    @FXML
    private void initialize() {

      //*****DIAGNOSTIC ONLY*******************//
      diagnosticButton.setOnAction(e->{
         for(CargoItem c: cargoList){
            System.out.print(c.display());
         }
      });
      //does not work when changes made via table editing, but may be useful when new list loaded from file?
      cargoList.addListener(new ListChangeListener< CargoItem>(){
         public void onChanged(Change<? extends CargoItem> c){
             // Do your changes here
            System.out.println(c.getList()); 
         }});

      //**** test choicebox***//
      StringConverter cargoItemConverter= new CargoItemStringConverter();
      externalChoiceBox.setConverter(cargoItemConverter);

      //***************** CARGO TABLE  **************//
      cargoTable.setEditable(true);
      // Initialize the cargo table with the 2 columns.
        cargoTableTypeCol.setCellFactory(TextFieldTableCell.forTableColumn());
        cargoTableTypeCol.setCellValueFactory(new PropertyValueFactory<>("type"));

        cargoTableSizeCol.setCellFactory(TextFieldTableCell.forTableColumn(new FloatIntegerStringConverter()));
        cargoTableSizeCol.setCellValueFactory(new PropertyValueFactory<>("size"));
    }

    /**
     * Is called by the main application to give a reference back to itself.
     *
     * @param mainApp
     */
    public void setMainApp(TestApp mainApp) {
        this.mainApp = mainApp;   
      cargoList.setAll(mainApp.getCargoData());

        // Add observable list data to the table
        cargoTable.setItems(cargoList);
        cargoTable.getSelectionModel().setCellSelectionEnabled(true);

      externalChoiceBox.setItems(cargoList); 
      //adds listener to choicebox selection, so size is rendered to text field
      externalChoiceBox.getSelectionModel().selectedItemProperty().addListener(
         new ChangeListener<CargoItem>() {
             @Override public void changed(ObservableValue<? extends CargoItem> observableValue, CargoItem oldChoice, CargoItem newChoice) {
                externalTextField.setText(new FloatIntegerStringConverter().toString(newChoice.getSize()));
            }
         });
    }
}

//*******string converter classes, nothing to see here************//
class CargoItemStringConverter extends StringConverter<CargoItem>{
   @Override
   public String toString(CargoItem c){
      return c.toString();         
   }
   @Override
   public CargoItem fromString(String type){
      return null;
   }
}

class FloatIntegerStringConverter extends StringConverter<Float> 
{
    DecimalFormat decimalFormat = new DecimalFormat("##,###,##0");
    private Float returnVal;

   @Override
   public Float fromString(String value) {       
      if (value == null) {
         return null;
      }
      value = value.trim();
      String digits = value.replaceAll("[^0-9.]", ""); //gets rid of non-numerics

      if (digits.length() < 1) {
         return null;
      }
      try{  //avoids exception by converting invalid strings to zero (which should just be redundant due to regexp above)
      returnVal=  Float.valueOf(Math.round(Float.valueOf(digits)));//have to do this so the float value coresponds to the rounded value in table
      } catch(NumberFormatException e) { returnVal= Float.valueOf(0); }
      return  returnVal;             
    }

    @Override 
    public String toString(Float value) {
      if (value == null) {  // If the specified value is null, return a zero-length String
         return "";
      }
      return decimalFormat.format(value)+" MT"; //truncated integer representation, unit metric tonnes
   }
}

TestApp.java:

package so_question01;

import java.io.*;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.*;
import javafx.collections.ObservableList;
import javafx.collections.FXCollections;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class TestApp extends Application {

    private Stage primaryStage;
   private  ObservableList<CargoItem> cargoData = FXCollections.observableArrayList(); 

   public TestApp() {
      // create some sample data
        CargoItem cargoInstance0 = new CargoItem("Toxic waste", 5000f);
        CargoItem cargoInstance1 = new CargoItem("Cement", 10000f);
        CargoItem cargoInstance2 = new CargoItem("Wheat", 20000f);
        cargoData.add(cargoInstance0);
        cargoData.add(cargoInstance1);
        cargoData.add(cargoInstance2);
   }

    /**
     * Returns the data as observable lists of objects. 
     * @return
     */
    public ObservableList<CargoItem> getCargoData() {return cargoData; }   //called in the controller

   @Override
   public void start(Stage primaryStage) { 
        this.primaryStage = primaryStage;
      this.primaryStage.setTitle("**** TEST ****");   
      show();
    }

      /**
     * Initializes the root layout.
     */
    public void show() {
        try {
            // Load root layout from fxml file.
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(TestApp.class.getResource("Table_test02.fxml"));
            AnchorPane root = (AnchorPane) loader.load();

            // Give the controller access to the main app.
            ControllerTest controller = loader.getController();
            controller.setMainApp(this);

            // Show the scene containing the root layout.
            Scene scene = new Scene(root);
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (IOException e) { System.out.println(e);}
    } 

   /**
     * Returns the main stage.
     * @return
     */
    public Stage getPrimaryStage() {
        return primaryStage;
    }

    public static void main(String[] args) { 
        Application.launch(TestApp.class, (java.lang.String[])null);
    }   
}

回答1:

Assuming I understand the question correctly, you need to initialize your cargoList with an extractor:

ObservableList<CargoItem> cargoList = FXCollections.observableArrayList(cargo -> 
    new Observable[]{cargo.typeProperty(), cargo.sizeProperty()});

ChoiceBox has a history of not working well with update notifications to its underlying list; I'm not sure if everything is fixed in the latest version. You might want to consider switching to a ComboBox instead.

Update

If you need to use a ChoiceBox, a workaround might be not to set the items directly to cargoList, but to update the items whenever cargoList changes. You can do this by making the following change in setMainApp(...):

//  externalChoiceBox.setItems(cargoList); 
    externalChoiceBox.getItems().setAll(cargoList);

and adding the following in your list change listener:

  cargoList.addListener(new ListChangeListener< CargoItem>(){
     public void onChanged(Change<? extends CargoItem> c){
         // Do your changes here
        System.out.println(c.getList()); 
        externalChoiceBox.getItems().setAll(cargoList);
     }});