3

I have run the Mastering FXML example, How to create custom components in JavaFX 2.0 using FXML and tried various other solutions from this site, but I still haven't found a simple enough example showing how to set up a custom control that is NOT the only part of the GUI. Since the question is still popping up it seems we need a simpler example for some of us..

I'm trying to create a simple control consisting of a vertical SplitPane with a Button in the top section and a label in the lower section. Then I want to place instances of this SplitPane-control in multiple tabs in a TabPane. Either the control won't show up, or I get stuck in various errors, depending on which example I try to follow. So, I'll backtrack a bit and will just simply ask: How do I separate out the SplitPane to be the custom control here?

Here is the FXML document:

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

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.AnchorPane?>

<TabPane prefHeight="256.0" prefWidth="477.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="customcontroltest.FXMLDocumentController">
   <tabs>
      <Tab>
         <content>
            <SplitPane dividerPositions="0.5" orientation="VERTICAL" prefHeight="114.0" prefWidth="160.0">
              <items>
                <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="100.0" prefWidth="160.0">
                     <children>
                          <Button fx:id="button" onAction="#handleButtonAction" text="Click Me!" />
                     </children>
                  </AnchorPane>
                <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="100.0" prefWidth="160.0">
                     <children>
                          <Label fx:id="label" minHeight="16" minWidth="69" />
                     </children>
                  </AnchorPane>
              </items>
            </SplitPane>
         </content>
      </Tab>
   </tabs>
</TabPane>

And the controller class:

package customcontroltest;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;

public class FXMLDocumentController implements Initializable
{

    @FXML
    private Label label;

    @FXML
    private void handleButtonAction(ActionEvent event)
    {
        label.setText("Hello World!");
    }

    @Override
    public void initialize(URL url, ResourceBundle rb)
    {
        // TODO
    }    
}

And the main test class:

package customcontroltest;

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


public class CustomControlTest extends Application
{
    @Override
    public void start(Stage stage) throws Exception
    {
        Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));

        Scene scene = new Scene(root);

        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args)
    {
        launch(args);
    }
}

I made a new FXML file and cut/pasted the whole SplitPane tag and all its contents into it. I replaced the SplitPane tag in the FXML document with <packageName.ControlClassName />. Then I made the controller class to extend SplitPane. I've tried specifying the controller in the FXML tag and/or in the controller class, but never got it right. Would someone with the right knowledge be willing to take a few minutes to just show a working example of this? I'm guessing more people would find such an example very useful. So, the SplitPane should be the new custom control, and you can then by default load it into the first tab in the TabPane. Then I will write code to add more instances into subsequent tabs.

Thank you so very much in advance.

UPDATE: I have broken out the SplitPane into its own FXML and controller class. Here is the FXML (CustomSplitPane.fxml):

<fx:root type="javafx.scene.control.SplitPane" dividerPositions="0.5" orientation="VERTICAL" prefHeight="114.0" prefWidth="160.0" xmlns:fx="http://javafx.com/fxml/1" fx:controller="customcontroltest.CustomSplitPaneController">
    <items>
        <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="100.0" prefWidth="160.0">
             <children>
                  <Button fx:id="button" onAction="#handleButtonAction" text="Click Me!" />
             </children>
        </AnchorPane>
        <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="100.0" prefWidth="160.0">
             <children>
                  <Label fx:id="label" minHeight="16" minWidth="69" />
             </children>
        </AnchorPane>
    </items>
</fx:root>

And the controller class (CustomSplitPaneController.java):

package customcontroltest;

public class CustomSplitPaneController extends AnchorPane
{
    @FXML
    private Label label;
    private SplitPane mySplitPane;

    public CustomSplitPaneController()
    {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("CustomSplitPane.fxml"));

        try 
        {
            fxmlLoader.load();
        } catch (IOException exception) 
        {
            throw new RuntimeException(exception);
        }
    }

    @FXML
    private void handleButtonAction(ActionEvent event)
    {
        label.setText("Hello World!");
    }
}

And the original main FXML now looks like this:

<TabPane prefHeight="256.0" prefWidth="477.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="customcontroltest.FXMLDocumentController">
   <tabs>
      <Tab>
         <content>
            <customcontroltest.CustomSplitPaneController /> 
         </content>
      </Tab>
   </tabs>
</TabPane>

