lundi 18 juin 2012

JavaFX 2.x custom component compatible with FXML


In November, last year, I wrote a little example on how to extend an existing component in JavaFx 2.0.
Today, I would use this component in FXML and make a little demo.
The application for the demo is very simple:
  • One SearchTextBox component (custom component) with events on OnCrossButtonMouseClicked and OnSearchEvent.
  • One ComboBox to change the font size of the SearchTextBox component.

But before making this simple application, I have to:
  • Add a method to change font of the SearchTextBox component (and thus its font size)
  • Fix some bugs
  • Add the FXML compatibility to the SearchTextBox component.
Add the font change and fix bugs
First, I began by adding the methods to change the font of the SearchTextBox component.

    public Font getFont(){
        return searchTextBoxSkin.fontProperty.getValue();
    }
    public void setFont(Font font){
        searchTextBoxSkin.fontProperty.setValue(font);
    }

Then, I have fixed some bugs...
 (For example: in the skin class (SearchTextBoxSkin) of the component to correctly render the cross button with any font size
or in the implementation of the method handle(KeyEvent t)in the SearchTextBox class)

Add the FXML compatibility to the SearchTextBox component
Normally to use the SearchTextBox component it in FXML, you should write something like this:

<SearchTextBox fx:id="searchTextBox1"                             crossButtonMouseClicked="#handleSearchTextBoxCrossButtonOnMouseClicked"
      searchEvent="#handleSearchTextBoxSearchEvent">   
</SearchTextBox> 

But, it isn't working because:
1.      An FXML's attribute must have a getter and a setter corresponding to it into the component (my 2 attributes, crossButtonMouseClicked and searchEvent, don't have them)
2.      The name of the FXML's attribute must begin by on to receive an EventHandler as parameter for the setter corresponding to it in the component. If the attribute doesn't begin by on, the setter will receive a String which contains the value of the attribute.

So, I added the getters and setters in SearchTextBox

    public void setOnCrossButtonMouseClicked (EventHandler eventHandler){
       crossButtonEventHandler = eventHandler;
    }
    public EventHandler getOnCrossButtonMouseClicked (){
        return crossButtonEventHandler;
    }
    public void setOnSearchEvent(EventHandler eventHandler){
       searchEventHandler = eventHandler;
    }
    public EventHandler getOnSearchEvent(){
        return searchEventHandler;
    } 
And, I have deprecated the methods crossButtonOnMouseClicked and searchEvent.

Now, to use the SearchTextBox component in FXML, you have to write something like this:

<SearchTextBox fx:id="searchTextBox1"                           onCrossButtonMouseClicked="#handleSearchTextBoxCrossButtonOnMouseClicked"
      onSearchEvent="#handleSearchTextBoxSearchEvent">   
</SearchTextBox> 

Note: The complete source code of the SearchTextBox component is available at the end of this post.

Make the application
Now, with a custom component working in FXML, write the demo is very simple.
The application consists of:
  • the FXML to describe the UI 
  • the controller of the FXML, which contains the code to interact with the UI.
  • and the JavaFX application to load the FXML and use it.
The FXML

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

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.collections.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<?import com.paddyweblog.control.*?>


<VBox id="vBox1"  
          prefWidth="300.0"
          prefHeight="200.0" 
          spacing="10.0"
          xmlns:fx="http://javafx.com/fxml" 
          fx:controller="fxmlusercomponent.Screen1">
    <children>
        <HBox id="hBox1" alignment="CENTER" spacing="10.0" >
            <children>
                <Label id="label1" contentDisplay="CENTER" text="Size font Text Search Text Box" />
                <ComboBox id="comboBox1" fx:id="comboBox1" onAction="#handleComboBoxAction" >
                    <items>
                        <FXCollections fx:factory="observableArrayList">
                            <String fx:value="10" />
                            <String fx:value="13" />
                            <String fx:value="15" />
                            <String fx:value="20" />
                            <String fx:value="25" />
                            <String fx:value="30" />
                            <String fx:value="35" />
                            <String fx:value="40" />
                            <String fx:value="45" />
                        </FXCollections>
                    </items>
                </ComboBox>
            </children>
        </HBox>
    </children>
    <children>
        <SearchTextBox fx:id="searchTextBox1" 
                       onCrossButtonMouseClicked="#handleSearchTextBoxCrossButtonOnMouseClicked" 
                       onSearchEvent="#handleSearchTextBoxSearchEvent">    
        </SearchTextBox>
    </children>
