0

(This is similar to a homework question.)

I recently made an example UI in Scenebuilder for something I later had to program with Java Swing. That more or less worked. Now it is my task, not for the actual development of the program, but for learning something in my job training, to make a similar UI with Scenebuilder, but this time an actually working one. The specifications are:

  • It's a window with a table in it.
  • At least two of the columns have radio boxes in them that look like checkboxes or checkboxes that act like radio boxes (because the company has weird standards).
  • It uses FXML files made with Scenebuilder for the layout.

Making the check boxes act as radio boxes should be easy, if I could just enable the editing. I found a lot of examples that do almost what I want, but are still all not really applicable to my situation. Here are some of them:

  • I started with this video and almost exactly copied the code to first have a working example. Then I adjusted it to my needs until I only had the check boxes to do (first working prototype had Booleans instead).
  • Then I took part of the full code of this answer to add the check boxes. That worked, but they don't react to clicks.
  • This, this and this seems to only apply to text fields, not checkboxes.
  • I then used the two lambda expressions from the second code block in this answer (actually I used the variant in the first answer and manually resolved some errors until suddenly Eclipse automatically converted it to lambda expressions). I also added the public ObservableValue<Boolean> getCompleted() method, Eclipse suggested some magic and then I had what you can see in the corresponding code below (without the console print).
  • I also added a listener to the boolean property, like this site (archive) apparently does (I think), but that doesn't seem like it helped much.

Here is a picture of how the dialog looks now, I still can't use the check boxes: enter image description here

My question: How can I make it so that the check boxes react to clicks? React can mean outputting something on the console, I don't need a given code that makes it automatically disable the other checkbox, I want to figure that part out myself.

My code:

src.controller.MainController.java

package controller;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import view.Table;
public class MainController implements Initializable{
 @FXML TableView<Table> tableID;
 @FXML TableColumn<Table,String> iFirstName;
 @FXML TableColumn<Table,String> iLastName;
 @FXML TableColumn<Table,Boolean> iMalebox;
 @FXML TableColumn<Table,Boolean> iFemalebox;
 @Override public void initialize(URL location,ResourceBundle resources){
  iFirstName.setCellValueFactory(new PropertyValueFactory<Table,String>("rFirstName"));
  iLastName.setCellValueFactory(new PropertyValueFactory<Table,String>("rLastName"));
  iMalebox.setCellValueFactory(p->p.getValue().getCompleted());
  iMalebox.setCellFactory(p->new CheckBoxTableCell<>());
  iMalebox.setEditable(true);
  // iMalebox.setCellValueFactory(p->p.getValue().getCompleted());
  // iMalebox.setCellFactory(p->new CheckBoxTableCell<>());
  iFemalebox.setCellValueFactory(p->p.getValue().getCompleted());
  iFemalebox.setCellFactory(p->new CheckBoxTableCell<>());
  // iMalebox.setCellValueFactory(new PropertyValueFactory<Table,Boolean>("rMalebox"));
  // iFemalebox.setCellValueFactory(new PropertyValueFactory<Table,Boolean>("rFemalebox"));
  tableID.setItems(FXCollections.observableArrayList(new Table("Horst","Meier",true,false),new Table("Anna","Becker",false,true),new Table("Karl","Schmidt",true,false)));
  tableID.setEditable(true);
 }
}

src.controller.MainView.java

package controller;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class MainView extends Application{
 @Override public void start(Stage primaryStage){
  try{
   // FXMLLoader.load(MainView.class.getResource("MainController.fxml"));
   AnchorPane page=(AnchorPane)FXMLLoader.load(MainView.class.getResource("MainController.fxml"));
   Scene scene=new Scene(page);
   primaryStage.setScene(scene);
   primaryStage.setTitle("Window Title");
   primaryStage.show();
  }catch(Exception e){
   Logger.getLogger(MainView.class.getName()).log(Level.SEVERE,null,e);
  }
 }
 public static void main(String[] args){
  Application.launch(MainView.class,(java.lang.String[])null);
 }
}

src.controller.MainController.fxml

<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="controller.MainController">
 <children>
<TableView fx:id="tableID" prefHeight="494.0" prefWidth="798.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columns>
<TableColumn fx:id="iFirstName" prefWidth="75.0" text="First name" />
<TableColumn fx:id="iLastName" prefWidth="75.0" text="Last name" />
            <TableColumn fx:id="iMalebox" prefWidth="75.0" text="Male" />
            <TableColumn fx:id="iFemalebox" prefWidth="75.0" text="Female" />
</columns>
</TableView>
 </children>
</AnchorPane>

src.view.Table.java

