简体   繁体   中英

JavaFXML create custom tag element that does not extend node

I would like to create a custom tag / element for my fxml file but I do not want this element to be forced to inherit from a Pane , Button , TextField and so on. Is there some interface that can be implemented in my custom element that would require me to implement lets say fx() method which is required to return a Node / Region element which is supposed to be rendered in the Scene . What I mean if something like the following is possible?

public class CustomElement implements SOME_FXML_INTERFACE {

    private String myArg;

    public CustomElement(@NamedArg("myArg") myArg) {
        this.myArg = myArg;
    }

    // method that is required to be implemented by SOME_FXML_INTERFACE
    // this method retuns some GUI element which actually needs to be rendered in the Scene
    @Override
    public Object fx() {
        return new TextField(myArg);
    }

}


<HBox>
    <CustomElement myArg="some_argument"/>
</HBox>

All this so that I could have CustomElement who can accept custom argument in the contructor.

You can create arbitrary objects, but factory objects can only be used by nodes that support this. (Technically you could do this but it would involve using a getter returning a new instance every time it's invoked.) However you can eg use your custom class as cellFactory for a ListView .

If you do not need to rely on an instance method, but are satisfied with a static factory method, you can use the fx:factory tag to specify a method to create the node instance and you do not need to implement any interface to do that:

package my.package;

...

public class CustomElement {

    public static Node fx() {
        return new TextField(myArg);
    }

}
<?import my.package.CustomElement?>
<?import javafx.scene.layout.HBox?>

<HBox xmlns:fx="http://javafx.com/fxml">
    <children>
        <CustomElement fx:factory="fx" />
    </children>
</HBox>

More information is available in the Introduction to FXML

BTW: If your class has a public constructor not taking any parameters you can create an instance of that class simply by adding a element with the name of the class to the fxml. Eg the following fxml results in an ArrayList when loaded; the ways you can use those instances is limited though;

<?import java.util.ArrayList?>

<ArrayList />

As well as using a factory method, as described by fabian , you can repurpose the builder mechanism to do this. As with <fx:factory> , which is limited to static methods with no arguments, the usage is a bit limited. However, here's an example factory/builder that creates either a TextField or a Label , depending on whether the editable flag is set:

package org.jamesd.examples.fxmlfactory;

import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.util.Builder;

public class Factory implements Builder<Node> {

    private String text = "" ;
    private boolean editable = false ;



    public String getText() {
        return text;
    }



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



    public boolean isEditable() {
        return editable;
    }



    public void setEditable(boolean editable) {
        this.editable = editable;
    }



    @Override
    public Node build() {
        if (editable) {
            return new TextField(text);
        } else {
            return new Label(text);
        }
    }


}

Here's an FXML file that uses this:

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

<?import javafx.scene.layout.BorderPane ?>
<?import javafx.scene.layout.HBox ?>
<?import javafx.scene.Node ?>


<BorderPane xmlns="http://javafx.com/javafx/11.0.1"
    xmlns:fx="http://javafx.com/fxml/1">

    <center>
        <HBox spacing="5">
            <Node text="Hello" editable="false" />
            <Node text="Name" editable="true" />
        </HBox>
    </center>
</BorderPane>

To make the FXMLLoader aware of your builder, you have to specify a class to use it for. Here we can use it for Node (but this requirement is a bit restrictive, as you'll be essentially limited to a single builder). You do this by creating a BuilderFactory implementation:

package org.jamesd.examples.fxmlfactory;

import javafx.fxml.JavaFXBuilderFactory;
import javafx.scene.Node;
import javafx.util.Builder;
import javafx.util.BuilderFactory;

public class NodeBuilderFactory implements BuilderFactory{

    private final JavaFXBuilderFactory defaultFactory = new JavaFXBuilderFactory();

    public Builder<?> getBuilder(Class<?> type) {
        if (type == Node.class) {
            return new Factory();
        }
        return defaultFactory.getBuilder(type);
    }

}

and then registering it with the FXMLLoader :

package org.jamesd.examples.fxmlfactory;

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

public class App extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {

        FXMLLoader loader = new FXMLLoader(getClass().getResource("Example.fxml"));
        loader.setBuilderFactory(new NodeBuilderFactory());
        Parent root = loader.load();
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM