Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.0k views
in Technique[技术] by (71.8m points)

xml - Building JavaFX UI dynamically on the fly

I am a novice to JavaFX. Still fiddling around a few samples to try and decide if that works out for the application that we are trying to build. First phase of our app is kind of a data entry phase where the users will be poised with a lot of questions and his responses are recorded. The catch here is that another team is building the question set and these questions are in an XML, like this.

<?xml version="1.0" encoding="UTF-8"?>
<userData>    
<question id="Q1" type ="desc">
    <text>Enter the name of the Component</text>
</question>
<question id ="Q2" type ="list">
    <text>Select mechanism type</text>
    <choices>
        <choice> Type 1 </choice>
        <choice> Type 2 </choice>
        <choice> Type 3 </choice>
        <choice> Type 4 </choice>
    </choices>
</question>
<question id ="Q5" type="yesNo">
    <text> Whether the parts have been verified by the supervisor? </text>
</question>
<question id ="Q6" type="yesNo">
    <text> Whether the component is available within the domicile </text>
</question>
<question id ="Q7" type="value">
    <text> Enter the quantity </text>
</question>
<question id ="Q8" type="value">
    <text> Enter the unit price </text>
</question>
</userData>

It corresponds to various fields like having a boolean radio button if its a yesNo type, a dropdown in case of list, a text field for values and so on. These questions can change depending on the user so the user can configure the questions through this file.

The idea is to load this xml during the application start, parse them and build appropriate UI Components on the fly. Can this be achieved through JavaFX? I made a small prototype of this app using an FXML file built through SceneBuilder. But the trick is to generate the FXML file required to build this UI Components for queries programmatic-ally after parsing the Questions XML file which was loaded during the start up.

What is a good starting to point in achieving this functionality?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

There are a couple of approaches you can take to this.

One is to simply parse the XML file, and create the FX controls in Java code as you go. This isn't too bad an approach, but you will not have any FXML at all. The basic idea is that you create a DocumentBuilder, use it to parse your xml file to a Document, which is an in-memory model of the xml document. You can use that to iterate through the xml elements, and create the appropriate JavaFX UI element for each xml element, adding them to some Pane.

The other approach is to use an Extensible Stylesheet Language Transformation to transform your XML file into FXML. I am certainly no expert in this technology, but the idea is pretty straightforward:

Define an xsl file that basically defines what your FXML file should look like based on the contents of your xml file. Again, I am not really familiar with the details of xsl, but something like this appears to work for your example:

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

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fx="http://javafx.com/fxml">

    <xsl:template match="/">

        <xsl:processing-instruction name="import">
            java.lang.*
        </xsl:processing-instruction>

        <xsl:processing-instruction name="import">
            javafx.scene.layout.*
        </xsl:processing-instruction>

        <xsl:processing-instruction name="import">
            javafx.scene.control.*
        </xsl:processing-instruction>

        <xsl:processing-instruction name="import">
            javafx.geometry.Insets
        </xsl:processing-instruction>

        <xsl:processing-instruction name="import">
            javafx.collections.FXCollections
        </xsl:processing-instruction>

        <GridPane hgap="5" vgap="5" fx:id="form" fx:controller="xml2fx.FormController">         
            <columnConstraints>
                <ColumnConstraints halignment="RIGHT" hgrow="NEVER" />
                <ColumnConstraints halignment="LEFT" hgrow="ALWAYS" />
            </columnConstraints>
            <padding>
                <Insets top="10" bottom="10" left="10" right="10"/>
            </padding>

            <xsl:apply-templates select="//text"/>
            <xsl:apply-templates select="//question"/>

        </GridPane>

    </xsl:template>

    <xsl:template match="text">
        <Label text="{.}" wrapText="true" textAlignment="RIGHT"
            GridPane.columnIndex="0"
            GridPane.rowIndex="{count(../preceding-sibling::question)}" />  
    </xsl:template>


    <xsl:template name="controlCoords">
            <GridPane.columnIndex>1</GridPane.columnIndex>
            <GridPane.rowIndex>
                <xsl:value-of select="count(preceding-sibling::question)"/>
            </GridPane.rowIndex>    
    </xsl:template>

    <xsl:template match="question[@type='desc']">
        <TextArea fx:id="{@id}" id="{@id}">
            <xsl:call-template name="controlCoords" />
        </TextArea>     
    </xsl:template>

    <xsl:template match="question[@type='list']">
        <ComboBox fx:id="{@id}" id="{@id}">
            <xsl:call-template name="controlCoords" />
            <items>
                <FXCollections fx:factory="observableArrayList">
                    <xsl:for-each select="choices/choice">
                        <String fx:value="{.}"/>
                    </xsl:for-each>
                </FXCollections>
            </items>
        </ComboBox>
    </xsl:template>

    <xsl:template match="question[@type='value']">
        <TextField fx:id="{@id}" id="{@id}">
            <xsl:call-template name="controlCoords" />
        </TextField>    
    </xsl:template>

    <xsl:template match="question[@type='yesNo']">
        <CheckBox fx:id="{@id}" id="{@id}">
            <xsl:call-template name="controlCoords" />
        </CheckBox> 
    </xsl:template>