</VBox>
The controller

Screen1.java
package fxmlusercomponent;

import com.paddyweblog.control.SearchTextBox;
import com.paddyweblog.control.SearchTextBoxEvent;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.input.MouseEvent;
import javafx.scene.text.Font;


public class Screen1 implements Initializable {
    
    @FXML
    private ComboBox comboBox1;
    
    @FXML
    private SearchTextBox searchTextBox1;
        
    @FXML
    private void handleComboBoxAction(ActionEvent event) {
        
        Font f = searchTextBox1.getFont();
        String name = f.getName();
        
        double sizeValue =  Double.parseDouble((String)comboBox1.getValue());
        Font fnew = new Font(name,sizeValue);
        searchTextBox1.setFont(fnew);
        
    }
     
    @FXML
    private void handleSearchTextBoxCrossButtonOnMouseClicked(MouseEvent mouseEvent){
        System.out.println("handleSearchTextBoxCrossButtonOnMouseClicked");
    }
    
    @FXML
    private void handleSearchTextBoxSearchEvent(SearchTextBoxEvent searchTextBoxEvent){
        System.out.println("handleSearchTextBoxSearchEvent"+searchTextBoxEvent.getText());
        
        if ("aaaa".equals(searchTextBoxEvent.getText())){
            System.out.println("Good !");
        }
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        Font f = searchTextBox1.getFont();       
        comboBox1.setValue(String.valueOf((int)f.getSize()));
    }   
}

The JavaFX application

FxmlUserComponent.java
package fxmlusercomponent;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class FxmlUserComponent extends Application {
    
    public static void main(String[] args) {
        Application.launch(FxmlUserComponent.class, args);
    }
    
    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("Screen1.fxml"));
        
        stage.setScene(new Scene(root));
        stage.show();
    }
}

Source code of the SearchTextBox component

The component

SearchTextBox.java
package com.paddyweblog.control;

import javafx.event.EventHandler;
import javafx.scene.control.TextField;
import javafx.scene.input.*;
import javafx.scene.text.Font;

public class SearchTextBox extends TextField{
    
    SearchTextBoxSkin searchTextBoxSkin = new SearchTextBoxSkin(this);
    
    EventHandler searchEventHandler = null;
    
    EventHandler crossButtonEventHandler = null;
    
    SearchTextBox searchTextBox = this;
        
    public SearchTextBox(){
        super();
        this.setSkin(searchTextBoxSkin); 
        
        this.setOnKeyReleased(new EventHandler<KeyEvent>(){        
            
            final KeyCombination combo = new KeyCodeCombination(KeyCode.TRACK_NEXT);

            @Override
            public void handle(KeyEvent t) {
                
                    t.consume();
        
                    //SearchTextBoxEvent e = new SearchTextBoxEvent(searchTextBox.getText()+t.getCharacter());
                    SearchTextBoxEvent e = new SearchTextBoxEvent(searchTextBox.getText());
    
                    if (searchEventHandler!=null){
                        searchEventHandler.handle(e);
                    }
            }
            
        });     
        
        searchTextBoxSkin.crossButton.setOnMouseClicked(new EventHandler<MouseEvent>() {

            @Override
            public void handle(MouseEvent t) {
                    searchTextBox.setText("");
                    if (crossButtonEventHandler!=null){
                        crossButtonEventHandler.handle(t);   
                    }
            }
            
        });
    }
    
    public void setOnCrossButtonMouseClicked (EventHandler eventHandler){
       crossButtonEventHandler = eventHandler;
    }
    
    public EventHandler getOnCrossButtonMouseClicked (){
        return crossButtonEventHandler;
    }
    
    public void setOnSearchEvent(EventHandler eventHandler){
       searchEventHandler = eventHandler;
    }
    
