JavaFx provides a few ways to change a control’s appearance. You can change the css or replace the FXML. However, there is a problem with adhering to the Model-View-Controller pattern with those approaches. The view and the controller are not well separated. The addition of SkinBase to JavaFx’s public API promises to improve the separation of the view and controller. Extending SkinBase may be the best way to preserve the Model-View-Controller pattern.
To test the usefulness of extending SkinBase I made a simple example of skinnable JavaFx control. I choose to make a login control, which is a commonly needed control (One I happen to need too).
I’m going to walk you through my development process. Following an Agile approach, I starting with making test code. As a core of the test code, I want a control that will takes an initial username and a callback to be run on clicking “sign in.” The below statement defines creating the control:
Login root = new Login(initialUsername, loginCallback);
Login Class
Next, I defined the Login class:
public class Login extends Control
{
private String initialUsername;
private Callback<Pair<String, String>, Void> loginCallback;
public Login(String initialUsername, Callback<Pair<String, String>, Void> loginCallback)
{
this.initialUsername = initialUsername;
this.loginCallback = loginCallback;
}
@Override
public Skin<?> createDefaultSkin() {
return new LoginSkin(
this,
initialUsername,
loginCallback);
}
}
Notice that the only required steps are to create a constructor that assigns the arguments to fields and override the createDefaultSkin method. Now I need to create a class for the default skin, that is used by default.
Default Skin
public class LoginSkin extends SkinBase<Login>
{
public LoginSkin(
Login control,
String initialUsername,
Callback<Pair<String, String>, Void> loginCallback)
{
super(control);
LoginBorderPane borderPane = new LoginBorderPane(initialUsername, loginCallback);
// the border pane is the same size as the whole skin
borderPane.prefWidthProperty().bind(getSkinnable().widthProperty());
borderPane.prefHeightProperty().bind(getSkinnable().heightProperty());
getChildren().add(borderPane);
// always add self as style class, because CSS should relate to the skin not the control
getSkinnable().getStyleClass().add(getClass().getSimpleName());
}
}
The LoginSkin must extend SkinBase in order to serve as a control’s skin. Some interesting things about the LoginSkin include binding the preferred width and height to the same properties of the skinnable. That is necessary to cause the border pane to be the same size as the whole skin. Also, the style class should relate to the skin and not the control to enable different css clases on changing skins.
LoginBorderPane
Finally, I set up the view portion. The view’s elements are set up in the LoginBorderPane. This is done with a controller class and FXML. First is the controller:
public class LoginBorderPane extends BorderPane
{
@FXML private Label organizationNameLabel;
@FXML private TextField usernameTextField;
@FXML private PasswordField passwordField;
@FXML private Button signInButton;
public LoginBorderPane(String initialUsername, Callback<Pair<String, String>, Void> loginCallback)
{
super();
loadFxml(LoginBorderPane.class.getResource("Login.fxml"), this);
usernameTextField.setText(initialUsername);
signInButton.setOnMouseClicked((EventHandler<? super MouseEvent>) (event) ->
{
loginCallback.call(new Pair<>(usernameTextField.getText(), passwordField.getText()));
});
}
}
Notice I use a static FXML loader named loadFxml. This method allows the FXML to omit specifying a controller. This isn’t necessary. It’s just my preference, as I think it could make FXML more reusable. Alternatively, the FXML could specify the controller. Notice, I set the initial username and assign a event handler that calls the loginCallback that the client provided.
I built the FXML with SceneBuilder, but I’m omitting it for brevity (don’t worry, it’s in the source available below)
Test Code for Default Control
public class LoginDemo3 extends Application
{
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception
{
Login root = new Login(null, (c) -> null);
Scene scene = new Scene(root, 300, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
}
The default test above has a stub as a callback, so there’s no functionality after “sign in” is clicked. If you want some functionality then replace the stub with some real code.
Customized Control
Adding a real callback can dictate what happens when the “sign in” button is pressed. This code separation allows the control to be versatile enough to be used for any kind of login system; be it a database, LDAP or stand-alone application.
Also, the css can be changed to replace the image and the styling.
Download
You can download the above example with the link below. A few additions are added. For example, I added a ResourceBundle to allow changing of the organization name and all the fields (for internationalization), but it’s essentially identical.
Any comments are welcomed!