The fxmlLoader.load() in the CustomSplitPaneController seems to be causing a java.lang.StackOverflowError. Maybe it's more evident now to someone here what I'm missing?

Thanks again.

Community
  • 1
  • 1
Andreas
  • 59
  • 6
  • 1
    I don't understand which part of the code you posted you consider to be the "custom control" here. Have you tried doing this the way it's described in the [documentation](http://docs.oracle.com/javase/8/javafx/api/javafx/fxml/doc-files/introduction_to_fxml.html#custom_components)? – James_D Jan 10 '17 at 23:04
  • I have, but seem to be missing something. I want the "custom control" to be everything inside the SplitPane tag. – Andreas Jan 11 '17 at 02:53
  • Well why don't you post that in your question? No one can possibly help you figure out what you're missing if they can't see your code. – James_D Jan 11 '17 at 02:54
  • Fair point. I was hoping someone would be able to just break out the SplitPane in a working example for me, instead of investigating errors in my code. I could be wrong. I will write up another try and post the code. Thanks. – Andreas Jan 11 '17 at 02:57
  • You've already cited a perfectly good working example though... it's not clear (at least not to me) what you would want that isn't in the example you linked. – James_D Jan 11 '17 at 02:58
  • I'm basically looking for the simplest example possible of how to create and use a "custom control" that is not just put as the root of the whole program/app. I guess something that would fall in between the two examples I linked to. The first link doesn't show how to use the control in the "middle" of an existing FXML, and the second link makes it way too complicated to show the basics. IMHO. – Andreas Jan 11 '17 at 05:06

4 Answers4

3

The fx:controller attribute in an FXML file is an instruction for the FXML loader to create an instance of the specified class and use it as the controller for the UI hierarchy defined by the FXML.

In the attempt to create a custom split pane that you posted, what happens when you create an instance of CustomSplitPaneController is:

  • You create an FXMLLoader in the constructor of CustomSplitPaneController, which loads CustomSplitPane.fxml
  • CustomSplitPane.fxml has a fx:controller attribute specifying CustomSplitPaneController as the controller class, so it creates a new instance of CustomSplitPaneController (by calling its constructor, of course)
  • The constructor of CustomSplitPaneController creates an FXMLLoader which loads CustomSplitPane.fxml
  • and so on.

So very quickly, you get a stack overflow exception.


Control classes in JavaFX encapsulate both the view and the controller. In the standard JavaFX control classes the view is represented by a Skin class, and the controller by a Behavior class. The control class itself extends Node (or a subclass: Region) and when you instantiate it, it instantiates both the skin and the behavior. The skin defines the layout and appearance of the control, and the behavior maps various input actions to actual code that modifies the properties of the control itself.

In the pattern you are trying to replicate, shown here and here, this is slightly modified. In this version, the "view" is defined by an FXML file, and the controller (the behavior) is implemented directly in the control class itself (there is no separate behavior class).

To make this work, you have to use FXML slightly differently to usual. First, when you use your custom control, you are going to instantiate the control class directly (without any knowledge of the FXML that defines its layout). So if you are using this in java, you would do new CustomSplitPane(), and if you are using it in FXML you would do <CustomSplitPane>. Either way, you invoke the constructor of your custom control (which I'm calling CustomSplitPane).

To use CustomSplitPane in the UI hierarchy, it must of course be a Node subclass. If you want it to be a kind of SplitPane, you would make it extend SplitPane:

public class CustomSplitPane extends SplitPane {

    // ...

}

Now, in the constructor of CustomSplitPane, you need to load the FXML file that defines the layout, but you need it to lay out the current object. (In the usual usage of an FXML file, the FXMLLoader creates a new node for the root of the hierarchy, of the type specified, and the load() method returns it. You want the FXMLLoader to use the existing object as the root of the hierarchy.) To do this, you use the <fx:root> element as the root element of the FXML file, and you tell the FXMLLoader to use this as the root:

loader.setRoot(this);

Additionally, since the handler methods are defined in the current object, you also want the controller to be the current object:

loader.setController(this);

Since you are specifying an existing object as the controller, you must not have a fx:controller attribute in the FXML file.

So you end up with:

package customcontroltest;

public class CustomSplitPane extends SplitPane {
    @FXML
    private Label label;

    public CustomSplitPaneController() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("CustomSplitPane.fxml"));

        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try  {
            fxmlLoader.load();
        } catch (IOException exception)  {
            throw new RuntimeException(exception);
        }
    }

    @FXML
    private void handleButtonAction(ActionEvent event) 
        label.setText("Hello World!");
    }
}

