快速创建一个基于JavaFX的桌面APP,用于模拟一个简单的客户端POST请求;
项目基于Maven创建,使用的IDE是Intellij IDEA,项目编码全部为UTF-8;
JDK版本为1.8。
制作目标
简单说明一下想制作的APP的样子:
实现一个APP,存在两个视图:首页和编辑页。首页简单的展示一些欢迎话语,编辑页负责填写POST请求的数据,并且有一个按钮提交请求。
顶部有菜单,部分菜单只在编辑视图中有效,两个视图间的切换带有动画效果。
下面,开始实现这个简单的APP。
1. 由于个人偏好使用RxJava,而且JavaFx的设计也包含对RxJava的使用,javafx有一个配套的框架,所以会引用rxjavafx框架;
2. 要实现的顶部菜单不在视图范围内,需要实现一个类似EventBus的功能,由顶部菜单向下传播点击操作的事件,因此会引入rxjava2中的部分功能;
3. controlsfx中提供了很多使用的自定义控件;
4. 控件样式直接使用基于Material Design的jfoenix;
5. datafx可以帮助我快速的管理各个视图和菜单的切换关系;
6. json解析库使用fasterxml.jackson,http请求使用apache的httpcomponents,也会使用到其他的commons库;
7. zenjava可以帮助快速打包项目为执行的APP;
8. 使用log4j记录执行日志。
项目依赖添加后部分内容如下:
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<fasterxml.jackson.version>2.8.8fasterxml.jackson.version>
properties>
<dependencies>
<dependency>
<groupId>org.controlsfxgroupId>
<artifactId>controlsfxartifactId>
<version>8.40.12version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.25version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-coreartifactId>
<version>${fasterxml.jackson.version}version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>${fasterxml.jackson.version}version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-annotationsartifactId>
<version>${fasterxml.jackson.version}version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>4.5.3version>
dependency>
<dependency>
<groupId>io.reactivex.rxjava2groupId>
<artifactId>rxjavaartifactId>
<version>2.0.6version>
dependency>
<dependency>
<groupId>io.reactivexgroupId>
<artifactId>rxjavafxartifactId>
<version>2.0.2version>
dependency>
<dependency>
<groupId>com.jfoenixgroupId>
<artifactId>jfoenixartifactId>
<version>1.4.0version>
dependency>
<dependency>
<groupId>io.datafxgroupId>
<artifactId>flowartifactId>
<version>8.0.1version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>utf-8encoding>
configuration>
plugin>
<plugin>
<groupId>com.zenjavagroupId>
<artifactId>javafx-maven-pluginartifactId>
<version>8.5.0version>
<configuration>
<vendor>snartvendor>
<mainClass>demo.AppmainClass>
configuration>
plugin>
plugins>
build>
maven直接创建的项目没有resources目录,因此需要在project setting中添加resources目录。
javafx项目的入口只需要实现javafx.application.Application类即可
public class App extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
// TODO
}
}
使用datafx管理视图的切换(datafx说明见: https://github.com/guigarage/DataFX ),首先需要在start方法中初始化一个ViewFlowContext来管理视图以及视图间共享的数据模型
ViewFlowContext flowContext = new ViewFlowContext();
Javafx用Stage(舞台)来做根基,用Scene(场景)来控制每个场景,每个场景可以定义要展示的内容,这里需要先将primaryStage注入到ViewFlowContext中,初始化一个视图流Flow,然后初始化一个Scene,每个Scene的初始化都需要一个root控件,这里我们使用jfoenix中的JFXDecorator(jfoenix说明见: https://github.com/jfoenixadmin/JFoenix )
flowContext.register("Stage", primaryStage);
// create flow and flow container, flow container controls view decoration and view exchange
Flow flow = new Flow(MainController.class);
DefaultFlowContainer container = new DefaultFlowContainer();
flow.createHandler(flowContext).start(container);
// JFXDecorator will be applied to primaryStage, and decorated on view which is created by flow container
JFXDecorator decorator = new JFXDecorator(primaryStage, container.getView(),
false, true, true);
// init scene with a decorator
Scene scene = new Scene(decorator, 750, 500);
primaryStage.setMinWidth(500);
primaryStage.setMinHeight(400);
primaryStage.setTitle("Demo");
primaryStage.setScene(scene);
primaryStage.show();
Flow的初始化需要一个默认的视图以及视图控制器:
import io.datafx.controller.ViewController;
@ViewController(value = "/views/main.fxml")
public class MainController {
}
注解@ViewController用于定义此控制器指定的视图位置,main.fxml定义在resource/views下:
<BorderPane xmlns="http://javafx.com/javafx/8.0.112"
xmlns:fx="http://javafx.com/fxml/1">
<top>
<HBox prefHeight="10.0" BorderPane.alignment="CENTER">
<MenuBar prefHeight="25.0" HBox.hgrow="ALWAYS">
<Menu mnemonicParsing="false" text="菜单">
<MenuItem mnemonicParsing="false" text="主页"/>
<MenuItem mnemonicParsing="false" text="发送请求"/>
Menu>
<Menu mnemonicParsing="false" text="编辑">
<MenuItem mnemonicParsing="false" text="保存"/>
Menu>
MenuBar>
HBox>
top>
<center>
center>
BorderPane>
main.fxml 定义了一个BorderPane,BorderPane是一个边框面板,存在上下左右中四个域,是一个非常适合做菜单的面板,我在上(top)定义了一个水平盒子(HBox),用于将菜单栏放在其中,菜单栏中定义了两个主菜单:菜单和编辑;
运行App,可以看到一个只有菜单的窗口,它的顶部是一个黑色的边框,上面有最小化、最大化和关闭三个通用的控件,这些控件的控制是在JFXDecorator初始化时定义的,可以查看下此类的构造函数的说明;
但是我不太喜欢这种黑色的边框,以及菜单的样式,因此下面我会将这些内容重新定义一番。
默认的App顶部不显示Title和Icon,双击顶部栏也不会触发窗口最大化,下面我要在顶部栏添加这些控件,由于jfoenix没有提供类似的控件,也没有实现此效果的接口,因此需要一点投机取巧的方式:
1. 首先重新实现Decorator
public class CustomJFXDecorator extends JFXDecorator {
public CustomJFXDecorator(Stage stage, Node node) {
this(stage, node, true, true, true);
}
public CustomJFXDecorator(Stage stage, Node node, boolean fullScreen, boolean max, boolean min) {
super(stage, node, fullScreen, max, min);
// top area is a buttons container and with a class 'jfx-decorator-buttons-container'
Node btnContainerOpt = this.lookup(".jfx-decorator-buttons-container");
if (btnContainerOpt != null) {
// buttons container is a HBox
final HBox buttonsContainer = (HBox) btnContainerOpt;
// check and get the index of maximum button
ObservableList buttons = buttonsContainer.getChildren();
int btnMaxIdx = 0;
if (fullScreen) {
btnMaxIdx++;
}
if (min) {
btnMaxIdx++;
}
if (buttons.size() >= btnMaxIdx) {
final JFXButton btnMax = (JFXButton) buttons.get(btnMaxIdx);
// set max button triggered when buttons container is double clicked
buttonsContainer.setOnMouseClicked(event -> {
if (event.getClickCount() == 2) {
btnMax.fire();
}
});
}
// add HBox in the left of buttons container
HBox leftBox = new HBox();
leftBox.setAlignment(Pos.CENTER_LEFT);
leftBox.setPadding(new Insets(0, 0, 0, 10));
leftBox.setSpacing(10);
// add icon in the left of HBox
HBox iconBox = new HBox();
iconBox.setAlignment(Pos.CENTER_LEFT);
iconBox.setSpacing(5);
// bind icon
stage.getIcons().addListener((ListChangeListener) c -> {
while (c.next()) {
iconBox.getChildren().clear();
ObservableList extends Image> icons = c.getList();
if (icons != null && !icons.isEmpty()) {
ImageView imageView;
for (Image icon : icons) {
imageView = new ImageView();
imageView.setFitWidth(20);
imageView.setFitHeight(20);
imageView.setImage(icon);
iconBox.getChildren().add(imageView);
}
}
}
});
// bind title
Label title = new Label();
title.textProperty().bindBidirectional(stage.titleProperty());
// set title to white because of the black background
title.setTextFill(Paint.valueOf("#fdfdfd"));
leftBox.getChildren().addAll(iconBox, title);
HBox.setHgrow(leftBox, Priority.ALWAYS);
buttonsContainer.getChildren().add(0, leftBox);
}
}
}
控件主要的目的就是将顶部栏的双击事件和最大化窗口按钮绑定,在栏目的左侧添加一个icon域和title域。然后再App中使用新的Decorator:
CustomJFXDecorator decorator = new CustomJFXDecorator(primaryStage, container.getView(), false, true, true);
添加primaryStage的icon,icon放在resources/image目录下:
primaryStage.getIcons().add(new Image("/image/icon.png"));
现在看起来稍微好点了,但是顶部太黑了,而且窗口两侧黑色区域太大,为了修改这些样式,需要自定义样式,javafx中可以通过css控制控件的样式。
2. 修改主框架样式
首先添加一个主样式文件 main.css, 放在目录 resources/css 下,然后查阅了相关资料得知修改decorator样式的css类名,修改如下:
/* jfx decorator part, about to change window style */
.jfx-decorator {
-fx-decorator-color: derive(#3f3f3f, -20%);
}
.jfx-decorator .jfx-decorator-buttons-container {
-fx-background-color: -fx-decorator-color;
}
.jfx-decorator .resize-border {
-fx-border-color: -fx-decorator-color;
-fx-border-width: 0 1 1 1;
}
然后将此样式应用到项目中,在App中添加:
final ObservableList stylesheets = scene.getStylesheets();
stylesheets.add(App.class.getResource("/css/main.css").toExternalForm());
3. 修改菜单样式
菜单上,给每个Item添加一个快捷键,同时稍微调整下菜单的显示。
首先给MenuBar添加styleClass属性,给每个Item添加快捷方式:
<MenuBar styleClass="top-menu" prefHeight="25.0" HBox.hgrow="ALWAYS">
<Menu mnemonicParsing="false" text="菜单">
<MenuItem mnemonicParsing="false" text="主页">
<accelerator>
<KeyCodeCombination alt="UP" code="H" control="DOWN" meta="UP" shift="UP" shortcut="UP"/>
accelerator>
MenuItem>
<MenuItem mnemonicParsing="false" text="发送请求">
<accelerator>
<KeyCodeCombination alt="UP" code="R" control="DOWN" meta="UP" shift="UP" shortcut="UP"/>
accelerator>
MenuItem>
Menu>
<Menu mnemonicParsing="false" text="编辑">
<MenuItem mnemonicParsing="false" text="保存">
<accelerator>
<KeyCodeCombination alt="UP" code="S" control="DOWN" meta="UP" shift="UP" shortcut="UP"/>
accelerator>
MenuItem>
Menu>
MenuBar>
然后开始在main.css中定义样式,选中时字体颜色不变白,去掉菜单和底部的空白间隙,菜单每个Item的宽度最小为10em,Item后面的快捷键标签显示为灰色:
/* top menu part, change menu style */
.top-menu .label {
-fx-text-fill: #2d2d2d;
}
.top-menu .menu-item {
-fx-padding: 0.3em 1em 0.3em 1em;
}
.top-menu .context-menu {
-fx-min-width: 10em;
-fx-padding: 0.1em 0 0 0;
}
.top-menu .menu-item .label {
-fx-text-fill: #2d2d2d;
}
.top-menu .menu-item .accelerator-text {
-fx-text-fill: grey;
-fx-padding: 0em 0em 0em 3em;
}
以上,样式的自定义就完成了。
上面的 main.fxml 主要定义整个App的主展示,包含一个顶部菜单和一个中间展示域,需要变动展示的区域即为中间展示域,下面我们通过在MainController初始化datafx需要控制的 view flow (视图流)
1.定义首页
首先进入App需要有一个首页,定义HomeController 和 home.fxml
HomeController.java
@ViewController(value = "/views/home.fxml")
public class HomeController {
}
home.fxml
<StackPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml">
<HBox alignment="CENTER">
<Label textFill="#3e8080">欢迎使用Demo!Label>
HBox>
StackPane>
首页只是简单地展示一个欢迎语;
然后将此视图添加入视图流,为了将菜单和视图流绑定,需要给每个fxml中的控件添加id(在需要添加的控件上添加属性fx:id),在MainController中引用这些控件(使用@FXML注解,字段名即为设置的id)
添加id
引用控件并注入ViewFlowContext
public class MainController {
@FXMLViewFlowContext
private ViewFlowContext context;
@FXML
private BorderPane root;
@FXML
private MenuItem home;
@FXML
private MenuItem demo;
@FXML
private MenuItem save;
}
为了实现动画切换的效果,需要重新实现一个自定义动画效果(这里使用ContainerAnimations已经定义好的动画)和动画时长的FlowContainer,动画效果实现的方式就是在加载新的视图之前,对原来的区域内容截图并设置动画效果,具体实现如下:
public class ExtendedAnimatedFlowContainer extends AnimatedFlowContainer implements FlowContainer<StackPane> {
private final StackPane view;
private final Duration duration;
private Function> animationProducer;
private Timeline animation;
private final ImageView placeholder;
/**
* Defaults constructor that creates a container with a fade animation that last 320 ms.
*/
public ExtendedAnimatedFlowContainer() {
this(Duration.millis(320));
}
/**
* Creates a container with a fade animation and the given duration.
*
* @param duration the duration of the animation
*/
public ExtendedAnimatedFlowContainer(Duration duration) {
this(duration, ContainerAnimations.FADE);
}
/**
* Creates a container with the given animation type and duration.
*
* @param duration the duration of the animation
* @param animation the animation type
*/
public ExtendedAnimatedFlowContainer(Duration duration, ContainerAnimations animation) {
this(duration, animation.getAnimationProducer());
}
/**
* Creates a container with the given animation type and duration.
*
* @param duration the duration of the animation
* @param animationProducer the {@link KeyFrame} instances that define the animation
*/
public ExtendedAnimatedFlowContainer(Duration duration, Function>
animationProducer) {
this.view = new StackPane();
this.duration = duration;
this.animationProducer = animationProducer;
placeholder = new ImageView();
placeholder.setPreserveRatio(true);
placeholder.setSmooth(true);
}
public void changeAnimation(ContainerAnimations animation) {
this.animationProducer = animation.getAnimationProducer();
}
@Override
public void setViewContext(ViewContext context) {
updatePlaceholder(context.getRootNode());
if (animation != null) {
animation.stop();
}
animation = new Timeline();
animation.getKeyFrames().addAll(animationProducer.apply(this));
animation.getKeyFrames().add(new KeyFrame(duration, (e) -> clearPlaceholder()));
animation.play();
}
/**
* Returns the {@link ImageView} instance that is used as a placeholder for the old view in each navigation
* animation.
*
* @return image view place holder
*/
public ImageView getPlaceholder() {
return placeholder;
}
/**
* Returns the duration for the animation.
*
* @return the duration for the animation
*/
public Duration getDuration() {
return duration;
}
public StackPane getView() {
return view;
}
private void clearPlaceholder() {
view.getChildren().remove(placeholder);
}
private void updatePlaceholder(Node newView) {
if (view.getWidth() > 0 && view.getHeight() > 0) {
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
Image placeholderImage = view.snapshot(parameters,
new WritableImage((int) view.getWidth(), (int) view.getHeight()));
placeholder.setImage(placeholderImage);
placeholder.setFitWidth(placeholderImage.getWidth());
placeholder.setFitHeight(placeholderImage.getHeight());
} else {
placeholder.setImage(null);
}
placeholder.setVisible(true);
placeholder.setOpacity(1.0);
view.getChildren().setAll(placeholder, newView);
placeholder.toFront();
}
}
在MainController初始化时初始化视图流控制root的中间域的切换展示:
@PostConstruct
public void init() throws FlowException {
Objects.requireNonNull(context);
// create the inner flow and content, set the default controller
Flow innerFlow = new Flow(HomeController.class);
final FlowHandler flowHandler = innerFlow.createHandler(context);
context.register("ContentFlowHandler", flowHandler);
context.register("ContentFlow", innerFlow);
final Duration containerAnimationDuration = Duration.millis(320);
root.setCenter(flowHandler.start(new ExtendedAnimatedFlowContainer(containerAnimationDuration,
ContainerAnimations.SWIPE_LEFT)));
context.register("ContentPane", root.getCenter());
// bind events on menu
JavaFxObservable.actionEventsOf(home).subscribe(actionEvent -> {
if (!(flowHandler.getCurrentView().getViewContext().getController() instanceof HomeController)) {
flowHandler.handle(home.getId());
}
});
// bind menu to view in flow
bindMenuToController(home, HomeController.class, innerFlow);
}
private void bindMenuToController(MenuItem menu, Class> controllerClass, Flow flow) {
flow.withGlobalLink(menu.getId(), controllerClass);
}
2. 定义编辑视图
下面添加编辑视图(填写Post数据),假设有一个信息保存接口,需要提供一个信息类型、名称和说明:
demo.fxml
<BorderPane fx:id="root" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<center>
<VBox fx:id="infoForm" alignment="TOP_CENTER" spacing="40">
<HBox spacing="10">
<Label>类型Label>
<JFXRadioButton fx:id="radioFood" selected="true" text="食物">
<toggleGroup>
<ToggleGroup fx:id="typeGroup"/>
toggleGroup>
<cursor>
<Cursor fx:constant="HAND"/>
cursor>
JFXRadioButton>
<JFXRadioButton fx:id="radioTool" text="工具" toggleGroup="$typeGroup">
<cursor>
<Cursor fx:constant="HAND"/>
cursor>
JFXRadioButton>
HBox>
<JFXTextField fx:id="nameField" labelFloat="true" promptText="名称">
JFXTextField>
<JFXTextField fx:id="descField" labelFloat="true" promptText="说明">
JFXTextField>
<padding>
<Insets bottom="20.0" left="10.0" right="10.0" top="10.0"/>
padding>
VBox>
center>
<bottom>
<HBox prefHeight="70" alignment="CENTER_LEFT">
<JFXTextField fx:id="urlField" promptText="链接" labelFloat="true"
HBox.hgrow="ALWAYS" maxWidth="1000">
JFXTextField>
<HBox HBox.hgrow="ALWAYS" alignment="CENTER_RIGHT">
<JFXButton fx:id="postBtn" buttonType="RAISED" prefWidth="100" styleClass="jfx-button-primary" text="提交"/>
HBox>
<padding>
<Insets left="10.0" bottom="10.0" right="10.0" top="10.0"/>
padding>
HBox>
bottom>
BorderPane>
定义一个Model,通过JavaFx的PropertyBinding功能,直接将Model的字段和输入控件绑定,点击提交按钮时发出Post请求,HttpsClient是一个对 CloseableHttpClient 的简单封装:
DemoController
@ViewController("/views/demo.fxml")
public class DemoController {
private static Logger logger = LoggerFactory.getLogger(DemoController.class.getName());
@FXML
private ToggleGroup typeGroup;
@FXML
private JFXRadioButton radioFood, radioTool;
@FXML
private JFXTextField nameField, descField, urlField;
@FXML
private JFXButton postBtn;
private DemoInfo demoInfo;
private static final int TYPE_FOOD = 1;
private static final int TYPE_TOOL = 2;
@PostConstruct
public void init() {
demoInfo = new DemoInfo();
bindDemoInfoToControls();
JavaFxObservable.actionEventsOf(postBtn)
.subscribeOn(Schedulers.computation())
.subscribe(actionEvent -> {
// runnable for that thread
new Thread(this::submit).start();
});
}
private void bindDemoInfoToControls() {
radioFood.setUserData(TYPE_FOOD);
radioTool.setUserData(TYPE_TOOL);
demoInfo.typeProperty().addListener((observable, oldValue, newValue) -> {
Toggle selected = null;
for (Toggle toggle : typeGroup.getToggles()) {
if (newValue == toggle.getUserData()) {
selected = toggle;
break;
}
}
if (selected == null) {
throw new IllegalArgumentException("Demo info set type value which is not in toggle values");
}
typeGroup.selectToggle(selected);
});
typeGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> demoInfo
.typeProperty().setValue((Integer) newValue.getUserData()));
nameField.textProperty().bindBidirectional(demoInfo.nameProperty());
descField.textProperty().bindBidirectional(demoInfo.descriptionProperty());
}
private void submit() {
try {
ObjectMapper om = new ObjectMapper();
String postData = om.writeValueAsString(demoInfo);
String url = urlField.getText();
HttpResponse response = HttpsClient.doPostSSL(url, postData);
if (response == null) {
return;
}
if (response.getStatus() != HttpStatus.SC_OK) {
Exception exception = response.getException();
if (exception != null && exception instanceof HttpHostConnectException) {
return;
}
return;
}
logger.info("Post success");
} catch (JsonProcessingException e) {
logger.error("Do post parse post data error", e);
}
}
}
在MainController中将视图添加进视图流
JavaFxObservable.actionEventsOf(demo).subscribe(actionEvent -> {
if (!(flowHandler.getCurrentView().getViewContext().getController() instanceof DemoController)) {
flowHandler.handle(demo.getId());
}
});
bindMenuToController(demo, DemoController.class, innerFlow);
然而,上面的操作流程无法看到请求是否在进行,以及请求的结果,所以将在流程中添加如下操作:当点击提交时,出现一个spinner,请求返回时,弹窗提醒结果。
对于现实的spinner,使用jfoenix的JFXSpinner,并将它展示一个模态框中,实现如下:
public class DemoController {
...
@FXMLViewFlowContext
private ViewFlowContext context;
@FXML
private BorderPane root;
...
private Stage spinnerStage;
...
private void initSpinner() {
StackPane spinnerRoot = new StackPane();
spinnerRoot.getStyleClass().add("register-dialog");
JFXSpinner first = new JFXSpinner();
first.getStyleClass().addAll("spinner-black", "first-spinner");
first.setStartingAngle(-40);
JFXSpinner second = new JFXSpinner();
second.getStyleClass().addAll("spinner-dark", "second-spinner");
second.setStartingAngle(-90);
JFXSpinner third = new JFXSpinner();
third.getStyleClass().addAll("spinner-gray", "third-spinner");
third.setStartingAngle(-120);
spinnerRoot.getChildren().addAll(first, second, third);
spinnerStage = new Stage(StageStyle.TRANSPARENT);
spinnerStage.initModality(Modality.APPLICATION_MODAL);
spinnerStage.initOwner((Stage) context.getRegisteredObject("Stage"));
Scene scene = new Scene(spinnerRoot, Color.TRANSPARENT);
scene.getStylesheets().add(DemoController.class
.getResource("/css/register-dialog.css").toExternalForm());
spinnerStage.setScene(scene);
}
private void showSpinner() {
Stage primaryStage = (Stage) context.getRegisteredObject("Stage");
spinnerStage.setWidth(primaryStage.getWidth());
spinnerStage.setHeight(primaryStage.getHeight());
spinnerStage.setX(primaryStage.getX());
spinnerStage.setY(primaryStage.getY());
spinnerStage.show();
}
private void closeSpinner(Runnable later) {
Platform.runLater(() -> {
spinnerStage.close();
if (later != null) {
later.run();
}
});
}
}
修改submit方法
@PostConstruct
public void init() {
demoInfo = new DemoInfo();
bindDemoInfoToControls();
initSpinner();
JavaFxObservable.actionEventsOf(postBtn)
.subscribeOn(Schedulers.computation())
.subscribe(actionEvent -> {
showSpinner();
// runnable for that thread
new Thread(this::submit).start();
});
}
private void submit() {
try {
ObjectMapper om = new ObjectMapper();
String postData = om.writeValueAsString(demoInfo);
String url = urlField.getText();
HttpResponse response = HttpsClient.doPostSSL(url, postData);
if (response == null) {
closeSpinner(() -> NotificationUtils.notifyError("提交失败!", root));
return;
}
if (response.getStatus() != HttpStatus.SC_OK) {
Exception exception = response.getException();
if (exception != null && exception instanceof HttpHostConnectException) {
closeSpinner(() -> NotificationUtils.notifyError("无法连接服务器!", root));
return;
}
closeSpinner(() -> NotificationUtils.notifyError("提交失败!", root));
return;
}
closeSpinner(() -> NotificationUtils.notifySuccess("提交成功!", root));
} catch (JsonProcessingException e) {
logger.error("Do post parse post data error", e);
closeSpinner(() -> NotificationUtils.notifyError("无法解析请求JSON:" + e.getMessage(), root));
}
}
以上,简单的编辑框就实现了。
菜单上的编辑->保存按钮还没有实现,这个按钮的目的是在进入编辑视图时,点击保存时能将当前填写的信息保存到本地中,下次进入此视图的时候会检查本地是否有缓存,如果存在,则读取缓存。但是,当不在编辑视图时,保存按钮需要被禁用(同时置灰)。
为了实现保存按钮只在编辑视图中有效,需要做以下操作:
在MainController中将save菜单的disableProperty和视图绑定
public class MainController {
...
private BooleanProperty saveDisable = new SimpleBooleanProperty();
@PostConstruct
public void init() throws FlowException {
...
JavaFxObservable.actionEventsOf(home).subscribe(actionEvent -> {
if (!(flowHandler.getCurrentView().getViewContext().getController() instanceof HomeController)) {
flowHandler.handle(home.getId());
saveDisable.setValue(Boolean.TRUE);
}
});
JavaFxObservable.actionEventsOf(demo).subscribe(actionEvent -> {
if (!(flowHandler.getCurrentView().getViewContext().getController() instanceof DemoController)) {
flowHandler.handle(demo.getId());
saveDisable.setValue(Boolean.FALSE);
}
});
...
saveDisable.setValue(Boolean.TRUE);
save.disableProperty().bindBidirectional(saveDisable);
}
}
实现cache的读写,首先需要实现EventBus:
Event
public class Event {
private EventType type;
public Event(EventType type) {
this.type = type;
}
public EventType getType() {
return type;
}
public void setType(EventType type) {
this.type = type;
}
public static enum EventType {
SAVE
}
}
EventBus
public class EventBus {
private static final EventBus INSTANCE = new EventBus();
private final PublishSubject mBusSubject = PublishSubject.create();
public static EventBus getInstance() {
return INSTANCE;
}
public Disposable register(Consumer onNext) {
return mBusSubject.subscribe(onNext);
}
public void postSave(Event event) {
mBusSubject.onNext(event);
}
}
在MainController中设置当点击保存时触发事件:
JavaFxObservable.actionEventsOf(save).subscribe(actionEvent -> {
EventBus.getInstance().postSave(new Event(Event.EventType.SAVE));
});
在DemoController中定义事件接收操作以及缓存相关操作:
public class DemoController {
...
private Disposable disposable;
...
private static final String CACHE_PATH = "data" + File.separator + "info.data";
@PostConstruct
public void init() {
...
initFields();
...
disposable = EventBus.getInstance().register(event -> {
if (event.getType() == Event.EventType.SAVE) {
doSave();
}
});
}
@PreDestroy
public void destroy() {
if (disposable != null) {
disposable.dispose();
}
}
...
private void initFields() {
DemoInfoCache cache = readCache();
if (cache != null) {
DemoInfo savedInfo = cache.getDemoInfo();
this.demoInfo.setType(savedInfo.getType());
this.demoInfo.setName(savedInfo.getName());
this.demoInfo.setDescription(savedInfo.getDescription());
this.urlField.setText(cache.getUrl());
}
}
private void doSave() {
final DemoInfoCache cache = new DemoInfoCache();
cache.setDemoInfo(this.demoInfo);
cache.setUrl(this.urlField.getText());
ObjectMapper om = new ObjectMapper();
Path localCache = Paths.get(CACHE_PATH);
Path localCacheDir = localCache.getParent();
OutputStream os = null;
try {
if (!Files.exists(localCacheDir) || !Files.isDirectory(localCacheDir)) {
Files.createDirectory(localCacheDir);
}
os = new FileOutputStream(CACHE_PATH);
byte[] saveObjBytes = om.writeValueAsBytes(cache);
byte[] saveFileBytes = Base64.getEncoder().encode(saveObjBytes);
os.write(saveFileBytes);
os.flush();
} catch (IOException e) {
logger.error("Save post data to local file failed, error", e);
NotificationUtils.notifyError("保存失败!", root);
} finally {
if (os != null) {
try {
os.close();
} catch (IOException ignored) {
}
}
}
}
private DemoInfoCache readCache() {
Path path = Paths.get(CACHE_PATH);
if (Files.isRegularFile(path)) {
try (InputStream is = Files.newInputStream(path)) {
byte[] localCacheBytes = new byte[is.available()];
int i = is.read(localCacheBytes);
logger.debug("read total {} bytes of register cache", i);
byte[] decrypt = Base64.getDecoder().decode(localCacheBytes);
ObjectMapper om = new ObjectMapper();
return om.readValue(decrypt, DemoInfoCache.class);
} catch (Exception e) {
logger.error("read register cache failed", e);
}
}
return null;
}
}
至此,一个简单的demo桌面app就完成了,项目地址见:https://github.com/blacksider/javafx-demo