söndag 14 april 2013

Project "FXComparer" part 3 - Handling input and showing results using a TableView

In this third part of the "FXComparer" series we will focus on how to connect the FXML with a controller, and how to show results in a TableView.

Connecting a controller to the FXML
When creating a UI using FXML interactions from the users are performed either by having inline scripts  in the FXML (like to old JavaFX) or using a controller. We will focus on the controller alternative.

A controller is a simple Java class. Before we needed to implement Initializable if we liked to do anything when the controller was created, but today this can be omitted and instead just use a no-arg initialize() method.

Nodes from the scene are "injected" to the controller using the @FXML annotation. The nodes are looked up using there id. We can also manually lookup nodes in a scene using the lookup(css selector) method.

Binding actions to nodes is also done in the FXML. Here we specify action by using the method name for the action to perform. The method needs to be defined in the controller with one argument of the type ActionEvent and annotated with the FXML-annotation.

Let us see a example of some FXML and a controller.
<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" 
    minHeight="-Infinity" minWidth="-Infinity" prefHeight="90.0" 
    prefWidth="200.00009999999747" xmlns:fx="http://javafx.com/fxml" 
    fx:controller="com.loop81.blog.fxml.FXMLDemoController">
  <children>
    <Label fx:id="labelHello" alignment="CENTER" contentDisplay="CENTER" 
      layoutX="16.0" layoutY="11.0" prefWidth="170.0" />
    <Label layoutX="14.0" layoutY="30.0" text="Times pressed:" />
    <Label fx:id="labelCounter" alignment="CENTER_RIGHT" 
      contentDisplay="RIGHT" layoutX="100.0" layoutY="30.0" prefWidth="84.0" />
    <Button layoutX="27.0" layoutY="54.0" mnemonicParsing="false" 
      onAction="#onButtonClick" text="Increase counter by one" />
  </children>
</AnchorPane>
public class FXMLDemoController {

 @FXML
 private Label labelHello;
 
 @FXML
 private Label labelCounter;
 
 private IntegerProperty counter = new SimpleIntegerProperty(0);
 
 public void initialize() {
  labelHello.setText("Hello!");
  labelCounter.textProperty().bind(counter.asString());
 }
 
 @FXML
 public void onButtonClick(ActionEvent event) {
  event.consume();
  counter.set(counter.get() + 1);
 }
}
Line 4 (FXML): Defines that FXMLDemoController should be the controller for this FXML.
Line 6 (FXML): Notice that we have given the label a unique id which is later matched against the Label variable name in the controller.
Line 12 (FXML): Here we define which method in the controller that should be called when a user presses the button. The method name needs to start with a '#'. This is easy forgotten and when actions is not working this is probably one of the reasons.
Line 3-7 (controller): Here we define that we like to have a reference to the two labels injected for us. Notice that we need to annotate the label with the @FXML annotation.
Line 9 (controller): Defines a integer property. Which we will use to show a counter for the clicking. If you are unsure about properties and bindings in JavaFX I recommend you to have a look at http://docs.oracle.com/javafx/2/binding/jfxpub-binding.htm.
Line 11-14 (controller): This method will automatically be called when the controller is instantiated. All injections of @FXML annotated nodes will be loaded before this method is called.
Line 12 (controller): Here we simply update the text of the "hello label".
Line 13 (controller): Each Label has a StringProperty and what we do here is that we assign the string value of our counter property to the labels property. Each time we update our counter with a new value the label also will be updated.
Line 16-20 (controller): Last we increase the counter when the button is clicked. It is always a good practice to consume the event if you do not like it to be propagated any further.

You can download the source of the demo here: http://files.loop81.com/rest/download/64925d28-4626-4662-bacd-7f07da55b31f

In the FXComparer application the controller is defined in com.loop81.fxcomparer.FXComparerController and the FXML in fxml/layout.fxml.

Showing results in a TableView
The result from a compare in FXComparer is presented using a TableView. In the FXComparer we have 3 columns which widths is 50%, 25%, 25%. Today there is no way where you can specify the width of the columns in the Scene Builder and needs to done pragmatically. The whole TableView is highly configurable and quite nice to work with. Bellow is a demo showing the most common use cases for the TableView, size of the columns, handle of a row click, and sorting.
private static final List<TableData> DATA = new ArrayList<>(Arrays.asList(
  new TableData("The first text", 99),
  new TableData("The second text", 0),
  new TableData("The third text", -99)));

