JavaFX9 高级教程(四)
原文:Pro JavaFX 9
六、在 JavaFX 中创建图表
任何足够先进的技术都和魔法没什么区别。—亚瑟·C·克拉克
在许多商业应用中,报告是一个重要的方面。JavaFX 平台包含一个用于创建图表的 API。因为一个Chart基本上就是一个Node,所以将图表与 JavaFX 应用程序的其他部分集成起来非常简单。因此,报告是典型 JavaFX 业务应用程序不可或缺的一部分。
设计一个 API 通常是许多需求之间的折衷。两个最常见的需求是“简单易用”和“易于扩展”JavaFX Chart API 满足了这两个要求。图表 API 包含许多方法,允许开发人员更改图表的外观和感觉以及数据,使其成为一个灵活的 API,可以很容易地进行扩展。不过,这些设置的默认值非常合理,只需几行代码就可以轻松地将图表与自定义应用程序集成在一起。
JavaFX 9 中的 JavaFX Chart API 有八个具体的实现,可供开发人员使用。除此之外,开发人员可以通过扩展一个抽象类来添加他们自己的实现。
JavaFX 图表 API 的结构
存在不同类型的图表,并且有多种方法对它们进行分类。JavaFX 图表 API 区分双轴图表和不带轴的图表。JavaFX 9 版本包含一个无轴图表的实现,即PieChart。双轴图有很多,都是抽象XYChart类的扩展,如图 6-1 。
图 6-1。
Overview of the charts in the JavaFX Chart API
抽象的Chart类定义了所有图表的设置。基本上,图表由三部分组成:标题、图例和内容。图表的内容对于每个实现都是特定的,但是图例和标题概念在各个实现中是相似的。因此,Chart类有许多带有相应 getter 和 setter 方法的属性,这些方法允许操作这些概念。Chart类的 Javadoc 提到了以下属性。
BooleanProperty animated
ObjectProperty<Node> legend
BooleanProperty legendVisible
ObjectProperty<Side> legendSide
StringProperty title
ObjectProperty<Side> titleSide
在接下来的示例中,我们使用了其中的一些属性,但是我们也展示了即使没有为这些属性设置值,Chart API 也允许您创建漂亮的图表。
因为Chart扩展了Region、Parent和Node,所以这些类上可用的所有属性和方法也可以在Chart上使用。好处之一是,用于向 JavaFX Node添加样式信息的 CSS 样式技术也适用于 JavaFX 图表。
JavaFX CSS 参考指南,可在 http://download.java.net/jdk8/jfxdocs/ javafx/scene/doc-files/cssref.html获得,包含可由设计者和开发者改变的 CSS 属性的概述。默认情况下,JavaFX 9 运行时附带的 modena 样式表用于显示 JavaFX 图表。有关在 JavaFX 图表中使用 CSS 样式的更多信息,请参考位于 http://docs.oracle.com/javase/8/javafx/user-interface-tutorial/css-styles.htm 的 Oracle 图表教程。
使用 JavaFX 饼图
PieChart以典型的饼图结构呈现信息,其中切片的大小与数据的值成比例。在深入细节之前,我们展示一个小应用程序来呈现一个PieChart。
简单的例子
我们的例子显示了许多编程语言的“市场份额”,基于 2017 年 4 月的 TIOBE 指数。TIOBE 编程社区指数可在 https://www.tiobe.com/tiobe-index 获得,它提供了基于搜索引擎流量的编程语言受欢迎程度的指示。2017 年 4 月排名截图如图 6-2 。
图 6-2。
Screenshot of the TIOBE index in April 2017, taken from www.tiobe.com/tiobe-index Note
在 https://www.tiobe.com/tiobe-index/programming-languages-definition/ 中描述了 TIOBE 使用的算法。这些数字的科学价值超出了我们示例的范围。
清单 6-1 包含了这个例子的代码。
package com.projavafx.charts ;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ChartApp1 extends Application {
@Override
public void start(Stage primaryStage) {
PieChart pieChart = new PieChart();
pieChart.setData(getChartData());
primaryStage.setTitle("PieChart");
StackPane root = new StackPane();
root.getChildren().add(pieChart);
primaryStage.setScene(new Scene(root, 400, 250));
primaryStage.show();
}
private ObservableList<PieChart.Data> getChartData() {
ObservableList<PieChart.Data> answer = FXCollections.observableArrayList();
answer.addAll(new PieChart.Data("java", 15.57),
new PieChart.Data("C", 6.97),
new PieChart.Data("C++", 4.55),
new PieChart.Data("C#", 3.58),
new PieChart.Data("Python", 3.45),
new PieChart.Data("PHP", 3.38),
new PieChart.Data("Visual Basic .NET", 3.25));
return answer;
}
}
Listing 6-1.
Rendering
the TIOBE Index in a PieChart
运行该示例的结果如图 6-3 所示。
图 6-3。
Rendering the TIOBE index in a PieChart
只需有限的代码,我们就可以在PieChart中呈现数据。在我们修改这个例子之前,我们解释一下不同的部分。
设置应用程序、舞台和场景所需的代码包含在第一章中。一个PieChart扩展了一个Node,所以我们可以很容易地将它添加到场景图中。start 方法中的前两行代码创建了PieChart,并向其中添加了所需的数据:
PieChart pieChart = new PieChart();
pieChart.setData(getChartData());
类型为ObservableList<PieChart.Data>的数据是从getChartData()方法中获得的,对于我们的例子,它包含静态数据。正如getChartData()方法的返回类型所指定的,返回的数据是PieChart.Data的一个ObservableList。
PieChart.Data的一个实例是PieChart的一个嵌套类,它包含了绘制一片饼图所需的信息。PieChart.Data有一个接受切片名称和值的构造器:
PieChart.Data(String name, double value)
我们使用这个构造器来创建包含编程语言名称及其在 TIOBE 索引中的分数的数据元素。
new PieChart.Data("java", 15.57)
然后,我们将这些元素添加到我们需要返回的 ObservableList <piechart.data>中。</piechart.data>
一些修改
虽然这个简单例子的结果看起来已经很好了,但是我们可以调整代码和渲染。首先,该示例使用两行代码来创建PieChart并用数据填充它:
PieChart pieChart = new PieChart();
pieChart.setData(getChartData());
因为PieChart也有一个参数构造器,前面的代码片段可以替换如下。
PieChart pieChart = new PieChart(getChartData());
除了在抽象类Chart上定义的属性之外,PieChart还有以下属性。
BooleanProperty clockwise
ObjectProperty<ObservableList<PieChart.Data>> data
DoubleProperty labelLineLength
BooleanProperty labelsVisible
DoubleProperty startAngle
我们在上一节中讨论了数据属性。其他一些属性将在下一段代码中演示。清单 6-2 包含了start()方法的修改版本。
public void start(Stage primaryStage) {
PieChart pieChart = new PieChart();
pieChart.setData(getChartData());
pieChart.setTitle("Tiobe index");
pieChart.setLegendSide(Side.LEFT);
pieChart.setClockwise(false);
pieChart.setLabelsVisible(false);
primaryStage.setTitle("PieChart");
StackPane root = new StackPane();
root.getChildren().add(pieChart);
primaryStage.setScene(new Scene(root, 400, 250));
primaryStage.show();
}
Listing 6-2.
Modified Version
of the PieChart Example
因为我们在新代码中使用了Side.LEFT字段,所以我们也必须在应用程序中导入Side类。这是通过在代码的导入块中添加以下行来实现的。
import javafx.geometry.Side
运行这个修改后的版本会产生如图 6-4 所示的修改后的输出。
图 6-4。
The output of the modified PieChart example
更改几行代码会导致输出看起来非常不同。我们更详细地回顾一下我们所做的更改。首先,我们向图表添加了一个标题。这是通过调用完成的
pieChart.setTitle("Tiobe index");
我们也可以使用titleProperty:
pieChart.titleProperty().set("Tiobe index");
这两种方法产生相同的输出。
Note
即将到来的修改也可以使用相同的模式来完成。我们只用 setter 方法来记录这种方法,但是用基于属性的方法来代替它是很容易的。
我们修改后的示例中的下一行代码更改了图例的位置:
pieChart.setLegendSide(Side.LEFT);
当未指定legendSide时,图例显示在默认位置,即图表下方。title和legendSide都是属于抽象Chart类的属性。因此,它们可以设置在任何图表上。我们修改后的示例中的下一行修改了一个特定于PieChart的属性:
pieChart.setClockwise(false);
默认情况下,PieChart中的切片是顺时针绘制的。通过将该属性设置为 false,切片将逆时针呈现。我们还禁止在PieChart中显示标签。标签仍显示在图例中,但它们不再指向单个切片。这是通过以下代码行实现的:
pieChart.setLabelsVisible(false);
到目前为止,所有布局更改都是以编程方式完成的。使用 CSS 样式表来设计一般的应用程序,特别是图表,也是可能的,并且经常被推荐。
我们从 Java 代码中删除了布局更改,并添加了一个包含一些布局说明的样式表。清单 6-3 显示了start()方法的修改代码,清单 6-4 包含了我们添加的样式表。
public void start(Stage primaryStage) {
PieChart pieChart = new PieChart();
pieChart.setData(getChartData());
pieChart.titleProperty().set("Tiobe index");
primaryStage.setTitle("PieChart");
StackPane root = new StackPane();
root.getChildren().add(pieChart);
Scene scene = new Scene (root, 400, 250);
scene.getStylesheets().add("/chartappstyle.css");
primaryStage.setScene(scene);
primaryStage.show();
}
Listing 6-3.Remove Programmatic Layout Instructions
.chart {
-fx-clockwise: false;
-fx-pie-label-visible: true;
-fx-label-line-length: 5;
-fx-start-angle: 90;
-fx-legend-side: right;
}
.chart-pie-label {
-fx-font-size:9px;
}
.chart-content {
-fx-padding:1;
}
.default-color0.chart-pie {
-fx-pie-color:blue;
}
.chart-legend {
-fx-background-color: #f0e68c;
-fx-border-color: #696969;
-fx-border-width:1;
}
Listing 6-4.Style Sheet
for PieChart Example
运行这段代码会产生如图 6-5 所示的输出。
图 6-5。
Using CSS to style the PieChart
我们现在回顾一下我们所做的更改。在我们详细讨论各个更改之前,我们将展示如何在应用程序中包含 CSS。这是通过向场景添加样式表来实现的,如下所示。
scene.getStylesheets().add("/chartappstyle.css");
当应用程序运行时,包含样式表的文件chartappstyle.css必须在类路径中。
在清单 6-2 中,我们使用
pieChart.setClockwise(false)
我们从清单 6-3 的代码中删除了那一行,取而代之的是在样式表的chart类上定义了-fx-clockwise属性:
.chart {
-fx-clockwise: false;
-fx-pie-label-visible: true;
-fx-label-line-length: 5;
-fx-start-angle: 90;
-fx-legend-side: right;
}
在同一个.chart类定义中,我们通过将- fx-pie-label-visible属性设置为 true 来使饼图上的标签可见,并将每个标签的线长度指定为 5。
此外,我们将整个饼图旋转 90 度,这是通过定义-fx-start-angle属性实现的。标签现在在样式表中定义了,我们通过省略下面一行从代码中删除了相应的定义。
pieChart.setLabelsVisible(false)
为了确保图例出现在图表的右侧,我们指定了-fx-legend-side属性。
默认情况下,PieChart使用在 caspian 样式表中定义的默认颜色。第一个切片用default-color0填充,第二个切片用default-color1填充,依此类推。更改不同切片颜色的最简单方法是覆盖默认颜色的定义。在我们的样式表中,这是通过
.default-color0.chart-pie {
-fx-pie-color: blue;
}
可以对其他切片进行同样的操作。
如果在没有 CSS 的其他部分的情况下运行该示例,您会注意到图表本身相当小,并且标签的大小占用了太多的空间。因此,我们将标签的字体大小修改如下:
.chart-pie-label {
-fx-font-size:9px;
}
此外,我们减少了图表区域的填充:
.chart-content {
-fx-padding:1;
}
最后,我们改变背景和图例的笔画。这是通过如下重写chart-legend类来实现的。
.chart-legend {
-fx-background-color: #f0e68c;
-fx-border-color: #696969;
-fx-border-width:1;
}
同样,我们建议读者参考http://docs.oracle.com/javase/9/javafx/user-interface-tutorial/css-styles.htm【TODO:FINAL LINK】了解更多关于使用 CSS 和 JavaFX 图表的信息。
使用 xy 图表
XYChart类是一个抽象类,有七个直接已知的子类。这些类和PieChart类的区别在于XYChart有两个轴和一个可选的alternativeColumn或alternativeRow。这转化为下面的XYChart附加属性列表。
BooleanProperty alternativeColumnFillVisible
BooleanProperty alternativeRowFillVisible
ObjectProperty<ObservableList<XYChart.Series<X,Y>>> data
BooleanProperty horizontalGridLinesVisible
BooleanProperty horizontalZeroLineVisible
BooleanProperty verticalGridLinesVisible
BooleanProperty verticalZeroLineVisible
XYChart中的数据按顺序排列。这些系列如何呈现是特定于XYChart子类的实现的。一般来说,一个系列中的单个元素包含许多对。下面的例子使用了三种编程语言在未来市场份额的假设预测。我们从 2017 年的 Java、C 和 C++的 TIOBE 指数开始,并在 2020 年之前的每一年向它们添加随机值(在–2 和+2 之间)。Java 的结果(year,number)对构成了 Java 系列,这同样适用于 C 和 C++。因此,我们有三个系列,每个系列包含 10 双鞋。
PieChart和XYChart的主要区别在于XYChart中有一个 x 轴和一个 y 轴。当创建一个XYChart时,这些轴是必需的,这可以从下面的构造器中观察到。
XYChart (Axis<X> xAxis, Axis<Y> yAxis)
Axis类是一个抽象类,用两个子类扩展了Region(因此也扩展了Parent和Node):CategoryAxis和ValueAxis。CategoryAxis用于呈现String格式的标签,这可以从类定义中观察到:
public class CategoryAxis extends Axis<java.lang.String>
ValueAxis用于呈现代表Number的数据条目。它本身是一个抽象类,定义如下。
public abstract class ValueAxis <T extends java.lang.Number> extends Axis<T>
ValueAxis类有一个具体的子类,即NumberAxis:
public final class NumberAxis extends ValueAxis<java.lang.Number>
通过这些例子,这些Axis类之间的差异将变得清晰。我们现在展示一些不同的XYChart实现的例子,从ScatterChart开始。所有XYChart共有的一些特征也在ScatterChart部分进行了解释。
Note
因为Axis类扩展了Region,它们允许应用与任何其他Regions相同的 CSS 元素。这允许高度定制的Axis实例。
使用散点图
ScatterChart类的一个实例用于呈现数据,其中每个数据项被表示为二维区域中的一个符号。如前一节所述,我们将呈现一个包含三个数据系列的图表,代表 Java、C 和 C++的 TIOBE 指数的假设发展。我们首先展示一个简单实现的代码,并将其提炼为更有用的内容。
一个简单的实现
清单 6-5 显示了我们的应用程序使用ScatterChart的第一个实现。
package com.projavafx ;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ChartApp3 extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
ScatterChart scatterChart = new ScatterChart(xAxis, yAxis);
scatterChart.setData(getChartData());
primaryStage.setTitle("ScatterChart");
StackPane root = new StackPane();
root.getChildren().add(scatterChart);
primaryStage.setScene(new Scene(root, 400, 250));
primaryStage.show();
}
private ObservableList<XYChart.Series<Integer, Double>> getChartData() {
double javaValue = 15.57;
double cValue = 6.97;
double cppValue = 4.55;
ObservableList<XYChart.Series<Integer, Double>> answer = FXCollections.observableArrayList();
Series<Integer, Double> java = new Series<>();
Series<Integer, Double> c = new Series<>();
Series<Integer, Double> cpp = new Series<>();
for (int i = 2017; i < 2027; i++) {
java.getData().add(new XYChart.Data(i, javaValue));
javaValue = javaValue + 4 * Math.random() - 2;
c.getData().add(new XYChart.Data(i, cValue));
cValue = cValue + Math.random() - .5;
cpp.getData().add(new XYChart.Data(i, cppValue));
cppValue = cppValue + 4 * Math.random() - 2;
}
answer.addAll(java, c, cpp);
return answer;
}
}
Listing 6-5.First Implementation of Rendering Data in a ScatterChart
执行该应用程序会产生一个类似于图 6-6 所示的图形。
图 6-6。
The result of the naive implementation of the ScatterChart
虽然图表显示了所需的信息,但可读性不强。我们添加了一些增强功能,但是首先让我们更深入地看看代码的不同部分。
与PieChart示例类似,我们创建了一个单独的方法来获取数据。其中一个原因是,在现实世界的应用程序中,不太可能有静态数据。通过将数据检索隔离在一个单独的方法中,改变获取数据的方式变得更加容易。
单个数据点由一个实例XYChart.Data<Integer、Double>定义,用构造器XYChart.Data(Integer i, Double d)创建,其中参数定义如下。
i: Integer, representing a specific year (between 2017 and 2026)
d: Double, representing the hypothetical TIOBE index for the particular series in the year specified by I
局部变量javaValue、cValue和cppValue用于记录不同编程语言的分数。它们用 2017 年的实际值初始化。每一年,个人得分会以–2 到+2 之间的随机值递增或递减。数据点堆叠成一系列。在我们的例子中,我们有三个系列,每个系列包含 10 个XYChart.Data<Integer、Double>的实例。这些系列属于XYChart.Series<Integer, Double>类型。
通过调用将数据条目添加到相应的序列中
java.getData().add (...)
c.getData().add(...)
和
cpp.getData().add(...)
最后,所有序列都被添加到ObservableList<XYChart.Series<Integer, Double>>并返回。
应用程序的start()方法包含创建和呈现ScatterChart以及用从getChartData方法获得的数据填充它所需的功能。
Note
如前所述,我们可以在这里使用不同的模式与PieChart。我们在示例中使用了 JavaBeans 模式,但是我们也可以使用属性。
为了创建一个ScatterChart,我们需要创建一个xAxis和一个yAxis。在我们的第一个简单实现中,我们为此使用了两个NumberAxis实例:
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
除了调用下面的ScatterChart构造器,这个方法与PieChart的情况没有什么不同。
ScatterChart scatterChart = new ScatterChart(xAxis, yAxis);
改进简单的实现
查看图 6-5 时,首先观察到的一个现象是,一个系列中的所有数据图几乎都呈现在彼此的顶部。原因很清楚:x-Axis从 0 开始,到 2250 结束。默认情况下,NumberAxis会自动确定其范围。我们可以通过将autoRanging属性设置为 false,并为lowerBound和upperBound提供值来否决这种行为。如果我们用下面的代码片段替换原始示例中的xAxis的构造器,
NumberAxis xAxis = new NumberAxis();
xAxis.setAutoRanging(false);
xAxis.setLowerBound(2017);
xAxis.setUpperBound(2027);
结果输出将类似于图 6-7 所示。
图 6-7。
Defining the behavior of the xAxis
接下来,我们希望向图表添加一个标题,并且希望图例节点中的符号附近有名称。向图表添加标题与向PieChart添加标题没有什么不同,这是通过代码实现的:
scatterChart.setTitle("Speculations");
通过向三个XYChart.Series实例添加名称,我们向图例节点中的符号添加标签。getChartData方法的相关部分变成
Series<Integer, Double> java = new Series<>();
Series<Integer, Double> c = new Series<>();
Series<Integer, Double> cpp = new Series<>();
java.setName("java");
c.setName("C");
cpp.setName("C++");
在应用两个更改后再次运行应用程序会产生类似于图 6-8 所示的输出。
图 6-8。
ScatterChart with a title and named symbols
到目前为止,我们用一个NumberAxis来表示xAxis。因为年可以被表示为Number实例,这是可行的。但是,因为我们不对年份进行任何数值运算,并且因为连续数据条目之间的距离总是一年,所以我们也可以使用一个String值来表示这些信息。
我们现在修改代码,用一个CategoryAxis代替xAxis。将xAxis从NumberAxis更改为CategoryAxis也意味着getChartData()方法应该返回ObservableList<XYChart.Series<String, Double>>的一个实例,这意味着单个Series中的不同元素应该具有类型XYChart.Data<String, Double>。
在清单 6-6 中,原始代码被修改为使用CategoryAxis。
package projavafx ;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ChartApp7 extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
CategoryAxis xAxis = new CategoryAxis();
NumberAxis yAxis = new NumberAxis();
ScatterChart scatterChart = new ScatterChart(xAxis, yAxis);
scatterChart.setData(getChartData());
scatterChart.setTitle("speculations");
primaryStage.setTitle("ScatterChart example");
StackPane root = new StackPane();
root.getChildren().add(scatterChart);
primaryStage.setScene(new Scene(root, 400, 250));
primaryStage.show();
}
private ObservableList<XYChart.Series<String, Double>> getChartData() {
double javaValue = 15.57;
double cValue = 6.97;
double cppValue = 4.55;
ObservableList<XYChart.Series<String, Double>> answer = FXCollections.observableArrayList();
Series<String, Double> java = new Series<>();
Series<String, Double> c = new Series<>();
Series<String, Double> cpp = new Series<>();
java.setName("java");
c.setName("C");
cpp.setName("C++");
for (int i = 2017; i < 2027; i++) {
java.getData().add(new XYChart.Data(Integer.toString(i), javaValue));
javaValue = javaValue + 4 * Math.random() - .2;
c.getData().add(new XYChart.Data(Integer.toString(i), cValue));
cValue = cValue + 4 * Math.random() - 2;
cpp.getData().add(new XYChart.Data(Integer.toString(i), cppValue));
cppValue = cppValue + 4 * Math.random() - 2;
}
answer.addAll(java, c, cpp);
return answer;
}
}
Listing 6-6.Using CategoryAxis Instead of
NumberAxis
for the xAxis
运行修改后的应用程序会产生类似于图 6-9 的输出。
图 6-9。
Using a ScatterChart with a CategoryAxis on the xAxis
使用折线图
上一节中的示例导致数据条目由单个点或符号表示。通常,最好用一条线将点连接起来,因为这有助于观察趋势。JavaFX LineChart非常适合这一点。
用于LineChart的 API 与用于ScatterChart的 API 有许多共同的方法。事实上,我们可以重用清单 6-6 中的大部分代码,只需用LineChart替换ScatterChart的出现,用javafx.scene.chart.LineChart替换javafx.scene.chart.ScatterChart的导入。数据保持不变,所以我们只在清单 6-7 中显示新的start()方法。
public void start(Stage primaryStage) {
CategoryAxis xAxis = new CategoryAxis();
NumberAxis yAxis = new NumberAxis();
LineChart lineChart = new LineChart(xAxis, yAxis);
lineChart.setData(getChartData());
lineChart.setTitle("speculations");
primaryStage.setTitle("LineChart example");
StackPane root = new StackPane();
root.getChildren().add(lineChart);
primaryStage.setScene(new Scene(root, 400, 250));
primaryStage.show();
}
Listing 6-7.Using a LineChart Instead of a ScatterChart
运行该应用程序会产生如图 6-10 所示的输出。
图 6-10。
Using a LineChart for displaying trends
对于ScatterChart可用的大多数功能对于LineChart也是可用的。使用LineChart可以改变图例的位置,添加或删除标题,以及使用NumberAxis代替CategoryAxis。
使用条形图
一个BarChart能够呈现与一个ScatterChart和一个LineChart相同的数据,但是看起来不同。在BarChart中,重点通常是显示给定类别的不同系列之间的相对差异。在我们的例子中,这意味着我们关注 Java、C 和 C++的值之间的差异。
同样,我们不需要修改返回数据的方法。的确,一个BarChart需要一个CategoryAxis作为它的xAxis,我们已经修改了getChartData()方法来返回一个包含XYChart.Series<String, double>的ObservableList。从清单 6-6 开始,我们仅将出现的ScatterChart更改为BarChart,并获得清单 6-8 。
public void start(Stage primaryStage) {
CategoryAxis xAxis = new CategoryAxis();
NumberAxis yAxis = new NumberAxis();
BarChart barChart = new BarChart(xAxis, yAxis);
barChart.setData(getChartData());
barChart.setTitle("speculations");
primaryStage.setTitle("BarChart example");
StackPane root = new StackPane();
root.getChildren().add(barChart);
primaryStage.setScene(new Scene(root, 400, 250));
primaryStage.show();
}
Listing 6-8.Using a BarChart Instead of a ScatterChart
一旦我们用javafx.scene.chart.BarChart的导入替换了javafx.scene.chart.ScatterChart的导入,我们就可以构建并运行应用程序了。结果是一个类似于图 6-11 所示的BarChart。
图 6-11。
Using BarChart for highlighting differences between the values
虽然结果确实显示了各年数值之间的差异,但并不十分清楚,因为条形相当小。总场景宽度为 400 像素,没有太多空间来渲染大条形。但是,条形图 API 包含定义条形之间的内部间距和类别之间的间距的方法。在我们的例子中,我们希望条之间的间隙更小,例如一个像素。这是通过调用
barChart.setBarGap(1);
将这一行代码添加到 start 方法并重新运行应用程序会产生如图 6-12 所示的输出。
图 6-12。
Setting the gap between bars to one pixel
显然,这一行代码导致了可读性的巨大差异。
使用堆叠条形图
JavaFX 2.1 中增加了StackedBarChart。与BarChart一样,StackedBarChart在条形中显示数据,但是StackedBarChart不是将同一类别的条形一个接一个地显示,而是将同一类别的条形一个接一个地显示。这通常使得检查总数更容易。
通常,类别与数据系列中的常用键值相对应。因此,在我们的示例中,不同的年份(2017 年、2018 年、……2026 年)可以被视为类别。我们可以将这些类别添加到xAxis,如下所示:
IntStream.range(2017,2026).forEach(t -> xAxis.getCategories().add(String.valueOf(t)));
除此之外,惟一的代码变化是在代码和导入语句中用StackedBarChart替换了BarChart。这导致了清单 6-9 中的代码片段。
public void start(Stage primaryStage) {
CategoryAxis xAxis = new CategoryAxis();
IntStream.range(2017,2026).forEach(t -> xAxis.getCategories().add(String.valueOf(t)));
NumberAxis yAxis = new NumberAxis();
StackedBarChart stackedBarChart = new StackedBarChart(xAxis, yAxis, getChartData());
stackedBarChart.setTitle("speculations");
primaryStage.setTitle("StackedBarChart example");
StackPane root = new StackPane();
root.getChildren().add(stackedBarChart);
Scene scene = new Scene(root, 400, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
Listing 6-9.Using a StackedBarChart Instead of a ScatterChart
现在运行应用程序会产生如图 6-13 所示的输出。
图 6-13。
Rendering stacked bar chart plots using StackedBarChart
使用面积图
在某些情况下,填充点连线下方的区域是有意义的。虽然与在LineChart的情况下呈现相同的数据,但结果看起来不同。清单 6-10 包含了修改后的start()方法,它使用了一个AreaChart来代替原来的ScatterChart。和之前的修改一样,我们没有改变getChartData()方法。
public void start(Stage primaryStage) {
CategoryAxis xAxis = new CategoryAxis();
NumberAxis yAxis = new NumberAxis();
AreaChart areaChart = new AreaChart(xAxis, yAxis);
areaChart.setData(getChartData());
areaChart.setTitle("speculations");
primaryStage.setTitle("AreaChart example");
StackPane root = new StackPane();
root.getChildren().add(areaChart);
primaryStage.setScene(new Scene(root, 400, 250));
primaryStage.show();
}
Listing 6-10.Using an AreaChart Instead of a ScatterChart
运行该应用程序会产生如图 6-14 所示的输出。
图 6-14。
Rendering area plots using AreaChart
使用堆叠面积图
StackedAreaChart对于AreaChart就像StackedBarChart对于BarChart一样。StackedAreaChart不是显示单个区域,而是总是显示特定类别中值的总和。
将AreaChart更改为StackedAreaChart只需要更改一行代码和适当的导入语句。
AreaChart areaChart = new AreaChart(xAxis, yAxis);
必须替换为
StackedAreaChart areaChart = new StackedAreaChart(xAxis, yAxis);
应用此更改并运行应用程序会产生类似图 6-15 中的图表。
图 6-15。
Rendering area plots using AreaChart
使用气泡图
XYChart的最后一个实现很特殊。BubbleChart不包含已经不在XYChart类上的属性,但是它是当前 JavaFX Chart API 中唯一使用XYChart.Data类上的附加参数的直接实现。
我们首先修改清单 6-6 中的代码,使用BubbleChart代替ScatterChart。因为默认情况下,当xAxis上的跨度与yAxis上的跨度相差很大时,气泡会被拉伸,所以我们不用年,而是用一年的十分之一作为xAxis上的值。这样,我们在xAxis (10 年)上的跨度为 100 个单位,而在yAxis上的跨度约为 30 个单位。这或多或少也是我们图表的宽度和高度之比。因此,气泡相对来说是圆形的。
清单 6-11 包含了呈现一个BubbleChart的代码。
package com.projavafx.charts;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class ChartApp14 extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
yAxis.setAutoRanging(false);
yAxis.setLowerBound(0);
yAxis.setUpperBound(30);
xAxis.setAutoRanging(false);
xAxis.setAutoRanging(false);
xAxis.setLowerBound(20170);
xAxis.setUpperBound(20261);
xAxis.setTickUnit(10);
xAxis.setTickLabelFormatter(new StringConverter<Number>() {
@Override
public String toString(Number n) {
return String.valueOf(n.intValue() / 10);
}
@Override
public Number fromString(String s) {
return Integer.valueOf(s) * 10;
}
});
BubbleChart bubbleChart = new BubbleChart(xAxis, yAxis);
bubbleChart.setData(getChartData());
bubbleChart.setTitle("Speculations");
primaryStage.setTitle("BubbleChart example");
StackPane root = new StackPane();
root.getChildren().add(bubbleChart);
primaryStage.setScene(new Scene(root, 400, 250));
primaryStage.show();
}
private ObservableList<XYChart.Series<Integer, Double>> getChartData() {
double javaValue = 15.57;
double cValue = 6.97;
double cppValue = 4.55;
ObservableList<XYChart.Series<Integer, Double>> answer = FXCollections.observableArrayList();
Series<Integer, Double> java = new Series<>();
Series<Integer, Double> c = new Series<>();
Series<Integer, Double> cpp = new Series<>();
java.setName("java");
c.setName("C");
cpp.setName("C++");
for (int i = 20170; i < 20260; i = i + 10) {
double diff = Math.random();
java.getData().add(new XYChart.Data(i, javaValue));
javaValue = Math.max(javaValue + 2 * diff - 1, 0);
diff = Math.random();
c.getData().add(new XYChart.Data(i, cValue));
cValue = Math.max(cValue + 2 * diff - 1, 0);
diff = Math.random();
cpp.getData().add(new XYChart.Data(i, cppValue));
cppValue = Math.max(cppValue + 2 * diff - 1, 0);
}
answer.addAll(java, c, cpp);
return answer;
}
}
Listing 6-11.Using the BubbleChart
xAxis的范围从 201670 年到 20261 年,但是我们当然希望在轴上显示年份。这可以通过调用
xAxis.setTickLabelFormatter(new StringConverter<Number>() {
...
}
我们提供的StringConverter将我们使用的数字(如 20210)转换成Strings(如 2021),反之亦然。这样做,我们能够使用任何我们想要的量来计算气泡,并且仍然有一个格式化标签的好方法。运行该示例会产生如图 6-16 所示的图表。
图 6-16。
Using a BubbleChart with fixed radius
直到现在,我们都没有利用XYChart.Data的三参数构造器。除了我们已经熟悉的双参数构造器之外,
XYChart.Data (X xValue, Y yValue)
XYChart.Data也有一个三参数的构造器:
XYChart.Data (X xValue, Y yValue, Object extraValue)
extraValue参数可以是任何类型。这允许开发人员实现他们自己的XYChart子类,利用可以包含在单个数据元素中的附加信息。BubbleChart实现使用这个extraValue来决定应该渲染多大的气泡。
我们现在修改getChartData()方法来使用三参数构造器。xValue和yValue参数仍然与前面的清单中的相同,但是我们现在添加了第三个参数,表示即将到来的趋势。这个参数越大,下一年的涨幅就越大。参数越小,下一年跌幅越大。修改后的getChartData()方法如清单 6-12 所示。
private ObservableList<XYChart.Series<Integer, Double>> getChartData() {
double javaValue = 15.57;
double cValue = 6.97;
double cppValue = 4.55;
ObservableList<XYChart.Series<Integer, Double>> answer = FXCollections.observableArrayList();
Series<Integer, Double> java = new Series<>();
Series<Integer, Double> c = new Series<>();
Series<Integer, Double> cpp = new Series<>();
java.setName("java");
c.setName("C");
cpp.setName("C++");
for (int i = 20170; i < 20270; i = i+10) {
double diff = Math.random();
java.getData().add(new XYChart.Data(i, javaValue, 2*diff));
javaValue = Math.max(javaValue + 2*diff - 1,0);
diff = Math.random();
c.getData().add(new XYChart.Data(i, cValue,2* diff));
cValue = Math.max(cValue + 2*diff - 1,0);
diff = Math.random();
cpp.getData().add(new XYChart.Data(i, cppValue, 2*diff));
cppValue = Math.max(cppValue + 2*diff - 1,0);
}
answer.addAll(java, c, cpp);
return answer;
}
Listing 6-12.Using a Three-Argument Constructor
for XYChart.Data Instances
将该方法与清单 6-11 中的start()方法相结合,会产生如图 6-17 所示的输出。
图 6-17。
Adding variations in the size of the Bubbles
摘要
JavaFX Chart API 为不同的图表类型提供了许多现成的实现。每一个实现都有不同的目的,开发人员可以选择最合适的Chart。
通过应用 CSS 规则或使用特定于Chart的方法或属性,可以修改Chart并为特定的应用程序进行调优。
如果您需要一个更加定制化的Chart,您可以扩展抽象的Chart类并利用该类上的现有属性,或者如果您的图表需要两个轴,您可以扩展抽象的XYChart类。
资源
有关 JavaFX Chart API 的更多信息,请参考以下资源:
http://docs.oracle.com/javase/8/javafx/user-interface-tutorial/charts.htmhttp://docs.oracle.com/javase/8/javafx/user-interface-tutorial/css-styles.htm
七、连接到企业服务
专家是在一个狭窄的领域里犯了所有可能犯的错误的人。—尼尔斯·玻尔
客户端应用程序可能非常令人兴奋,但通常它们不会生活在孤立的环境中。典型的客户端应用程序以某种方式与其他应用程序、后端组件和云环境交换数据和功能。这对客户端应用程序的开发提出了新的要求。
到目前为止,我们已经解释了如何使用 JavaFX 平台来呈现信息和交互操作数据。在本章中,我们简要概述了将 JavaFX 应用程序与企业系统集成的可用选项,然后继续介绍该过程的一些具体示例。
我们的示例旨在演示 JavaFX 应用程序如何轻松地访问 REST 资源,然后将响应(来自 JSON 或 XML 格式)转换为 JavaFX 控件可以理解的格式。作为我们的示例外部数据源,堆栈交换 API 是理想的,因为它们是公开可用的,易于理解,并且在互联网上广泛使用。
前端和后端平台
JavaFX 通常被认为是一个前端平台。虽然这种说法没有公平对待 JavaFX 平台中与 UI 无关的 API,但大多数 JavaFX 应用程序确实关注“内容”的丰富和交互式可视化。
Java 的一大优点是,一种语言可以在多种设备、桌面和服务器上使用。创建 JavaFX 核心的 Java 语言也是 Java 平台企业版(Java EE)的基础核心。
Java 平台是企业应用程序的头号开发平台。JavaFX 平台提供了丰富的交互式 UI,与运行在 Java 平台上的企业应用程序相结合,创造了巨大的可能性。为此,JavaFX 应用程序和 Java 企业应用程序必须交换数据。交换数据可以以多种方式发生,并且取决于需求(从前端以及从后端);一种方式可能比另一种方式更合适。
基本上,有两种不同的方法:
图 7-2。
JavaFX application communicates with enterprise components on a remote server
图 7-1。
JavaFX and enterprise components on a single system
- JavaFX 应用程序可以利用这样一个事实,即它运行在与典型企业应用程序相同的基础设施上,并且可以与这些企业组件深度集成。如图 7-1 所示。
- JavaFX 应用程序运行在一个相对简单的 Java 平台上,使用 Java 企业组件已经支持的标准协议与企业服务器交换数据。如图 7-2 所示。
第一种方法已被提及并简要涉及,但本章的重点是第二种方法,即 JavaFX 客户端与远程服务器上的 Java 企业组件进行通信。
没有所谓的最佳方法,因为它实际上取决于环境和用例。一个典型的 Java 客户端应用程序运行在比今天的后端服务器和云环境功能更少的硬件上,在这种情况下,不推荐第一种方法。然而,很明显,在一些情况下,资源(CPU、集群、可伸缩性)在客户端系统上是广泛可用的,在这种情况下,这种方法肯定是可以考虑的。
这里还应该强调的是,只要使用标准的、定义良好的协议(例如 SOAP/REST),就很有可能将 JavaFX 应用程序连接到非 Java 后端应用程序。客户机和服务器之间的分离确实允许在客户机和服务器上使用不同的编程语言。
在同一环境中合并 JavaFX 和 Java 企业模块
JavaFX 9 构建在 Java 平台标准版之上。因此,这个平台提供的所有功能都可以在 JavaFX 9 中使用。两个最流行的 Java 企业框架——Java 平台企业版和 Spring 框架——也构建在 Java 平台标准版之上。因此,JavaFX 应用程序可以与使用 Java Platform,Enterprise Edition 的应用程序或使用 Spring Framework 构建的应用程序生活在同一个环境中。
JavaFX 开发人员因此可以使用他或她喜欢的企业工具来创建应用程序。这样做有许多好处。企业组件提供的工具允许开发人员专注于特定的领域层,同时保护他们免受数据库资源和事务的影响。
Java 是企业环境中的一个流行平台,许多公司、组织和个人已经开发了许多企业组件和库。
Java 平台企业版是由通过 Java 社区进程(JCP)计划标准化的规范定义的。对于不同的组成部分,单独的 Java 规范请求(JSR)被归档。
这些单独的 JSR 中的大多数都是由许多公司实现的,并且这些实现通常被组合到一个产品中。典型的企业框架实现一个或多个 JSR,并且它们可能包含额外的特定于产品的功能。
在这些规范中最流行的实现中,有 Tomcat/TomEE、Hibernate、JBoss/WildFly、GlassFish、Payara、WebLogic 和 WebSphere。许多产品实现了所有的 JSR,这些产品被称为 Java 平台企业版的实现,通常被称为 Java EE 平台或应用服务器。
另一个流行的 Java 企业框架 Spring Framework 包含了 Java 平台企业版中定义的许多 JSR 的实现,并添加了更多特定的组件和 API。
从技术上讲,JavaFX 平台中没有阻止使用 Java 企业组件的限制。很有可能在客户机系统上运行 Java 企业应用服务器,或者在同一个客户机系统上执行 Spring 框架应用程序。利用 Java 企业应用服务器或 Spring 框架的应用程序也可以包含 JavaFX 代码。
然而,企业开发在许多方面不同于客户端开发:
- 企业基础设施正在向云转移。特定任务(例如,存储、邮件等。)外包给“云”中提供特定功能的组件。企业服务器通常位于云环境中,允许与云组件进行快速无缝的交互。
- 就资源需求而言,企业系统侧重于计算资源(CPU、缓存和内存),而台式计算机和笔记本电脑侧重于视觉资源(例如,图形硬件加速)。
- 启动时间在服务器中几乎不成问题,但在许多桌面应用程序中却至关重要。此外,服务器应该是 24/7 全天候运行的,而大多数客户机并不是这样。
- 部署和生命周期管理通常特定于服务器产品或客户端产品。升级服务器或服务器软件通常是一个乏味的过程。必须尽量减少停机时间,因为客户端应用程序可能会打开与服务器的连接。部署客户端应用程序可以通过多种方式进行,例如通过独立、自包含的应用程序或 Java 网络启动协议(JNLP)。
- 企业开发使用了许多在客户端开发中有用的模式(例如,控制反转、基于容器的初始化),但是这些模式通常需要与传统客户端不同的架构。
使用 JavaFX 调用远程(Web)服务
企业组件通常通过 web 资源来访问。一些规范清楚地描述了基于 web 的框架应该如何与企业组件交互以呈现信息。然而,还有其他规范允许从非 web 资源访问企业组件(用 Java 或其他语言编写)。因为那些规范允许企业开发和任何其他开发之间的解耦,所以它们已经被许多涉众定义了。
1998 年,简单对象访问协议(SOAP)由微软发明,随后被用作 Java 应用程序和。NET 应用程序。SOAP 是基于 XML 的,当前的版本 1.2 是 2003 年 W3C 推荐的。Java 提供了许多工具,允许开发人员与 SOAP 交换数据。
尽管 SOAP 功能强大且可读性相对较好,但通常被认为相当冗长。随着 mashups 和提供特定功能的简单服务的兴起,出现了一种新的架构风格:表述性状态转移(REST)。REST 允许服务器和客户端开发人员以一种松散耦合且更加简化的方式交换数据,其中协议可以是 XML、JSON、Atom 或任何其他格式。
在下一节中,我们将展示一些使用 REST APIs 在 JavaFX 客户端应用程序和服务器或云组件之间建立通信的例子。虽然这种方法将显示数据是如何传输的,但它仍然需要一些锅炉板代码。在本节之后,我们将讨论一些框架,这些框架使得与企业组件的连接对 JavaFX 开发人员来说更加透明。
休息
在互联网上可以找到大量关于 REST 和基于 REST 的 web 服务的资源和文档。基于 REST 的 web 服务公开了许多可以使用 HTTP 协议访问的 URIs。通常,不同的 HTTP request方法(get、post、put、delete)用于表示对资源的不同操作。
基于 REST 的 web 服务可以使用标准的 HTTP 技术来访问,Java 平台附带了许多 API(主要在java.io和java.net中),方便了对基于 REST 的 web 服务的访问。
JavaFX 在 Java 平台(Standard Edition 9)之上编写的主要优势之一是能够在 JavaFX 应用程序中使用所有这些 API。这就是我们在本章第一个例子中所做的。我们展示了如何使用 Java APIs 来消费基于 REST 的 web 服务,以及如何将结果集成到 JavaFX 应用程序中。
接下来,我们将展示如何利用 JavaFX APIs 来避免常见的缺陷(例如,无响应的应用程序、无动态更新等)。).最后,我们简要概述了使 JavaFX 开发人员能够轻松访问基于 REST 的 web 服务的第三方库。
设置应用程序
首先,我们为样本创建框架。我们将使用 Stack Exchange 提供的 API。Stack Exchange 网络是一个论坛集群,每个论坛位于一个特定的域中,在这里,问题和答案以这样一种方式组合在一起,即来自最受信任的用户的“最佳”答案会出现在顶部。Java 开发人员可能对 Stack Overflow 很熟悉,它是 Stack Exchange 中的第一个站点,提供了大量与 IT 相关的问题和答案。
Stack Exchange 提供的 REST APIs 在 https://api.stackexchange.com 有很好的描述。我们的目标不是探索 Stack Exchange 和相应 API 提供的所有可能性,因此感兴趣的读者可以参考网站上的文档。
在本章的示例中,我们希望将问题的作者、问题的标题以及提出问题的日期可视化。
最初,我们用带有 getters 和 setters 的 Java 对象来表示一个问题。这显示在清单 7-1 中。
package projavafx;
public class Question {
private String owner;
private String question;
private long timestamp;
public Question () {
}
public Question (String o, String q, long t) {
this.owner = o;
this.question = q;
this.timestamp = t;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getQuestion() {
return question;
}
public void setQuestion(String question) {
this.question = question;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
Listing 7-1.
Question Class
我们的Question类有两个构造器。在下面的一个例子中需要零参数构造器,我们稍后再回到这个例子。在其他示例中,为了方便起见,使用了带三个参数的构造器。
在清单 7-2 中,我们展示了如何显示问题。在第一个例子中,问题不是通过栈交换 API 获得的,但是它们在这个例子中是硬编码的。
package projavafx;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class StackOverflowApp1 extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
ListView<Question> listView = new ListView<>();
listView.setItems(getObservableList());
StackPane root = new StackPane();
root.getChildren().add(listView);
Scene scene = new Scene(root, 500, 300);
primaryStage.setTitle("StackOverflow List");
primaryStage.setScene(scene);
primaryStage.show();
}
ObservableList<Question> getObservableList() {
ObservableList<Question> answer = FXCollections.observableArrayList();
long now = System.currentTimeMillis();
long yesterday = now - 1000 * 60 * 60 * 24;
Question q1 = new Question("James", "How can I call a REST service?", now);
Question q2 = new Question("Stephen", "Does JavaFX work on Android?", yesterday);
answer.addAll(q1, q2);
return answer;
}
}
Listing 7-2.Framework for Rendering Questions in a
ListView
如果您已经阅读了前面的章节,这段代码并没有包含任何新的内容。我们创建一个ListView,将其添加到一个StackPane,创建一个Scene,并渲染Stage。
用包含Question s 的ObservableList填充ListView,这个ObservableList是通过调用getObservableList()方法获得的。在下面的示例中,我们修改了这个方法,并展示了如何从堆栈交换 API 中检索Question s。
Note
getObservableList返回一个ObservableList。ListView自动观察这个ObservableList。因此,ObservableList的变化会立即呈现在ListView控件中。在后面的示例中,我们将利用这一功能。
运行该示例会产生如图 7-3 所示的窗口。
图 7-3。
The result of the first example
结果窗口包含一个有两个条目的ListView。这些条目对应于清单 7-2 底部的getObservableList()方法中创建的两个问题。
窗口中显示的有关问题的信息不是很有用。事实上,我们告诉ListView它应该显示一些Question的实例,但是我们没有告诉它们应该如何显示。后者可以通过指定一个CellFactory来实现。在这一章中,我们的目标不是创建一个花哨的 UI;相反,我们希望展示如何检索数据并在 UI 中呈现这些数据。因此,我们简要地展示了开发人员如何通过使用CellFactory概念来改变数据的可视化。关于我们在示例中使用的 UI 控件的概述(ListView和TableView,请参考第六章。
在清单 7-3 中,我们创建了一个QuestionCell类,它扩展了ListCell并定义了如何布局一个单元格。
package projavafx;
import java.text.SimpleDateFormat;
import java.util.Date;
import javafx.scene.control.ListCell;
public class QuestionCell extends ListCell<Question> {
static final SimpleDateFormat sdf = new SimpleDateFormat ("dd-MM-YY");
@Override
protected void updateItem(Question question, boolean empty){
super.updateItem(question, empty);
if (empty) {
setText("");
} else {
StringBuilder sb= new StringBuilder();
sb.append("[").append(sdf.format(new Date(question.getTimestamp()))).append("]")
.append(" ").append(question.getOwner()+": "+question.getQuestion());
setText(sb.toString());
}
}
}
Listing 7-3.Define
QuestionCell
当一个单元格项需要更新时,我们告诉它显示一些包含方括号中的时间戳的文本,后面是作者和问题的标题。接下来,ListView需要被告知它应该呈现QuestionCell s。我们通过调用ListView.setCellFactory()方法来做到这一点,并提供一个 lambda 表达式,该表达式在被调用时创建一个新的QuestionCell。在清单 7-4 中,我们展示了我们的StackOverflowApplication的启动方法的修改版本。
public void start(Stage primaryStage) {
ListView<Question> listView = new ListView<>();
listView.setItems(getObservableList());
listView.setCellFactory(l -> new QuestionCell());
StackPane root = new StackPane();
root.getChildren().add(listView);
Scene scene = new Scene(root, 500, 300);
primaryStage.setTitle("StackOverflow List");
primaryStage.setScene(scene);
primaryStage.show();
}
Listing 7-4.Use CellFactory on the ListView
如果我们现在运行应用程序,输出如图 7-4 所示。
图 7-4。
The result of adding a QuestionCell
对于ListView条目中的每个问题,现在的输出是我们所期望的。我们可以用CellFactories做更多的事情(例如,我们可以使用图形而不仅仅是文本),但是这超出了本章的范围。
我们现在用通过堆栈交换 API 获得的真实信息替换硬编码的问题。
使用堆栈交换 API
栈交换网( http://stackexchange.com )允许第三方开发者使用基于 REST 的接口浏览访问问题和答案。Stack Exchange 维护了许多基于 REST 的 API,但是在我们的例子中,我们仅限于 Stack Exchange Search API。关于此 API 的更多信息,请访问 http://api.stackexchange.com/docs 。
资源 URL——REST 服务的端点——非常简单:
http://api.stackexchange.com/2.2/search
这里可以提供许多查询参数。我们将只使用两个参数,感兴趣的读者可以参考栈交换文档,了解关于其他参数的信息。
site:指定您想要搜索的域,在我们的例子中是“stackoverflow"。tagged:分号分隔的标签列表。我们希望搜索所有标有javafx"的问题。
组合这两个参数会导致以下 REST 调用:
http://api.stackexchange.com/2.2/search?tagged=javafx&site=stackoverflow
当在浏览器中执行这个 REST 调用,或者使用命令工具(例如 cURL)时,结果类似于清单 7-5 中的 JSON-text。
{
"items": [
{
"tags": [
"java",
"sorting",
"javafx",
"tableview"
],
"owner": {
"reputation": 132,
"user_id": 578518,
"user_type": "registered",
"accept_rate": 84,
"profile_image": "https://www.gravatar.com/avatar/bdbee99c377a7063b24e09e7121fb1ab?s=128&d=identicon&r=PG",
"display_name": "Rps",
"link": "http://stackoverflow.com/users/578518/rps"
},
"is_answered": false,
"view_count": 7,
"answer_count": 1,
"score": 0,
"last_activity_date": 1397845222,
"creation_date": 1397844823,
"last_edit_date": 1397845143,
"question_id": 23159737,
"link": "http://stackoverflow.com/questions/23159737/javafx-tableview-ordered-by-date",
"title": "javafx Tableview ordered by date"
},
...
,"has_more":true
,"quota_max":300,
"quota_remaining":290
}
Listing 7-5.
JSON Response
Obtained from the Stack Exchange Search API
堆栈交换 API 只提供基于 JSON 的响应。许多 web 服务以 XML 的形式提供信息,还有一些同时提供 JSON 和 XML。因为我们还想展示如何处理 XML 响应,所以我们为 Stack Exchange REST 服务创建了自己的基于 XML 的输出。这个 XML 响应不是调用外部 REST 端点,而是通过读取本地文件获得的。
我们自定义的 XML 响应如清单 7-6 所示。
<?xml version="1.0" encoding="UTF-8"?>
<items>
<item>
<tags>
<tag>java</tag>
<tag>sorting</tag>
<tag>javafx</tag>
<tag>tableview</tag>
</tags>
<owner>Rps</owner>
<creation_date>1397844823</creation_date>
<title>javafx Tableview ordered by date</title>
<item>
</items>
Listing 7-6.Artificial XML Response
Obtained from the Stack Exchange Search API
尽管 JSON 响应中的数据包含与 XML 响应中的数据相同的信息,但是格式当然是非常不同的。JSON 和 XML 都在互联网上广泛使用,大量 web 服务以这两种格式提供响应。
根据用例和开发人员的不同,一种格式可能比另一种格式更受青睐。一般来说,JavaFX 应用程序应该能够使用这两种格式,因为它们必须与第三方数据连接,JavaFX 开发人员不能总是影响后端使用的数据格式。
Note
许多应用程序允许多种格式,通过指定 HTTP“Accept”头,客户端可以在不同的格式之间进行选择。
在下一个例子中,我们将展示如何检索和解析栈交换搜索 API 中使用的 JSON 响应。
JSON 响应格式
JSON 是互联网上非常流行的格式,尤其是在用 JavaScript 解析输入数据的 web 应用程序中。JSON 数据相当紧凑,或多或少具有可读性。
Java 中有许多工具可以读写 JSON 数据。截至 2013 年 6 月,当 Java 企业版 7 发布时,Java 中有一个描述如何读写 JSON 数据的标准规范。本 Java 规范定义为 JSR 353,更多信息可在 www.jcp.org/en/jsr/detail?id=353 .获取
JSR 353 only defines a specification, and an implementation is still needed to do the actual work. In our examples, we will use JSONP, which is the Reference Implementation of JSR 353\. This Reference Implementation can be found at https://jsonp.java.net/ 。不过,我们鼓励读者尝试他们最喜欢的 JSR 353 实现。
尽管 JSR 353 是 Java 企业版的一部分,但是参考实现也可以在 Java 标准版环境中工作。没有外部依赖性。
我们现在用通过 Stack Exchange REST API 获得的真题替换包含两个假题的硬编码列表。我们保留现有的代码,但是我们修改了清单 7-7 中所示的getObservableList()方法。
ObservableList<Question> getObservableList() throws IOException {
String url = "http://api.stackexchange.com/2.2/search?tagged=javafx&site=stackoverflow";
URL host = new URL(url);
JsonReader jr = Json.createReader(new GZIPInputStream(host.openConnection().getInputStream()));
JsonObject jsonObject = jr.readObject();
JsonArray jsonArray = jsonObject.getJsonArray("items");
ObservableList<Question> answer = FXCollections.observableArrayList();
jsonArray.iterator().forEachRemaining((JsonValue e) -> {
JsonObject obj = (JsonObject) e;
JsonString name = obj.getJsonObject("owner").getJsonString("display_name");
JsonString quest = obj.getJsonString("title");
JsonNumber jsonNumber = obj.getJsonNumber("creation_date");
Question q = new Question(name.getString(), quest.getString(), jsonNumber.longValue() * 1000);
answer.add(q);
});
return answer;
}
Listing 7-7.Obtain Questions via the Stack Exchange REST API
, JSON Format, and Parse the JSON
在深入研究代码之前,我们在图 7-5 中展示了修改后的应用程序的结果。
图 7-5。
The result of the StackOverflowApplication retrieving JSON data
清单 7-7 中的代码可以分为四个部分:
- 调用 REST 端点。
- 获取原始 JSON 数据。
- 将每个项目转换成一个问题。
- 将
Question添加到结果中。
调用 REST 端点非常简单:
String url = "http://api.stackexchange.com/2.2/search?tagged=javafx&site=stackoverflow";
URL host = new URL(url);
JsonReader jr = Json.createReader(new GZIPInputStream(host.openConnection().getInputStream()));
首先,我们创建一个指向所需位置的 URL 对象。接下来,我们打开到该位置的连接。因为 Stack Exchange 以压缩数据的形式发送数据,所以我们使用从连接中获得的InputStream打开一个GZIPInputStream。我们将这个GZIPInputStream作为Json.createReader()方法中的InputStream参数。
我们现在有了一个 JSON 阅读器,它使用我们想要的数据。从这个 JSON 阅读器手动提取 Java 对象需要针对特定情况的特定代码。
Note
我们也可以使用 JSON 解析器来代替 JSON 阅读器。我们并不打算提供详尽的 JSON 解析指南。我们只是试图展示如何将 JSON 数据转换成我们特定用例的 Java 对象。你可以很容易地在网上找到许多关于 JSON 的教程。
在清单 7-5 中,我们观察到问题在一个名为 items 的数组中,从左边的方括号(``)开始。我们可以使用下面的语句获得这个 JSON 数组:
JsonArray jsonArray = jsonObject.getJsonArray("items");
接下来,我们需要迭代所有这些元素。对于我们遇到的每个项目,我们希望创建一个Question实例。
遍历数组元素可以使用
jsonArray.iterator().forEachRemaining((JsonValue e) -> {
...
}
为了创建一个Question实例,我们需要获得问题的作者姓名、标题和创建日期。Java JSON API 为此提供了一种标准的方法:
JsonObject obj = (JsonObject) e;
JsonString name = obj.getJsonObject("owner").getJsonString("display_name");
JsonString quest = obj.getJsonString("title");
JsonNumber jsonNumber = obj.getJsonNumber("creation_date");
最后,我们需要基于这些信息创建一个Question实例,并将其添加到我们将返回的ObservableList实例中:
Question q = new Question(name.getString(), quest.getString(), jsonNumber.longValue() * 1000);
answer.add(q);
这个例子表明,检索和读取从 REST 端点获得的 JSON 数据,并将结果转换成一个ListView是非常容易的。在下一节中,我们将演示 XML 响应的类似过程。
XML 响应格式
XML 格式在 Java 平台中被广泛使用。因此,Java 中基于 XML 的操作的标准化在几年前就开始了。Java 平台标准版内置了许多 XML 工具,我们可以在 JavaFX 中使用这些 API 和工具,而无需任何外部依赖。在本节中,我们首先使用一个 DOM 处理器来解析我们人工构建的 XML 响应。接下来,我们使用 JAXB 标准来自动获取 Java 对象。
将我们的应用程序从 JSON 输入更改为 XML 输入只需要更改getObservableList方法。新的实现如清单 [7-8 所示。
ObservableList<Question> getObservableList() throws IOException, ParserConfigurationException, SAXException {
ObservableList<Question> answer = FXCollections.observableArrayList();
InputStream inputStream = this.getClass().getResourceAsStream("/stackoverflow.xml");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(inputStream);
NodeList questionNodes = doc.getElementsByTagName("item");
int count = questionNodes.getLength();
for (int i = 0; i < count; i++) {
Question question = new Question();
Element questionNode = (Element) questionNodes.item(i);
NodeList childNodes = questionNode.getChildNodes();
int cnt2 = childNodes.getLength();
for (int j = 0; j < cnt2; j++) {
Node me = childNodes.item(j);
String nodeName = me.getNodeName();
if ("creation_date".equals(nodeName)) {
question.setTimestamp(Long.parseLong(me.getTextContent()));
}
if ("owner".equals(nodeName)) {
question.setOwner(me.getTextContent());
}
if ("title".equals(nodeName)) {
question.setQuestion(me.getTextContent());
}
}
answer.add(question);
}
return answer;
}
Listing 7-8.Obtaining Questions from the XML-Based Response
同样,本节的目标不是给出 DOM APIs 的全面概述。互联网上有大量的资源提供关于 XML 的信息,特别是关于 DOM 的信息。
为了能够编译清单 7-8 中的代码,必须添加以下导入语句。
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
在我们详细讨论代码之前,我们在图 7-6 中展示了这个例子的输出。
图 7-6。
The result of the question application using XML response
清单 7-8 中的代码与清单 7-7 中的代码有一些相似之处。在这两种情况下,我们处理文本格式(JSON 或 XML)的可用数据,并将数据转换成问题实例。在清单 7-8 中,DOM 方法用于检查收到的响应。
使用下面的代码获得了一个org.w3c.dom.Document实例。
InputStream inputStream = this.getClass().getResourceAsStream("/stackoverflow.xml");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(inputStream);
在这种情况下,我们基于一个InputStream创建一个Document。InputStream是从人为创建的文件中获得的。我们还可以从一个URLConnection创建一个InputStream,并将这个InputStream传递给db.parse()方法。更简单的是,DocumentBuilder.parse方法还接受一个包含 REST 端点 URL 的String参数。
这表明,尽管我们在这种情况下使用的是包含问题的静态文件,但在使用真正的 REST 端点时,我们可以轻松地使用相同的代码。
现在可以查询结果Document。从清单 7-6 中显示的 XML 响应中,我们了解到各个问题都包含在名为“item”的 XML 元素中。我们使用下面的代码来获取这些 XML 元素的列表。
NodeList questionNodes = doc.getElementsByTagName("item");
然后我们遍历这个列表,通过检查各个 XML 元素中的childNodes来获得特定于问题的字段。最后,我们将产生的问题添加到名为 answer 的Question对象的ObservableList中。
这种方法相当简单,但是我们仍然需要做一些手工的 XML 解析。尽管这考虑到了灵活性,但是随着数据结构复杂性的增加,解析变得更加困难和容易出错。
幸运的是,Java 标准版 API 包含将 XML 直接转换成 Java 对象的工具。这些 API 的规范由 JAXB 标准定义,可以在javax.xml.bind包中获得。将 XML 数据转换成 Java 对象的过程称为解组。
我们现在修改我们的示例,并混合使用 DOM 解析和 JAXB 解组。同样,我们只改变了getObservableList()方法。修改后的实现如清单 7-9 所示。
ObservableList<Question> getObservableList() throws IOException, ParserConfigurationException, SAXException {
ObservableList<Question> answer = FXCollections.observableArrayList();
InputStream inputStream = this.getClass().getResourceAsStream("/stackoverflow.xml");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(inputStream);
NodeList questionNodes = doc.getElementsByTagName("item");
int count = questionNodes.getLength();
for (int i = 0; i < count; i++) {
Element questionNode = (Element) questionNodes.item(i);
DOMSource source = new DOMSource(questionNode);
final Question question = (Question) JAXB.unmarshal(source, Question.class);
answer.add(question);
}
return answer;
}
Listing 7-9.Combining XML Parsing and JAXB
这种方法与清单 7-8 中使用的方法的唯一区别是对单个问题的解析。我们使用 JAXB 中的 unmarshal 方法,而不是使用 DOM 解析来获取各个问题的特定字段。JAXB 规范允许大量的灵活性和配置,而JAXB.unmarshal方法只是一种方便的方法。但是,在很多情况下,这种方法就足够了。JAXB.unmarshal方法有两个参数:输入源和作为转换结果的类。
我们希望将 XML 源转换成我们的Question类的实例,但是 JAXB 框架如何知道如何映射字段呢?在许多情况下,映射很简单,不需要修改现有的代码,但是在其他情况下,映射稍微复杂一些。很好,有一个完整的带注释的包,我们可以用它来帮助 JAXB 确定 XML 和 Java 对象之间的转换。
为了让清单 7-9 中的代码工作,我们对Question类做了一些小的修改。清单 7-10 显示了Question类的新代码。
package projavafx;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
@XmlAccessorType(XmlAccessType.FIELD)
public class Question {
private String owner;
@XmlElement(name = "title")
private String question;
@XmlElement(name = "creation_date")
private long timestamp;
public Question(String o, String q, long t) {
this.owner = o;
this.question = q;
this.timestamp = t;
}
public Question() {
}
/**
* @return the owner
*/
public String getOwner() {
return owner;
}
/**
* @param owner the owner to set
*/
public void setOwner(String owner) {
this.owner = owner;
}
/**
* @return the question
*/
public String getQuestion() {
return question;
}
/**
* @param question the question to set
*/
public void setQuestion(String question) {
this.question = question;
}
/**
* @return the timestamp
*/
public long getTimestamp() {
return timestamp;
}
/**
* @param timestamp the timestamp to set
*/
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
Listing 7-10.
Question Class with JAXB Annotations
我们向原始的Question类添加了三个注释。首先,我们用
@XmlAccessorType(XmlAccessType.FIELD)
这个注释告诉 JAXB 框架将 XML 数据映射到这个类的字段上,而不是映射到这个类的JavaBean属性(getter/setter 方法)上。第二个和第三个注释被添加到question字段和timeStamp字段:
@XmlElement(name = "title")
private String question;
@XmlElement(name = "creation_date")
private long timestamp;
这表明question字段对应于名为“title”的 XML 元素,时间戳字段对应于名为"creation_date"的 XML 元素。事实上,如果我们查看清单 7-6 ,它显示问题在名为“标题”的元素中,时间戳在名为“creation_date"的元素中。我们必须指示 JAXB 运行时用我们的时间戳字段映射这个元素,这就是我们对@XmlElement注释所做的。
使用 JAXB 注释可以很容易地将 XML 问题元素转换成单独的Question实例,但是在我们的主类中仍然有一些手工的 XML 处理。但是,我们可以完全去掉手动的XMLParsing,将整个 XML 响应转换成一个 Java 对象。这样做,getObservableList()方法变得非常简单,如清单 7-11 所示。
ObservableList<Question> getObservableList() {
InputStream inputStream = this.getClass().getResourceAsStream("/stackoverflow.xml");
QuestionResponse response = JAXB.unmarshal(inputStream, QuestionResponse.class);
return FXCollections.observableArrayList(response.getItem());
}
Listing 7-11.Parsing Incoming XML Data Using JAXB
在这个例子中,我们使用 JAXB 将 XML 响应转换成一个QuestionResponse实例,然后通过这个QuestionResponse实例获得问题。注意,按照方法签名的要求,我们将问题从常规的List对象转换为ObservableList对象。稍后,我们将展示一个无需进行额外转换的示例。
QuestionResponse类有两个目标:将 XML 响应映射到 Java 对象上,并使问题项作为Question实例的List可用。这是通过清单 7-12 中的代码实现的。
package projavafx;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="items")
@XmlAccessorType(XmlAccessType.FIELD)
public class QuestionResponse {
private List<Question> questions;
public List<Question> getQuestions() {
return questions;
}
public void setQuestions(List<Question> questions) {
this.questions = questions;
}
}
Listing 7-12.
QuestionResponse Class
, Enabling Conversion Between XML Response and Java Objects
QuestionResponse类本身有两个注释。我们已经讨论了以下内容:
@XmlAccessorType(XmlAccessType.FIELD)
这个注释表明这个类对应于 XML 结构中的一个名为"items"的根对象。
@XmlRootElement(name="items")
这确实对应于我们在清单 7-6 中创建的 XML 响应的语法。
前面的示例展示了如何使用 Java 2 平台标准版中的现有技术从 web 服务获取数据,并将这些数据注入 JavaFX 控件中。我们现在修改示例代码,以利用 JavaFX 平台的一些特定特性。
异步处理
到目前为止,示例的一个主要问题是它们在数据检索和解析过程中阻塞了 UI。在许多现实情况下,这是不可接受的。由于网络或服务器问题,对外部 web 服务的调用可能会比预期时间长。即使外部调用很快,暂时没有响应的 UI 也会降低应用程序的整体质量。
幸运的是,JavaFX 平台允许并发和异步任务。任务、工人和服务的概念在第七章中讨论过。在这一节中,我们将展示如何在访问 web 服务时利用javafx.concurrent包。我们还利用了这样一个事实,即ListView监视包含其项目的ObservableList。
基本思想是,当创建ListView时,我们立即返回一个空的ObservableList,同时在后台线程中检索数据。一旦我们检索并解析了数据,我们将它添加到ObservableList,结果将立即在ListView中可见。
清单 7-13 显示了这个例子的主类。我们从清单 7-7 中的代码开始,在那里我们使用一个对栈交换 API 的 REST 请求获得了 JSON 格式的问题。不过,经过一些小的修改,我们也可以使用 XML 响应。
package projavafx;
import java.io.IOException;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class StackOverflow4 extends Application {
@Override
public void start(Stage primaryStage) throws IOException {
ListView<Question> listView = new ListView<>();
listView.setItems(getObservableList());
listView.setCellFactory(l -> new QuestionCell());
StackPane root = new StackPane();
root.getChildren().add(listView);
Scene scene = new Scene(root, 500, 300);
primaryStage.setTitle("StackOverflow List");
primaryStage.setScene(scene);
primaryStage.show();
System.out.println (« Done with the setup ») ;
}
ObservableList<Question> getObservableList() throws IOException {
String url = "http://api.stackexchange.com/2.2/search?order=desc&sort=activity&tagged=javafx&site=stackoverflow";
Service<ObservableList<Question>> service = new QuestionRetrievalService(url);
ObservableList<Question> answer = FXCollections.observableArrayList();
service.stateProperty().addListener(new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
System.out.println("value is now "+service.getState());
if (service.getState().equals(Worker.State.SUCCEEDED)) {
answer.addAll(service.getValue());
}
}
});
System.out.println("START SERVICE = "+service.getTitle());
service.start();
return answer;
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Listing 7-13.Use a Background Thread for Retrieving Question
ListView
main 方法与前面的例子没有什么不同,除了添加了一个System.out日志消息,它将在我们完成设置时打印一条消息。
getObservableList方法将首先创建一个ObservableList的实例,该实例在方法完成时返回。最初,这个实例将是一个空列表。在这个方法中,创建了一个QuestionRetrievalService的实例,并在构造器中传递了 REST 端点的位置。扩展了javafx.concurrent.Service的QuestionRetrievalService被启动,我们监听服务的State的变化。当服务的状态变为State.SUCCEEDED时,我们将检索到的问题添加到ObservableList中。请注意,在QuestionRetrievalService实例的每个状态变化时,我们都会向System.out记录一条消息。
我们现在仔细看看QuestionRetrievalService以理解它如何启动一个新线程,以及它如何确保使用 JavaFX 线程将检索到的问题添加到ListView控件中。QuestionRetrievalService的代码如清单 7-14 所示。
package projavafx;
import java.net.URL;
import java.util.zip.GZIPInputStream;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonValue;
public class QuestionRetrievalService extends Service<ObservableList<Question>> {
private String loc;
public QuestionRetrievalService(String loc) {
this.loc = loc;
}
@Override
protected Task<ObservableList<Question>> createTask() {
return new Task<ObservableList<Question>>() {
@Override
protected ObservableList<Question> call() throws Exception {
URL host = new URL(loc);
JsonReader jr = Json.createReader(new GZIPInputStream(host.openConnection().getInputStream()));
JsonObject jsonObject = jr.readObject();
JsonArray jsonArray = jsonObject.getJsonArray("items");
ObservableList<Question> answer = FXCollections.observableArrayList();
jsonArray.iterator().forEachRemaining((JsonValue e) -> {
JsonObject obj = (JsonObject) e;
JsonString name = obj.getJsonObject("owner").getJsonString("display_name");
JsonString quest = obj.getJsonString("title");
JsonNumber jsonNumber = obj.getJsonNumber("creation_date");
Question q = new Question(name.getString(), quest.getString(), jsonNumber.longValue() * 1000);
System.out.println("Adding question "+q);
answer.add(q);
});
return answer;
}
};
}
}
Listing 7-14.
QuestionRetrievalService
QuestionRetrievalService扩展了Service,因此必须实现一个createTask方法。当Service启动时,该任务在一个单独的线程中执行。QuestionRetrievalService上的createTask方法创建一个新的Task并返回它。这种方法的特征,
Task<ObservableList<Question>> createTask(),
确保Task创建一个ObservableList问题。泛型类型参数ObservableList<Question>与Service声明中的类型参数相同。因此,Service的getValue()方法也将返回Question s 的ObservableList
事实上,下面的代码片段说明了questionRetrievalService.getValue()应该返回一个ObservableList<Question>。
ObservableList<Question> answer = FXCollections.observableArrayList();
...
if (now == State.SUCCEEDED) {
answer.addAll(service.getValue());
}
我们在QuestionRetrievalService中创建的Task实例必须实现 call 方法。这个方法实际上在做前面例子中的getObservableList方法正在做的事情:检索数据并解析它们。
虽然Service(由createTask创建的Task)中的实际工作是在后台线程中完成的,但是Service上的所有方法,包括getValue()调用,都应该从 JavaFX 线程中访问。内部实现确保对Service中可用属性的所有更改都在 JavaFX 应用程序线程上执行。
运行该示例会给出与运行上一个示例完全相同的视觉输出。然而,为了清楚起见,我们添加了一些System.out消息。如果我们运行该示例,可以在控制台上看到以下消息。
State of service is READY
State of service is SCHEDULED
Done with the setup
State of service is RUNNING
Adding question projavafx.Question@482fb3d5
...
Adding question projavafx.Question@2d622bf7
State of service is SUCCEEDED
这表明getObservableList方法在问题被获取并添加到列表之前返回。
Note
理论上,您可能会注意到一种不同的行为,因为后台线程可能会在其他初始化完成之前完成。然而,在实践中,当涉及网络调用时,这种行为是不太可能的。
将 Web 服务数据转换为TableView
到目前为止,我们所有的例子都显示了 a ListView中的问题。ListView是一个简单而强大的 JavaFX 控件,然而,在某些情况下还有其他控件更适合呈现信息。
我们也可以在一个TableView中显示Question数据,这就是我们在本节中所做的。数据的检索和解析与上一个示例中的一样。然而,我们现在使用一个TableView来呈现数据,并且我们必须定义我们想要看到的列。对于每一列,我们必须指定数据的来源。清单 7-15 中的代码显示了示例中使用的启动方法。
@Override
public void start(Stage primaryStage) throws IOException {
TableView<Question> tableView = new TableView<>();
tableView.setItems(getObservableList());
TableColumn<Question, String> dateColumn = new TableColumn<>("Date");
TableColumn<Question, String> ownerColumn = new TableColumn<>("Owner");
TableColumn<Question, String> questionColumn = new TableColumn<>("Question");
dateColumn.setCellValueFactory((CellDataFeatures<Question, String> cdf) -> {
Question q = cdf.getValue();
return new SimpleStringProperty(getTimeStampString(q.getTimestamp()));
});
ownerColumn.setCellValueFactory((CellDataFeatures<Question, String> cdf) -> {
Question q = cdf.getValue();
return new SimpleStringProperty(q.getOwner());
});
questionColumn.setCellValueFactory((CellDataFeatures<Question, String> cdf) -> {
Question q = cdf.getValue();
return new SimpleStringProperty(q.getQuestion());
});
questionColumn.setPrefWidth(350);
tableView.getColumns().addAll(dateColumn, ownerColumn, questionColumn);
StackPane root = new StackPane();
root.getChildren().add(tableView);
Scene scene = new Scene(root, 500, 300);
primaryStage.setTitle("StackOverflow Table");
primaryStage.setScene(scene);
primaryStage.show();
}
Listing 7-15.The Start Method
in the Application Rendering Questions in a
TableView
显然,这个例子比显示ListView的例子需要更多的代码。设置一个表稍微复杂一些,因为涉及到不同的列。设置ListView的内容和设置TableView的内容没有太大区别。这是通过做来实现的
tableView.setItems(getObservableList());
其中的getObservableList()方法与上一个例子中的实现相同。注意,我们也可以使用方便的构造器
TableView<Question> tableView = new TableView<>(getObservableList());
当使用一个TableView时,我们必须定义若干个TableColumns。这是在下面的代码片段中完成的。
TableColumn<Question, String> dateColumn = new TableColumn<>("Date");
TableColumn<Question, String> ownerColumn = new TableColumn<>("Owner");
TableColumn<Question, String> questionColumn = new TableColumn<>("Question");
使用TableColumn构造器,我们创建一个标题为“Date”的TableColumn,一个标题为“Owner”,第三个标题为“Question”Generics <Question, String>表示一行中的每个条目代表一个Question,指定列中的单个单元格属于String类型。
接下来,我们创建的TableColumn的实例需要知道它们应该呈现什么数据。这是使用CellFactories完成的,如下面的代码片段所示。
dateColumn.setCellValueFactory((CellDataFeatures<Question, String> cdf) -> {
Question q = cdf.getValue();
return new SimpleStringProperty(getTimeStampString(q.getTimestamp()));
});
对setCellValueFactory方法的详细描述超出了本章的范围。我们鼓励读者在处理表格的时候看看TableView和TableColumn类的 Javadoc。Javadoc 解释说,我们必须用 call 方法指定一个Callback类,该方法返回一个包含特定单元格内容的ObservableValue。幸运的是,我们可以为此使用 lambda 表达式。
我们在这一行中显示的问题可以通过在这个 lambda 表达式中作为单个参数传递的CellDataFeatures实例获得。因为我们想要显示时间戳,所以我们返回一个SimpleStringProperty,它的内容被设置为指定的Question的时间戳。
同样的技术必须用于另一个TableColumns(包含所有者和适用的Question对象中包含的问题)。
最后,我们必须将列添加到TableView:
tableView.getColumns().addAll(dateColumn, ownerColumn, questionColumn);
运行此示例会产生如图 7-7 所示的可视化输出。
图 7-7。
Using a TableView for rendering questions
对于一个简单的表,这个示例需要大量的样板代码,但幸运的是,JavaFX 平台包含了一种减少代码量的方法。为每一列手动设置CellValueFactory实例很麻烦,但是我们可以使用另一种方法来完成,通过使用 JavaFX 属性。清单 7-16 包含了主类的 start 方法的修改版本,其中我们利用了 JavaFX 属性的概念。
@Override
public void start(Stage primaryStage) throws IOException {
TableView<Question> tableView = new TableView<>();
tableView.setItems(getObservableList());
TableColumn<Question, String> dateColumn = new TableColumn<>("Date");
TableColumn<Question, String> ownerColumn = new TableColumn<>("Owner");
TableColumn<Question, String> questionColumn = new TableColumn<>("Question");
dateColumn.setCellValueFactory(new PropertyValueFactory<>("timestampString"));
ownerColumn.setCellValueFactory(new PropertyValueFactory<>("owner"));
questionColumn.setCellValueFactory(new PropertyValueFactory<>("question"));
questionColumn.setPrefWidth(350);
tableView.getColumns().addAll(dateColumn, ownerColumn, questionColumn);
StackPane root = new StackPane();
root.getChildren().add(tableView);
Scene scene = new Scene(root, 500, 300);
primaryStage.setTitle("StackOverflow Table");
primaryStage.setScene(scene);
primaryStage.show();
}
Listing 7-16.Rendering Data in Columns Based on JavaFX Properties
这段代码显然比前一个示例中的代码要短。我们实际上替换了
dateColumn.setCellValueFactory((CellDataFeatures<Question, String> cdf) -> {
Question q = cdf.getValue();
return new SimpleStringProperty(getTimeStampString(q.getTimestamp()));
});
经过
dateColumn.setCellValueFactory(new PropertyValueFactory<>("timestampString"));
这同样适用于ownerColumn和questionColumn。
我们使用javafx.scene.control.cell.PropertyValueFactory<S,T>(String name)的实例来定义哪些特定数据应该呈现在哪个单元格中。
PropertyValueFactory搜索具有指定名称的 JavaFX 属性,并在被调用时返回该属性的ObservableValue。如果找不到具有这样名称的属性,Javadoc 会这样说:
In this example, the "firstName" string is used as a reference to the firstNameProperty () method assumed in the
Personclass type (the class type of TableView items list). In addition, the method must return aPropertyinstance. If you find a way to meet these requirements, then fillTableCellwith this observable value. In addition, TableView will automatically add an observer to the return value, so that TableView will observe any triggered changes, which will cause the cell to be updated immediately. If there is no method matching this pattern, there is failure support for trying to call the get < attribute > () or the is < attribute > () (that is,getFirstName()orisFirstName()in the previous example). If there is a method that matches the pattern, the value returned from this method is wrapped in aReadOnlyObjectWrapperand returned to TableCell. However, in this case, this means that TableCell will not be able to observe the change of ObservableValue (this is the case with the first method).
由此可见,JavaFX 属性是在TableView中呈现信息的首选方式。到目前为止,我们使用带有 JavaBean getter 和 setter 方法的 POJO Question类作为显示在ListView和TableView中的值对象。
尽管前面的例子在不使用 JavaFX 属性的情况下也能工作,如 Javadoc 所述,我们现在修改Question类,使用 JavaFX 属性来表示所有者信息。timeStamp和文本字段也可以修改为使用 JavaFX 属性,但是混合示例表明 Javadoc 中描述的失败场景确实有效。修改后的Question级如清单 7-17 所示。
package projavafx;
import java.text.SimpleDateFormat;
import java.util.Date;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.PROPERTY)
public class Question {
static final SimpleDateFormat sdf = new SimpleDateFormat ("dd-MM-YY");
private StringProperty ownerProperty = new SimpleStringProperty();
private String question;
private long timestamp;
public Question (String o, String q, long t) {
this.ownerProperty.set(o);
this.question = q;
this.timestamp = t;
}
public String getOwner() {
return ownerProperty.get();
}
public void setOwner(String owner) {
this.ownerProperty.set(owner);
}
public String getQuestion() {
return question;
}
public void setQuestion(String question) {
this.question = question;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public String getTimestampString() {
return sdf.format(new Date(timestamp));
}
}
Listing 7-17.Implementation of Question Class Using JavaFX Properties
for the Author Field
关于这个实现,有一些事情需要注意。ownerProperty遵循标准的 JavaFX 约定,如第三章所述。
除了引入 JavaFX 属性之外,Question类的实现还有另一个重大变化。该类现在用
@XmlAccessorType(XmlAccessType.PROPERTY)
这样做的原因是,在这样做的时候,setter 方法将被JAXB.unmarshal方法调用,当它用一些特定的信息创建一个Question的实例时。既然我们正在使用 JavaFX 属性而不是基本类型,这是必需的。JAXB 框架可以很容易地将 XML 元素“owner”的值赋给 owner String字段,但是默认情况下它不能将值赋给 JavaFX Property对象。
通过使用XmlAccessType.PROPERTY,JAXB 框架将调用setOwner(String v)方法,向setOwner方法提供 XML 元素的值。此方法的实现
ownerProperty.set(owner);
然后将更新随后被TableColumn和TableView使用的 JavaFX 属性。
Question实现中的另一个重要变化是我们添加了一个方法
String getTimestampString()
该方法将以人类可读的格式返回时间戳。您可能已经注意到,在清单 7-16 中,我们将dateColumn的CellValueFactory设置为指向"timestampString"而不是"timeStamp"的PropertyValueFactory:
dateColumn.setCellValueFactory(new PropertyValueFactory<>("timestampString"));
这样做的原因是getTimestamp()方法返回一个 long 类型,而我们更喜欢以一种可读性更好的格式来显示时间戳。通过添加一个getTimestampString()方法并将CellValueFactory指向该方法,该列中单元格的内容将成为可读的时间指示。
到目前为止,我们在本章中展示的例子表明,Java 平台标准版已经包含了许多在访问 web 服务时非常有用的 API。我们还展示了如何使用 JavaFX 并发框架、ObservableList模式、JavaFX 属性和PropertyValueFactory类来增强调用 web 服务和在 JavaFX 控件中呈现数据之间的流程。
虽然示例中没有涉及火箭科学,但是额外的需求会使事情变得更加复杂,并且需要更多的样板代码。幸运的是,JavaFX 社区中已经出现了许多倡议,目标是让我们的生活变得更容易。
使用外部库
到目前为止,我们所有的例子都不需要任何额外的外部库。Java Platform,Standard Edition 和 JavaFX platform 提供了一个很好的环境,可以用来访问 web 服务。在本节中,我们使用两个外部库,并展示它们如何使访问 web 服务变得更容易。
胶子连接
本书之前的版本提到 DataFX 是一个外部库,它提供了一个 JavaFX API 来连接移除端点。DataFX 数据服务的开发已经并入了 Gluon Connect 的开发。
Gluon Connect 是一个开源的、BSD 许可的框架,由 Gluon 管理。胶子连接产品在 http://gluonhq.com/products/mobile/connect 描述,代码在 Bitbucket ( https://bitucket.org/gluon-oss/gluon-connect )提供。
根据 Bitbucket 的说法:“Gluon Connect 是一个客户端库,它简化了将任何来源和格式的数据绑定到 JavaFX UI 控件的过程。它的工作原理是从数据源中检索数据,并将数据从特定格式转换成 JavaFX 可观察列表和可观察对象,这些列表和对象可以直接在 JavaFX UI 控件中使用。它旨在允许开发人员轻松添加对自定义数据源和数据格式的支持。”
Gluon Connect 的一个主要优势是它也支持移动平台。我们将在下一章讨论移动设备上的 JavaFX。
在下一个例子中,我们将 Gluon Connect 与我们的栈交换例子集成在一起。同样,唯一的变化是在getObservableList方法中,但是为了清楚起见,我们在清单 7-18 中显示了整个主类。
import java.io.IOException;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.collections.ObservableList;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.datafx.provider.ListDataProvider;
import org.datafx.provider.ListDataProviderBuilder;
import org.datafx.io.RestSource;
import org.datafx.io.RestSourceBuilder;
import org.datafx.io.converter.InputStreamConverter;
import org.datafx.io.converter.JsonConverter;
public class StackOverflowGluonConnect extends Application {
@Override
public void start(Stage primaryStage) throws IOException {
ListView<Question> listView = new ListView<>();
listView.setItems(getObservableList());
listView.setCellFactory(l -> new QuestionCell());
StackPane root = new StackPane();
root.getChildren().add(listView);
Scene scene = new Scene(root, 500, 300);
primaryStage.setTitle("StackOverflow List");
primaryStage.setScene(scene);
primaryStage.show();
System.out.println ("Done with the setup");
}
ObservableList<Question> getObservableList() throws IOException {
InputStreamConverter converter = new JsonConverter("item", Question.class);
RestSource restSource = RestSourceBuilder.create()
.converter(converter)
.host("http://api.stackexchange.com")
.path("2.2").path("search")
.queryParam("order", "desc")
.queryParam("sort", "activity")
.queryParam("tagged", "javafx")
.queryParam("site", "stackoverflow").build();
ListDataProvider<Question> ldp = ListDataProviderBuilder.create()
.dataReader(restSource)
.build();
Worker<ObservableList<Question>> retrieve = ldp.retrieve();
return retrieve.getValue();
}
public static void main(String[] args) {
launch(args);
}
}
Listing 7-18.Obtaining Questions Using Gluon Connect
相关的部分,getObservableList方法的实现非常简单。
我们首先使用构建器模式创建一个RestClient。我们通过添加请求方法、主机名和路径来提供关于 REST 端点的信息。通过为查询中需要使用的每个查询参数调用queryParam()方法来提供查询参数。
接下来,我们调用 DataProvider 上的一个便利方法,并要求它检索属于我们刚刚创建的 RestClient 的列表。我们为列表中的不同实体提供目标类。这样,DataProvider 将尝试将传入的数据映射到我们指定的目标。Gluon Connect 将识别 JSON 和 XML 格式的数据,并且可以处理压缩的输入流。
后者在堆栈溢出示例中是必需的,因为堆栈溢出端点以压缩格式返回其数据。手动检查数据是否被压缩需要大量样板代码。
调用DataProvider.retrieveList的结果是GluonObservableList的一个实例,一个扩展标准 JavaFX ObservableList的类。GluonObservableList为ObservableList增加了一些属性,使其更适合远程数据检索的特定领域。例如,有一个状态属性可以具有下列值之一:
- 准备好的
- 运转
- 不成功的
- 成功
- 离开的
如果数据检索失败,state 属性将设置为 FAILED。另一个属性 exception 属性将包含包装的异常,该异常指示数据检索失败的原因。
调用DataProvider.retrieveList方法启动一个异步服务。我们可以立即将结果对象返回给我们的可视控件,而不是等待结果。Gluon Connect 框架将在读取和解析输入数据的同时更新结果对象。对于大块数据,这非常有用,因为这种方法允许开发人员在其他部分仍在输入或仍在处理时呈现部分数据。
Gluon Connect 是一个开源框架,在业务友好的 BSD 许可下获得许可,因此您可以在开源和专有(商业)软件中自由使用它。它已经包含了许多增强功能,删除了样板代码,使它更适合客户端到服务器的通信。
使用 Gluon Connect,您可以轻松地从各种来源检索数据,包括文件和 REST 端点。
胶子也为此提供了商业扩展。位于 https://gluonhq.com/products/cloudlink 的 Gluon CloudLink 允许开发人员在 JavaFX 客户端应用程序和许多云提供商和后端服务之间同步数据和功能。Gluon CloudLink 中的数据服务组件允许内容的实时双向同步。这样,Java 企业开发人员可以在一些后端代码中修改 Java 类,结果将立即在 JavaFX 客户端的 UI 组件中可见。
Gluon Connect 包含对 Gluon CloudLink 的支持,在数据来自 Gluon CloudLink 的情况下,您在前面的示例中使用的 API 可以重用。
JAX-RS 啊
Java 企业版 7 的发布包括 JAX-RS 2.0 的发布。该规范不仅定义了 Java 开发人员如何提供 REST 端点,还定义了 Java 代码如何消费 REST 端点。
在下一个例子中,我们修改清单 7-14 的QuestionRetrievalService来使用 JAX-RS API。这显示在清单 7-19 中。
package projavafx.jerseystackoverflow;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
public class QuestionRetrievalService extends Service<ObservableList<Question>> {
private String loc;
private String path;
private String search;
public QuestionRetrievalService(String loc, String path, String search) {
this.loc = loc;
this.path = path;
this.search = search;
}
@Override
protected Task<ObservableList<Question>> createTask() {
return new Task<ObservableList<Question>>() {
@Override
protected ObservableList<Question> call() throws Exception {
Client client = ClientBuilder.newClient();
WebTarget target = client.target(loc).path(path).queryParam("tagged", search).queryParam("site", "stackoverflow");
QuestionResponse response = target.request(MediaType.APPLICATION_JSON).get(QuestionResponse.class);
return FXCollections.observableArrayList(response.getItem());
}
};
}
}
Listing 7-19.Using JAX-RS for Retrieving Questions
为了展示 JAX-RS 的一个很好的工具,我们稍微修改了一下QuestionRetrievalService的构造器,以接受三个参数:
public QuestionRetrievalService(String host, String path, String search);
这是因为 JAX-RS 允许我们使用Builder模式来构建 REST 资源,允许区分主机名、路径、查询参数和其他内容。
因此,我们必须对清单 7-13 稍作修改:
String url = "http://api.stackexchange.com/2.2/search?order=desc&sort=activity&tagged=javafx&site=stackoverflow";
Service<ObservableList<Question>> service = new QuestionRetrievalService(url);
被替换为
String url = "http://api.stackexchange.com/";
String path = "2.2/search";
String search = "javafx";
Service<ObservableList<Question>> service = new QuestionRetrievalService(url, path, search);
主机名、路径和搜索参数用于创建 JAX-RS WebTarget:
Client client = ClientBuilder.newClient();
WebTarget target = client.target(loc).path(path).queryParam("tagged", search)
.queryParam("site", "stackoverflow");
在这个WebResource上,我们可以调用 request 方法来执行请求,后面是get(Class clazz)方法,并提供一个类参数。REST 调用的结果将被解析成所提供的类的一个实例,这也是我们在清单 7-11 的例子中使用 JAXB 所做的。
QuestionResponse response = target.request(MediaType.APPLICATION_JSON).get(QuestionResponse.class);
响应现在包含一个问题列表,我们可以使用与清单 7-4 中完全相同的代码来呈现问题。
摘要
在本章中,我们简要解释了集成 JavaFX 应用程序和企业应用程序的两个选项。我们展示了许多通过 web 服务检索数据的技术,还展示了如何在典型的 JavaFX 控件(如ListView和TableView)中呈现数据。
我们使用了第三方工具来简化检索、解析和呈现数据的过程。我们演示了一些与远程 web 服务相关的特定于 JavaFX 的问题(例如,更新 UI 应该发生在 JavaFX 应用程序线程上)。
重要的是要认识到 JavaFX 客户端应用程序和 web 服务之间的解耦允许很大程度的自由度。处理 web 服务有不同的工具和技术,我们鼓励开发人员在 JavaFX 应用程序中使用他们喜欢的工具。