动态构建JavaFX UI [英] Building JavaFX UI dynamically on the fly

查看:86
本文介绍了动态构建JavaFX UI的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我是JavaFX的新手。仍在摆弄几个样本,试图决定是否适用于我们正在尝试构建的应用程序。
我们的应用程序的第一阶段是一个数据输入阶段,用户将面临很多问题并记录他的回答。这里的问题是另一个团队正在构建问题集,这些问题都是XML格式的。

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>

它对应于各种字段,例如如果它是yesNo类型则有一个布尔单选按钮,以及大写字母的下拉列表列表,值的文本字段等。这些问题可能会因用户而异,因此用户可以通过此文件配置问题。

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.

想法是在应用程序启动期间加载此xml,解析它们并构建适当的UI组件即时。这可以通过JavaFX实现吗?我使用通过SceneBuilder构建的FXML文件制作了这个应用程序的小原型。但诀窍是在解析启动期间加载的Questions XML文件后,以编程方式生成为查询构建此UI组件所需的FXML文件。

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?

推荐答案

您可以采取几种方法。

一种是简单地解析XML文件,并在Java代码中创建FX控件。这不是一个很糟糕的方法,但你根本不会有任何FXML。基本的想法是你创建一个 DocumentBuilder ,用它来解析你的xml文件到 文档 ,这是xml文档的内存模型。您可以使用它来遍历xml元素,并为每个xml元素创建适当的JavaFX UI元素,将它们添加到某些 Pane

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.

另一种方法是使用可扩展样式表语言转换将 XML 文件转换为 FXML 。我当然不是这项技术的专家,但这个想法非常简单:

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:

定义基本上 xsl 的文件根据 xml 文件的内容定义 FXML 文件的外观。同样,我并不熟悉 xsl 的详细信息,但是这样的内容似乎适用于您的示例:

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>

现在你只需要创建一个 Transformer 从那个 xsl 文件(我将其命名为 xml2fxml.xsl )。 转换方法将读取 xml 文件,根据 xsl中的规则对其进行转换文件,并将输出发送到输出流。你只需要一点点技巧就可以将它传递给输入流并指示 FXMLLoader 从中读取生成的 fxml

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);
    }
}

更新:(另外注意上面 xsl 的轻微更新

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

虽然这很光滑,但它几乎太透明了使得访问控制器中的表单控件变得很困难。你需要对FXML定义的场景图的根元素的内容进行一些丑陋的检查才能找到正确的元素。

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.

这个例子使用了一些反思以获得价值;你也可以用很多 instanceof 测试和一些演员来做。它也通过知道它们都在第1列中来控制,这实际上违反了视图和控制器的分离;在控件上分配 id 的约定可能会更好(这可以区别于 Label s)而是使用它。

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

这篇关于动态构建JavaFX UI的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