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
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 Label
s) 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 {
return selectedItem.toString();
}
}
}