and the FXML file:

<fx:root type="javafx.scene.control.SplitPane" dividerPositions="0.5" orientation="VERTICAL" prefHeight="114.0" prefWidth="160.0" xmlns:fx="http://javafx.com/fxml/1" >
    <items>
        <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="100.0" prefWidth="160.0">
             <children>
                  <Button fx:id="button" onAction="#handleButtonAction" text="Click Me!" />
             </children>
        </AnchorPane>
        <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="100.0" prefWidth="160.0">
             <children>
                  <Label fx:id="label" minHeight="16" minWidth="69" />
             </children>
        </AnchorPane>
    </items>
</fx:root>

And now you can use this in another FXML file as you need:

<TabPane prefHeight="256.0" prefWidth="477.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="customcontroltest.FXMLDocumentController">
   <tabs>
      <Tab>
         <content>
            <customcontroltest.CustomSplitPane /> 
         </content>
      </Tab>
   </tabs>
</TabPane>
James_D
  • 201,275
  • 16
  • 291
  • 322
1

Your custom control class should extends one of the Parent class, for example Region or Pane. If you are too lazy to do layout stuff, just extend an advanced pane like GridPane.

Then your custom control class should load an FXML containing the SplitPane and its children. The controller for the FXML can be simply this custom control class, or you can still separate it out to its personal controller class. The SplitPane node should be added as a child of the custom control class; this means that your custom control (which is a Parent type), has to handle some layout logic. At this point, your custom control is complete.

This control is ready to be used in FXML. However, if you want to use it in Scene Builder, you need to package it into a JAR file, and add it to Scene Builder. Note that in order for Scene Builder to work, your custom control class MUST have a parameterless constructor defined.

Jai
  • 8,165
  • 2
  • 21
  • 52
  • Ah, that might be where I've been going wrong. I interpreted everything I read to mean that the control class could extend `SplitPane`, which is what I've done so far. I'll try extending the `AnchorPane` instead then. Many thanks! – Andreas Jan 11 '17 at 03:02
  • @Andreas Well, you can definitely extend from `SplitPane`, but how would you write your FXML? You have lost a container to host two separate things, and you cannot have two root nodes in FXML. Of course, you can always choose to split it into two FXML and load the two into your custom control, but wouldn't it be messier this way. – Jai Jan 11 '17 at 03:06
  • I have updated my original post with the code that I broke out of the initial post, and extended 'AnchorPane'. Still missing some vital piece.. – Andreas Jan 11 '17 at 04:57
  • @Andreas You must set the controller. `fxmlLoader.setController(this);` – Jai Jan 11 '17 at 05:22
  • @Andreas But the control class you posted doesn't extend `SplitPane`. – James_D Jan 11 '17 at 13:03
  • @Jai I did that from the start, but that causes `java.lang.RuntimeException: javafx.fxml.LoadException: Controller value already specified.` – Andreas Jan 11 '17 at 13:06
  • @Andreas Right. Because you have a `fx:controller` attribute. Again, follow the example you cited. There is no such attribute in that example. Writing an answer now, may take a little while... – James_D Jan 11 '17 at 13:07
  • @James_D Correct. Now I tried to follow Jai's recommendation to extend a `Pane` or similar. – Andreas Jan 11 '17 at 13:08
  • @Andreas I don't understand why you don't just extend `SplitPane`, since you want a custom `SplitPane`.... – James_D Jan 11 '17 at 13:09
  • @James_D When I take out the `fx:controller` attribute, and add `fxmlLoader.setRoot(this); fxmlLoader.setController(this);` I get `javafx.fxml.LoadException: Root is not an instance of javafx.scene.control.SplitPane.` – Andreas Jan 11 '17 at 13:13
  • @Andreas So make sure it is an instance of `SplitPane`. ' – James_D Jan 11 '17 at 13:16
  • @James_D I did extend `SplitPane` many times at my first attempts, but got all of these various errors that I couldn't figure out. BUT, now I changed it back to extending `SplitPane`and.. I guess I'm having a better day today, because now it suddenly works..! I could have sworn I had tried this exact thing many times over already. OK, now I'm going to study this harder to see what I actually did.. Thank you! – Andreas Jan 11 '17 at 13:19
  • @Andreas Added an answer summarizing (and explaining) all this. But basically, all you have to do is follow the provided examples. – James_D Jan 11 '17 at 13:27