package view;
import javafx.beans.InvalidationListener;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
public class Table{
 private SimpleStringProperty rFirstName;
 private SimpleStringProperty rLastName;
 private SimpleBooleanProperty rMalebox;
 private SimpleBooleanProperty rFemalebox;
 public Table(String sFirstName,String sLastName,Boolean sMalebox,Boolean sFemalebox){
  rFirstName=new SimpleStringProperty(sFirstName);
  rLastName=new SimpleStringProperty(sLastName);
  rMalebox=new SimpleBooleanProperty(sMalebox);
  rMalebox.addListener((ChangeListener)(observable,oldValue,newValue)->{
   System.out.println("test");
   System.out.println("abc");
  });
  rFemalebox=new SimpleBooleanProperty(sFemalebox);
 }
 public String getRFirstName(){
  return rFirstName.get();
 }
 public void setRFirstName(String v){
  rFirstName.set(v);
 }
 public String getRLastName(){
  return rLastName.get();
 }
 public void setRLastName(String v){
  rLastName.set(v);
 }
 public Boolean getRMalebox(){
  return rMalebox.get();
 }
 public void setRMalebox(Boolean v){
  rMalebox.set(v);
 }
 public Boolean getRFemalebox(){
  return rFemalebox.get();
 }
 public void setRFemalebox(Boolean v){
  rFemalebox.set(v);
 }
 public ObservableValue<Boolean> getCompleted(){
  return new ObservableValue<Boolean>(){
   @Override public void removeListener(InvalidationListener arg0){}
   @Override public void addListener(InvalidationListener arg0){}
   @Override public void removeListener(ChangeListener<? super Boolean> listener){}
   @Override public Boolean getValue(){
    return null;
   }
   @Override public void addListener(ChangeListener<? super Boolean> listener){
    System.out.println("Test");
   }
  };
 }
}
Fabian Röling
  • 1,227
  • 1
  • 10
  • 28
  • You need to implement the property accessor methods (i.e. `public BooleanProperty rMaleBoxProperty();`, etc) in your model class (`Table`). Not sure if that is the only problem, but your cells won't be able to bind to the properties correctly without those methods. Then set the cell value factories to point to the properties returned from those methods. (I have no real idea what the property you return from `getCompleted()` is supposed to be doing; it just seems to return a property that does nothing.) – James_D Oct 25 '17 at 15:55
  • Firstly: Changing `SimpleBooleanProperty` to `BooleanProperty` made them clickable, yay! Who knew that it would be such a simple fix to remove the simple stuff... ;) But I still don't get a console output when I click on it. – Fabian Röling Oct 26 '17 at 09:12
  • How and where should I refer to the addListener method? I can trigger it, but what do I do with that listener then? My new code is now, directly before adding the row to the table (second to last code line in `MainController.java`): `Table horst=new Table("Horst","Meier",true,false); horst.getCompleted().addListener(new ChangeListener(){ @Override public void changed(ObservableValue extends Boolean> observable,Boolean oldValue,Boolean newValue){} });` That actually triggers the `addListener` method when I start the program (meaning that I get the console output `Test`), – Fabian Röling Oct 26 '17 at 11:03
  • I still have no idea what `getCompleted()` is for. What is it that you are representing as "complete"? Why don't you try the approach I outlined? Did you create the property accessor methods for male and female? Just add the listeners to those properties. – James_D Oct 26 '17 at 11:05
  • but I probably have to do something with either the column or the single cell, right? I need a change listener for the checkbox. I have this `rMalebox.addListener((ChangeListener)(observable,oldValue,newValue)->{System.out.println("test");});` that I now expanded to `rMalebox.addListener(new ChangeListener(){ @Override public void changed(ObservableValue extends Boolean> observable,Boolean oldValue,Boolean newValue){System.out.println("test");}});` to fix a warning, but it still doesn't get triggered. – Fabian Röling Oct 26 '17 at 11:06
  • `getCompleted` was generated by Eclipse by an automatic fix for an error that prevented compilation. I don't even really understand that part myself, because this is slowly but surely leaving my area of knowledge. That's why I asked this question. If `getCompleted` is not needed, then fine, but the code won't compile if I just delete it. – Fabian Röling Oct 26 '17 at 11:08
  • Actually, I just tried it, I deleted the entire method and everything in it, `call` in `setCellValueFactory` of `iMalebox` now just returns `null` (because I don't know what else it should return) and I removed the `getCompleted().addListener([...])` calls from the `Table`s. Now it does compile and the checkboxes even react to clicks, but they still don't do anything useful. So where do I put my code to run whenever the user clicks on the checkbox? – Fabian Röling Oct 26 '17 at 11:12
  • Update: I finally managed to get a console output when I click on the check box. And this console output has the new value (true/false) in it. This answer helped: https://stackoverflow.com/a/26866083/6743127 Now I'll try to understand it all, tidy up the mess that the code became in the meantime, see what was actually necessary and what code does nothing useful and do misc. stuff, then I'll post my own answer. If someone in the meantime wants to post an answer with actual knowledge behind it, go ahead. – Fabian Röling Oct 27 '17 at 13:41

1 Answers1

2

I found a solution. Since I changed so much (I worked on getting this stupid thing to work for almost 20 hours after asking that question), it's not really useful to enumerate all the changes I did. But here is at least a working example.
There are very few lines responsible for making the "male" and "female" boxes trigger each other's opposites (which is specific to my task), everything else is just for getting actually properly working CheckBoxTableCells. One would think that that is a really common case and there should be standard methods for that, like for "print text into the console" or "read file", but apparently not, apparently everything has to be complicated in programming with UIs.

Anyway, rant aside, here is the working code:

src.controller.MainView.java

package controller;
import java.io.*;
import javafx.application.*;
import javafx.fxml.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.stage.*;
public class MainView extends Application{
 @Override public void start(Stage primaryStage) throws IOException{
  primaryStage.setScene(new Scene((AnchorPane)FXMLLoader.load(MainView.class.getResource("MainController.fxml"))));
  primaryStage.show();
 }
 public static void main(String[] args){
  Application.launch(MainView.class);
 }
}

src.controller.MainController.java

package controller;
import java.net.*;
import java.util.*;
import javafx.beans.value.*;
import javafx.collections.*;
import javafx.fxml.*;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.*;
import javafx.scene.control.cell.*;
import javafx.util.*;
import view.*;
public class MainController implements Initializable{
 @FXML TableView<Table> tableID;
 @FXML TableColumn<Table,String> iFirstName;
 @FXML TableColumn<Table,String> iLastName;
 @FXML TableColumn<Table,Boolean> iMalebox;
 @FXML TableColumn<Table,Boolean> iFemalebox;
 @Override public void initialize(URL location,ResourceBundle resources){
  iFirstName.setCellValueFactory(new PropertyValueFactory<Table,String>("rFirstName"));
  iLastName.setCellValueFactory(new PropertyValueFactory<Table,String>("rLastName"));
  iMalebox.setCellValueFactory(new Callback<CellDataFeatures<Table,Boolean>,ObservableValue<Boolean>>(){
   @Override public ObservableValue<Boolean> call(CellDataFeatures<Table,Boolean> cellData){
    return cellData.getValue().maleCheckedProperty(true);
   }
  });
  iMalebox.setCellFactory(new Callback<TableColumn<Table,Boolean>,TableCell<Table,Boolean>>(){
   @Override public TableCell<Table,Boolean> call(TableColumn<Table,Boolean> param){
    return new CheckBoxTableCell<>();
   }
  });
  iFemalebox.setCellValueFactory(new Callback<CellDataFeatures<Table,Boolean>,ObservableValue<Boolean>>(){
   @Override public ObservableValue<Boolean> call(CellDataFeatures<Table,Boolean> cellData){
    return cellData.getValue().femaleCheckedProperty(true);
   }
  });
  iFemalebox.setCellFactory(new Callback<TableColumn<Table,Boolean>,TableCell<Table,Boolean>>(){
   @Override public TableCell<Table,Boolean> call(TableColumn<Table,Boolean> param){
    return new CheckBoxTableCell<>();
   }
  });
  tableID.setItems(FXCollections.observableArrayList(new Table("Horst","Meier",true),new Table("Anna","Becker",false),new Table("Karl","Schmidt",true)));
 }
}

src.view.Table.java

package view;
import javafx.beans.property.*;
public class Table{
 private String rFirstName;
 private String rLastName;
 public Table(String sFirstName,String sLastName,Boolean sMale){
  rFirstName=sFirstName;
  rLastName=sLastName;
  maleCheckedProperty(false).set(sMale);
 }
 private SimpleBooleanProperty maleChecked=new SimpleBooleanProperty(false);
 private SimpleBooleanProperty femaleChecked=new SimpleBooleanProperty(false);
 public SimpleBooleanProperty maleCheckedProperty(boolean recursion){
  if(recursion) femaleCheckedProperty(false).set(!maleChecked.get());
  return maleChecked;
 }
 public SimpleBooleanProperty femaleCheckedProperty(boolean recursion){
  if(recursion) maleCheckedProperty(false).set(!femaleChecked.get());
  return femaleChecked;
 }
 public String getRFirstName(){
  return rFirstName;
 }
 public String getRLastName(){
  return rLastName;
 }
}

src.controller.MainController.fxml

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="98.0" prefWidth="218.0" xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="controller.MainController">
 <children>
 <TableView fx:id="tableID" editable="true" prefHeight="494.0" prefWidth="798.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
  <columns>
   <TableColumn fx:id="iFirstName" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="63.0" text="First name" />
   <TableColumn fx:id="iLastName" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="63.0" text="Last name" />
   <TableColumn fx:id="iMalebox" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="45.0" text="Male"/>
   <TableColumn fx:id="iFemalebox" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="45.0" text="Female"/>
   </columns>
  </TableView>
 </children>
</AnchorPane>
Fabian Röling
  • 1,227
  • 1
  • 10
  • 28
  • No, past Me, only JavaFX has to be so complicated. In Java Swing, this would have been one file instead of four and maybe even shorter than the shortest ones here. – Fabian Röling Jul 04 '22 at 16:10