@FXML
private Label labelSelectedRow;

@FXML
private TableView<TableData> tableView;

@FXML
private TableColumn<TableData, String> column1;

@FXML
private TableColumn<TableData, String> column2;

@FXML
private TableColumn<TableData, TableData> column3;

public void initialize() {
 // Column 1 should be 50% wide and show the text in the TableData.
 column1.prefWidthProperty().bind(tableView.widthProperty().divide(2));
 column1.setCellValueFactory(new PropertyValueFactory<TableData, String>("text"));
 
 // Column 2 should be 25% wide and show the number.
 column2.prefWidthProperty().bind(tableView.widthProperty().divide(4));
 column2.setCellValueFactory(new PropertyValueFactory<TableData, String>("number"));
 
 // Column 3 should be 25% wide and show the text + number, but the sorting should be on 
    // the number.
 column3.prefWidthProperty().bind(tableView.widthProperty().divide(4));
 column3.setCellValueFactory(
   new Callback<TableColumn.CellDataFeatures<TableData,TableData>, 
                            ObservableValue<TableData>>() {
  @Override
  public ObservableValue<TableData> call(CellDataFeatures<TableData, TableData> cell) {
   return new SimpleObjectProperty<>(cell.getValue());
  }
 });
 
 // Last we add the data to the table.
 tableView.setItems(FXCollections.observableList(DATA));
 
 // Add a listener to the selected item property so we can detect a selection change 
    // in the table.
 tableView.getSelectionModel().selectedItemProperty().addListener(
          new ChangeListener<TableData>() {
  @Override
  public void changed(ObservableValue<? extends TableData> value, TableData oldValue, 
                  TableData newValue) {
   labelSelectedRow.setText(tableView.getSelectionModel().getSelectedIndex() + " " 
                     + newValue.toString());
  }
 });
}
Line 1-4: The TableData is a simple object which has two properties, one for a text, and one for a number. The TableData also implement Comparable<TableData> which handles the sorting of the third column.
Line 12-19: We inject the TableColumns so we later on can modify the width of them.
Line 23: To specify the width of a column use the prefWidthProperty(). To this property we bind half the size of the TableView. If the TableView would change its size the column will change as well.
Line 24: The value of a cell is rendered using a CellFactory and a CellValueFactory. For the first two columns it is enough to use the PropertyValueFactory which simply fetch the value from the object in the data model for the table using the property.
Line 27: Column 2 and Column 3 will only have a size of 1/4 of the TableView.
Line 33-40: Since the last columns value is a concatenation of the string and the number we have to use a custom CellValueFactory which returns a new SimpleObjectProperty.
Line 43: Here we assign the items to the table. The TableView requires that we use a observable list since if the data is changed this also should be reflected in the table.
Line 47-55: When a user selects a item in the table the SelectionModel is updated. What we like to know is simply when that property is updated and when it is updated we show the new item together with the row id in the label.

Understanding the properties and bindings in JavaFX is important. I have seen a lot of different solution to the problem of getting the row a user click on. Some solutions added a custom ClickListener using a CellFactory when all you need is to listen for changes in the selection model.

You can download the source of the demo here: http://files.loop81.com/rest/download/6b6bcf5c-c8ed-47a2-9373-b12e0a7f66f2

In the FXComparer application the TableView code is defined in com.loop81.fxcomparer.FXComparerController.

Wrapping up
Using FXML really makes us write less Java-code. In some cases however I guess writing Java code would make sense, but for common parts of the application I do not see any reason for not using FXML. The TableView is a highly customization component and I suggest you to have a look in the official TableView tutorial http://docs.oracle.com/javafx/2/ui_controls/table-view.htm.

In the next post in the series about "FXComparer" we will see how we can use a nice Maven plugin for deployment of the application.

All source code for "FXCompare" is found here: https://code.google.com/p/fx-comparer/.

The articles in this series are the following:


Inga kommentarer:

Skicka en kommentar