jeudi 17 novembre 2011

Extend an existing UI component in JavaFX 2.0


Last year, I coded a custom component on JavaFX 1.x, then naturally I wished to rewrite it on JavaFX 2.0.
To do this, i tried different solutions,  like creating to a totally new component with this method
But, it isn't a great solution when you want to customize or to enhance an existing component, like I want to do.
To rewrite my custom component, I chose to extend an existing control (TextField in my case) and its skin, because you cannot add content directly in the control but you can do that in its skin.



Description of the implementation
My control's class ( SearchTextBox ) extends the TextField's control and implements my SearchTextBox events (crossButtonOnMouseClicked & searchEvent).
And my skin's class ( SearchTextBoxSkin ) extends TextFieldSkin to add the reset button in the original TextField.
I also created a new EventType ( SearchTextBoxEvent ) to manage the searchEvent.
The component code

SearchTextBox.java
package com.paddyweblog.control;

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

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());
    
                    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 crossButtonOnMouseClicked(EventHandler eventHandler){
        crossButtonEventHandler = eventHandler;
    }
    
    public void searchEvent(EventHandler eventHandler){
        searchEventHandler = eventHandler;
    }   
}

SearchTextBoxSkin.java
package com.paddyweblog.control;

import com.sun.javafx.scene.control.skin.TextFieldSkin;
import javafx.beans.binding.DoubleBinding;
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;


public class SearchTextBoxSkin extends TextFieldSkin{
    
    SearchTextBox searchTextBox;
    
    Group crossButton = new Group();
    
    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);
        this.setPadding(new Insets(3,17,3,3));
    }
}

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;
    }   
    
}

The Application code, to test the SearchTextBox component

SearchTextBoxControl.java
package searchtextboxcontrol;

import com.paddyweblog.control.SearchTextBox;
import com.paddyweblog.control.SearchTextBoxEvent;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

public class SearchTextBoxControl extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }
    
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Test SearchTextBox control");
        Group root = new Group();
        Scene scene = new Scene(root, 300, 250);
        Button btn = new Button();
        btn.setLayoutX(10);
        btn.setLayoutY(100);
        btn.setText("just a button to change focus");
        btn.setOnAction(new EventHandler<ActionEvent>() {

            public void handle(ActionEvent event) {
                System.out.println("just a button to change focus");
            }
        });
        root.getChildren().add(btn);
        
        SearchTextBox stb = new SearchTextBox();

        stb.setLayoutX(10);
        stb.setLayoutY(50);
        stb.setPrefWidth(150);
        stb.crossButtonOnMouseClicked(new EventHandler<MouseEvent>(){

            @Override
            public void handle(MouseEvent t) {
               System.out.println("CrossButtonOnMouseClicked");
            }
            
        });
        
        stb.searchEvent(new EventHandler<SearchTextBoxEvent>(){
    
            public void handle(SearchTextBoxEvent t) {
                
                System.out.println("searchEventHandler : "+t.getText());
            }
            
        });                
        
        root.getChildren().add(stb);         
        
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

Note: You probably should to change the Java Platform from the projet properties>librairies (to choose your JavaFX 2.0 Platform) to run the project in NetBeans

5 commentaires:

Highlander a dit…

Hi,
I have tried example, and it is not working for me. Have you tried with new version of javaFX 2.0.1.

Patrick Champion a dit…

Yes, it works on JavaFX 2.0.1 and 2.0 on Windows and also on Mac OS X.
extend-ui-control-javafx2.0.1.jpg
Which issue do you have ?

Highlander a dit…

Sorry for late response. I am getting this error:

java.lang.NullPointerException
at com.sun.javafx.scene.control.skin.TextInputControlSkin.setCaretAnimating(TextInputControlSkin.java:353)
at com.sun.javafx.scene.control.behavior.TextFieldBehavior.setCaretAnimating(TextFieldBehavior.java:138)
at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callAction(TextInputControlBehavior.java:113)
at com.sun.javafx.scene.control.behavior.BehaviorBase.callActionForEvent(BehaviorBase.java:165)
at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callActionForEvent(TextInputControlBehavior.java:107)
at com.sun.javafx.scene.control.behavior.BehaviorBase$1.handle(BehaviorBase.java:127)
at com.sun.javafx.scene.control.behavior.BehaviorBase$1.handle(BehaviorBase.java:125)
at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:56)
at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:162)
at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:115)
at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:38)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:37)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:92)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:35)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:92)
at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:47)
at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:33)
at javafx.event.Event.fireEvent(Event.java:171)
at javafx.scene.Scene$KeyHandler.process(Scene.java:2938)
at javafx.scene.Scene$KeyHandler.access$1700(Scene.java:2868)
at javafx.scene.Scene.impl_processKeyEvent(Scene.java:1431)
at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:1862)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:107)
at com.sun.glass.ui.View.handleKeyEvent(View.java:280)
at com.sun.glass.ui.View.notifyKey(View.java:577)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.access$100(WinApplication.java:29)
at com.sun.glass.ui.win.WinApplication$2$1.run(WinApplication.java:62)
at java.lang.Thread.run(Thread.java:722)

Highlander a dit…

After investigating, I realized that css of old javafx, that I was using in my application, made this problem.

Patrick Champion a dit…

to extend an existing JavaFX's UI component while maintaining compatibility with FXML, read http://paddyweblog.blogspot.fr/2012/06/custom-component-compatible-with-fxml.html