    public EventHandler getOnSearchEvent(){
        return searchEventHandler;
    }  
    
    @Deprecated
    public void crossButtonOnMouseClicked(EventHandler eventHandler){
        crossButtonEventHandler = eventHandler;
    }
    
    @Deprecated
    public void searchEvent(EventHandler eventHandler){
        searchEventHandler = eventHandler;
    }   
    
    public Font getFont(){
        return searchTextBoxSkin.fontProperty.getValue();
    }
    
    public void setFont(Font font){
        searchTextBoxSkin.fontProperty.setValue(font);
    }
}

The component skin

SearchTextBoxSkin.java
package com.paddyweblog.control;

import com.sun.javafx.scene.control.skin.TextFieldSkin;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;


public class SearchTextBoxSkin extends TextFieldSkin{
    
    SearchTextBox searchTextBox;
    
    Group crossButton = new Group();
    
    ObjectProperty<Insets> paddingProperty = this.paddingProperty();
    ObjectProperty<Font> fontProperty = this.font;
    
    public SearchTextBoxSkin(final SearchTextBox searchTextBox){
     
        super(searchTextBox);
        this.searchTextBox = searchTextBox;
        
        crossButton.setFocusTraversable(false);
               
        DoubleBinding crossWidth = searchTextBox.heightProperty().multiply(0.45);
        DoubleBinding crossHeight = searchTextBox.heightProperty().multiply(0.05);
                
        Circle circle = new Circle();
        circle.setFill(Color.GRAY);       
        circle.radiusProperty().bind(searchTextBox.heightProperty().multiply(0.325));
        circle.setFocusTraversable(false);
        
        Rectangle rect1 = new Rectangle();
        rect1.widthProperty().bind(crossWidth);
        rect1.heightProperty().bind(crossHeight);
        rect1.setFill(Color.WHITE);
        rect1.setRotate(45);
        rect1.translateXProperty().bind(crossWidth.divide(2).negate());
        rect1.translateYProperty().bind(crossHeight.divide(2).negate());
        rect1.setFocusTraversable(false);
        
        Rectangle rect2 = new Rectangle();
        rect2.widthProperty().bind(crossWidth);
        rect2.heightProperty().bind(crossHeight);
        rect2.setFill(Color.WHITE);
        rect2.setRotate(-45);
        rect2.translateXProperty().bind(crossWidth.divide(2).negate());
        rect2.translateYProperty().bind(crossHeight.divide(2).negate());
        rect2.setFocusTraversable(false);
        
        crossButton.getChildren().add(circle);
        crossButton.getChildren().add(rect1);
        crossButton.getChildren().add(rect2);
        
        crossButton.translateXProperty().bind(searchTextBox.widthProperty().subtract(searchTextBox.heightProperty()));        
        
        crossButton.visibleProperty().bind(searchTextBox.textProperty().greaterThan(""));
                        
        getChildren().add(crossButton);
        SearchTextBoxSkin.setAlignment(crossButton,Pos.CENTER_LEFT);
        
        searchTextBox.heightProperty().addListener(new ChangeListener(){

            public void changed(ObservableValue ov, Object t, Object t1) {
                Double d = (Double)t1;                         
                paddingProperty.set(new Insets(3,d.intValue(),3,3));
            }
            
        } );        
    }
}


The custom event

SearchTextBoxEvent.java
package com.paddyweblog.control;

import javafx.event.Event;
import javafx.event.EventType;

public class SearchTextBoxEvent extends Event {
    
    public static final EventType<SearchTextBoxEvent> SEARCHTEXTBOXEVENT = new EventType(Event.ANY, "SEARCHTEXTBOXEVENT");
    
    public SearchTextBoxEvent(String text){
        super(SEARCHTEXTBOXEVENT);
        this.text = text;
    }
    
    private String text;

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }       
}

NetBeans project

2 commentaires:

raju dial a dit…

good post and with useful information
But i like the assertions in java

Marcin Kołodziej a dit…

Hi
Do you have something against using fragments of your code in commercial project? I couldn't find any note about license on your blog.