1

I'm not entirely sure if this is what you're looking for, but JavaFX controls are based on the Model, View, Controller (MVC) pattern.

Model Where the Model class is where the any information is stored for your system. For example, if you had a textField, you'd store what value the text field is holding in the model class. I always think of it as a miniature database for my control.

View The View class is visually what your control looks like. Defining the size, shape, color, etc. A note on "color", this is where you'd set the default color of your control. (this could also be done using FXML, but I personally would rather use java code). The model is often passed to the View constructor as an argument for binding using bean properties. (For java, not sure how you'd do it for FXML)

Controller The controller class is where manipulation can occur. If I click a button, or change something in my textField, what does the controller do or how does it manipulate the model. The Model and View are both passed as argument to the controller. This gives the controller a reference to the model and the view which allows the controller to manipulate each as designed. Other outside classes can interact with your controller class and your controller class acts on the model and view.

With that said, without any additional information, it looks like everything you're doing is just combining existing controls into something pre-defined for reuse. It may be worth looking into defining a class that extends SplitPane, and a constructor that already adds your button and label to where you want them. Your new class could then be treated like a SplitPane as well as have your action for your button built in.

A really good break down of this is in the book,

Apress JavaFX 8 Introduction By Example Chapter 6

Deezee
  • 51
  • 1
  • 9
1

OK, so here is the working solution all laid out, file by file. Hopefully this is useful to someone else too.

CustomControlTest.java

package customcontroltest;

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


public class CustomControlTest extends Application
{
    @Override
    public void start(Stage stage) throws Exception
    {
        Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));

        Scene scene = new Scene(root);

        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args)
    {
        launch(args);
    }
}

FXMLDocument.fxml

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

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.AnchorPane?>

<TabPane prefHeight="256.0" prefWidth="477.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="customcontroltest.FXMLDocumentController">
   <tabs>
      <Tab>
         <content>
            <customcontroltest.CustomSplitPaneController /> 
         </content>
      </Tab>
   </tabs>
</TabPane>

FXMLDocumentController.java

package customcontroltest;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.Initializable;

public class FXMLDocumentController implements Initializable
{
    @Override
    public void initialize(URL url, ResourceBundle rb)
    {
        // TODO
    }    
}

CustomSplitPane.fxml

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

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<fx:root type="javafx.scene.control.SplitPane" dividerPositions="0.5" orientation="VERTICAL" prefHeight="114.0" prefWidth="160.0" xmlns:fx="http://javafx.com/fxml/1" >
    <items>
        <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="100.0" prefWidth="160.0">
             <children>
                  <Button fx:id="button" onAction="#handleButtonAction" text="Click Me!" />
             </children>
        </AnchorPane>
        <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="100.0" prefWidth="160.0">
             <children>
                  <Label fx:id="label" minHeight="16" minWidth="69" />
             </children>
        </AnchorPane>
    </items>
</fx:root>

The NetBeans IDE will give an error on #handleButtonAction, saying "Controller is not defined on root component", but it won't actually give compilation errors. (This is where I was tricked into not even trying to compile when I saw that highlighted error!)

CustomSplitPaneController.java

package customcontroltest;

import java.io.IOException;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.control.SplitPane;


public class CustomSplitPaneController extends SplitPane
{
    @FXML
    private Label label;

    public CustomSplitPaneController()
    {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("CustomSplitPane.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try 
        {
            fxmlLoader.load();
        } catch (IOException exception) 
        {
            throw new RuntimeException(exception);
        }
    }

    @FXML
    private void handleButtonAction(ActionEvent event)
    {
        label.setText("Hello World!");
    }
}
Andreas
  • 59
  • 6
  • 1
    One thing to note, `CustomSplitPane` is technically not quite a controller; it itself, when instantiated, works just like any controls (like `GridPane` etc). The way you are naming it now, you are going to confuse yourself when you look at this half a year from now. – Jai Jan 12 '17 at 00:41
  • 1
    Ah, yes I already think I see what you mean. A "controller" should really be an item/object that holds an instance of a CustomSplitPane, right? But since in this case the CustomSplitPaneController.java is setting itself to be the controller, I can get away with it, so to speak. (?) =) – Andreas Jan 12 '17 at 14:11