</xsl:stylesheet>

Now you just need to create a Transformer from that xsl file (I named it xml2fxml.xsl). The transform method will read the xml file, transform it according to the rules in the xsl file, and send the output to an output stream. You just need a little trickery to pipe that to an input stream and instruct the FXMLLoader to read the generated fxml from it:

import java.io.PipedInputStream;
import java.io.PipedOutputStream;

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

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;


public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            final PipedOutputStream transformOutput = new PipedOutputStream();
            final PipedInputStream fxmlInputStream = new PipedInputStream(transformOutput);

            Thread transformThread = new Thread( () -> {
                try {
                    StreamSource xsltSource = new StreamSource(getClass().getResourceAsStream("xml2fxml.xsl"));
                    Transformer transformer = TransformerFactory.newInstance().newTransformer(xsltSource);
                    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                    StreamSource xmlSource = new StreamSource(getClass().getResourceAsStream("questionnaire.xml"));
                    StreamResult transformerResult = new StreamResult(transformOutput);
                    transformer.transform(xmlSource, transformerResult);
                    transformOutput.close();

                } catch (Exception e) {
                    e.printStackTrace();
                }
            });

            transformThread.start();

            FXMLLoader loader = new FXMLLoader();
            Parent root = loader.load(fxmlInputStream);
            Scene scene = new Scene(root, 400, 400);
            primaryStage.setScene(scene);
            primaryStage.show();


        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

Update: (Also note slight updates to the xsl above)

While this is quite slick, it is almost too transparent in that it makes it difficult to get access to the form controls in the controller. You need to do a bit of ugly examination of the contents of the root element of the FXML-defined scene graph in order to find the correct elements.

This example uses some reflection to get at the values; you could also do it with a lot of instanceof tests and some casting. It also gets at the controls by "knowing" that they are all in column 1, which really violates the separation of view and controller; it might be better to have some convention on the id assigned to the controls (that distinguishes them from the Labels) and use that instead.

package xml2fx;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.SelectionModel;
import javafx.scene.control.TextInputControl;
import javafx.scene.layout.GridPane;

public class FormController {

    private static final String SELECTED_VALUE = "yes" ;
    private static final String UNSELECTED_VALUE = "no" ;

    @FXML
    private GridPane form ;

    private final Map<String, Control> controls = new HashMap<>();
    private final List<String> ids = new ArrayList<>();

    public void initialize() {
        for (Node node : form.getChildren()) {
            if (GridPane.getColumnIndex(node) == 1) { // all form controls are in column 1
                if (node instanceof Control) {
                    String id = node.getId();
                    controls.put(id, (Control)node);
                    ids.add(id);
                }
            }
        }
    }

    public List<String> getIds() {
        return Collections.unmodifiableList(ids);
    }

    public String getUserValue(String id) throws ReflectiveOperationException {
        Control control = controls.get(id);
        if (control == null) throw new IllegalArgumentException("No control with id "+id);
        return getValueForControl(control);
    }

    private String getValueForControl(Control control) throws ReflectiveOperationException {
        if (isTextControl(control)) {
            return getTextControlValue(control);
        } else if (isSelectable(control)) {
            return getSelectableValue(control);
        } else if (hasSelectionModel(control)) {
            return getSelectedValue(control);
        }
        throw new IllegalArgumentException("Unsupported control class: "+control.getClass().getName());
    }

    private boolean isTextControl(Control control) {
        // TextAreas, TextFields, etc:
        return control instanceof TextInputControl ;
    }

    private String getTextControlValue(Control control) {
        return ((TextInputControl) control).getText();
    }

    private boolean isSelectable(Control control) {
        // ToggleButtons, CheckBoxes...
        for (Method method :  control.getClass().getMethods()) {
            if (method.getName().equals("isSelected") 
                    && method.getReturnType() == boolean.class) {
                return true ;
            }
        }
        return false ;
    }

    private String getSelectableValue(Control control) throws ReflectiveOperationException {
        Method isSelectedMethod = control.getClass().getMethod("isSelected");
        boolean selected = (Boolean) isSelectedMethod.invoke(control);
        if (selected) {
            return SELECTED_VALUE ;
        } else {
            return UNSELECTED_VALUE ;
        }
    }

    private boolean hasSelectionModel(Control control) {
        // ComboBoxes, ListViews, TableViews, etc:
        for (Method method : control.getClass().getMethods()) {
            if (method.getName().equals("getSelectionModel")) {
                return true ;
            }
        }
        return false ;
    }

    private String getSelectedValue(Control control) throws ReflectiveOperationException  {
        Method selectionModelMethod = control.getClass().getMethod("getSelectionModel");
        SelectionModel<?> selectionModel = (SelectionModel<?>) selectionModelMethod.invoke(control);
        Object selectedItem = selectionModel.getSelectedItem();
        if (selectedItem == null) {
            return "" ;
        } else {
       

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...