JavaFx的那些坑--学习JavaFX过程中遇到的坑

最近在看JavaFX,为了早日进入状态,没有从基础看起,而是直接从网上的例子教程开始编写代码。期间遇到不少的坑,对界面开发也有不少心得,因此在这篇文章中记录下来,持续更新。

我的开发环境是

  • IntelliJ 2017
  • JDKD1.8.0_152

0.JavaFX最佳的学习资源

其实Oracle已经把最佳的学习资源提供给大家了,就是jdk自带的sample中的demo,尤其是Ensemble8这个demo,几乎包罗万象。最新的JDK8的下载地址在这里(我用的是8u152):

Java SE Development Kit 8

页面中除了JDK以外,还有一个可以下载的包就是“ Java SE Development Kit 8u152 Demos and Samples Downloads”,下载下来后,jdk1.8.0_152\demo\javafx_samples\Ensemble8.jar就是一个可以执行的jar包。其源代码就在jdk1.8.0_152\demo\javafx_samples\src\Ensemble8中。

设置好JDK,直接双击jar包,运行起来是这个样子的:

所有控件的使用方法,以及对应的源代码都可以在程序中直接查看,还支持app内搜索,以及直接作为浏览器连接Oracle网站查看相关文档。作为一个demo,它已经做到了最好。

Ensemble8的源代码有两种管理方式,一种是Gradle,一种是NetBean的project。因为我使用的是IDEA,因此直接new一个新的project,选择Gradle模式,导入源代码包即可运行。需要任何控件或者特性的使用方法,直接在这个demo中搜索关键字(例如tree),然后运行相应的demo就可以,还能查看源代码、相关文档和相关的示例,简直是JavaFX的良心之作。

1.JDK8必须是最新版才能支持所有的功能

例如Alert对话框必须在JDK8的比较新的版本中才支持,较低的版本你会发现没有这个类,例子如下:

Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("信息");
alert.setHeaderText("header");
alert.setContentText("message");
alert.show();

刚开始我的JDK是1.8.0_20,就没有Alert类。

2.对于MVC模式的理解

M就是各种POJO类,也可以认为是bean类,这些类的属性可以bind另外的属性,例如控件中的属性,这样就可以和控件上的显示联动;

V就是fxml文档,可以使用JavaFX Scene Builder 2.0工具来实现可视化的编写,这个工具要单独下载,然后和IDEA关联起来使用;

C就是Controller类,这个类中的方法都是fxml文件中定义的控件方法的实现。

3.JavaFX Scene Builder 2.0的下载、设置和使用

Oracle把这个工具的下载地址藏得很好,我也不知道为什么,它在这里:

JavaFX Scene Builder 1.x Archive

下载后直接在系统中安装,然后在IntelliJ 中设置:

Settings-->Languages & frameworks-->JavaFx-->Path to SceneBuilder

设置为JavaFX Scene Builder 2.0的exe工具地址。

在IntelliJ工程中,选中fxml文件,右键选中open in SceneBuilder就可以打开使用了。确实非常好用,强烈推荐。

4.在Application类中获取Controller

Controller类用来处理界面的各种控件属性,各种响应函数,因此有些Controller中的代码希望在Application类中显式执行,因此必须在Application类中获取Controller类的实例。在Application类中获取Controller的方法,一般来说start函数是这样的:

Parent root = FXMLLoader.load(getClass().getResource("MySecene.fxml"));
primaryStage.setTitle("My Application");
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();

这样来获取Controller:

//FXMLLoader必须使用参数初始化,否则getController会失败
FXMLLoader loader = new FXMLLoader(getClass().getResource("MySecene.fxml"));
Parent root = loader.load();
//这个方法必须在load方法之后调用
MySeceneController controller = loader.getController();

primaryStage.setTitle("My Application");
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();

5.Controller的initialize方法

可以在Controller类中添加一个initialize方法,这个方法则会自动在构造函数之后调用,例如:

public void initialize() {
    System.out.println("MySeceneController initialize");
}
public MySeceneController() {
    System.out.println("constructor");
}

这样在程序启动时,会先调用构造函数,然后调用initialize方法。

6.bind属性

将一个TextField的文本属性bind到Stage的Title属性,代码如下(从不同的类中聚合到此处):

TextField txtBind;
SimpleStringProperty bindProperty;
//初始化控件等。。。
bindProperty.bind(txtBind.textProperty());
stage.titleProperty().bind(bindProperty);

然后在TextField填写文本时,Stage标题会随之改变。

7.在非UI线程中更新界面的问题(例如进度条完成后弹出对话框)

一个经典场景,进度条完成后,弹出一个对话框。那么,首先需要绑定一个SimpleDoubleProperty到进度条的progressProperty()属性上,然后给进度条增加一个事件监听ChangeListener。当进度条完成时,希望弹出一个对话框。

如果直接在ChangeListener的实现函数中这样写:

progressBar.progressProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                System.out.println(newValue);
                if (newValue.intValue() >= 1.0) {
                    Alert alert = new Alert(Alert.AlertType.INFORMATION);
                    alert.setTitle("完成");
                    alert.setHeaderText("header");
                    alert.setContentText("进度完成");
                    alert.show();

                }
            }
        });

那么就是这样报错:

Exception in thread "Thread-5" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-5

原因就是在非UI线程中调用了界面相关方法。

解决方法就是利用Platform.runLater方法:

progressBar.progressProperty().addListener(new ChangeListener<Number>() {
    @Override
    public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
        System.out.println(newValue);
        if (newValue.intValue() >= 1.0) {
            runLaterProgressOver();
        }
    }
});

private void runLaterProgressOver() {
    Platform.runLater(new Runnable() {
        @Override
        public void run() {
            //更新JavaFX的主线程的代码放在此处
            Alert alert = new Alert(Alert.AlertType.INFORMATION);
            alert.setTitle("完成");
            alert.setHeaderText("header");
            alert.setContentText("进度完成");
            alert.show();
        }
    });
}

事实上各种界面语言都是这样的,不过解决方法各有不同而已。例如在Android中就是用这些方法:

为了解决这个问题,Android提供了一些方法,从其他线程访问UI线程:

总之,我们也要与Android遵循同样的守则

1.不要阻塞UI线程。

2.不要在UI线程之外访问 UI 相关代码。

8.定制DatePicker

要让DatePicker返回选择的日期值,办法是用一个SimpleStringProperty来bind它的getEditor().textProperty()的值(直接在DatePicker中找相关属性没有找到)。

定制DatePicker的单元格DateCell,方法是设置setDayCellFactory方法,其参数是一个Callback<DatePicker, DateCell>的实例。

具体代码如下:

public class DatePickerMain extends Application {
    private SimpleStringProperty dateString;

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

    @Override
    public void start(Stage stage) {
        VBox vbox = new VBox(20);
        Scene scene = new Scene(vbox, 400, 400);
        stage.setScene(scene);
        final DatePicker startDatePicker = new DatePicker();
        DatePicker endDatePicker = new DatePicker();
        startDatePicker.setValue(LocalDate.now());
        //为DateCell添加功能
        final Callback<DatePicker, DateCell> dayCellFactory = new Callback<DatePicker, DateCell>() {
            @Override
            public DateCell call(final DatePicker datePicker) {
                return new DateCell() {
                    @Override
                    public void updateItem(LocalDate item, boolean empty) {
                        super.updateItem(item, empty);
                        long p = ChronoUnit.DAYS.between(startDatePicker.getValue(), item);
                        setTooltip(new Tooltip("You're about to stay for " + p + " days"));
                    }
                };
            }
        };
        endDatePicker.setDayCellFactory(dayCellFactory);
        endDatePicker.setValue(startDatePicker.getValue().plusDays(1));

        //将endDatePicker的Editor的textProperty属性bind到dateString
        dateString = new SimpleStringProperty();
        dateString.bindBidirectional(endDatePicker.getEditor().textProperty());

        endDatePicker.getEditor().textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                //在UI线程中弹出对话框
                runLaterDate(dateString);
            }
        });
        vbox.getChildren().add(new Label("Start Date:"));
        vbox.getChildren().add(startDatePicker);
        vbox.getChildren().add(new Label("End Date:"));
        vbox.getChildren().add(endDatePicker);
        stage.show();
    }

    private void runLaterDate(SimpleStringProperty dateString) {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                Alert alert = new Alert(Alert.AlertType.INFORMATION);
                alert.setTitle("完成");
                alert.setHeaderText("header");
                alert.setContentText("选择的日期为:" + dateString.getValue());
                alert.show();
            }
        });
    }
}

(话说Snagit截图真好用)

9.添加系统托盘

JavaFX中并没有系统托盘相关功能,而是要直接使用AWT中的相关功能。注意两点:

一是必须调用Platform.setImplicitExit(false);来保证窗口关闭后,Stage对象仍然存活;二是在AWT的ActionListener中,要调用JavaFX相关显示API,需要使用Platform.runLater()方法,来保证JavaFX的代码运行在UI线程中。

例子代码如下,例子中添加了托盘,双击托盘能够隐藏/显示窗口,托盘的弹出菜单和响应函数:

/**
     * 设置系统托盘
     *
     * @param primaryStage
     */
    private void setSystemTray(Stage primaryStage) {
        //必须加上这个设置,才能让窗口在hide后重新show
        Platform.setImplicitExit(false);
        TrayIcon trayIcon = null;
        if (SystemTray.isSupported()) {
            // get the SystemTray instance
            SystemTray tray = SystemTray.getSystemTray();
            // load an image
            File file = new File("");
            String filepath = file.getAbsolutePath() + "\\src\\resources\\apple16.png";
            Image image = Toolkit.getDefaultToolkit().getImage(filepath);

            // create a action listener to listen for default action executed on the tray icon
            java.awt.event.ActionListener listener = new ActionListener() {
                public void actionPerformed(java.awt.event.ActionEvent e) {
                    // execute default action of the application
                    String action = e.getActionCommand();
                    if (action.equals("item1_action")) {
                        System.out.println("item1");
                    } else if (action.equals("item2_action")) {
                        System.out.println("item2");
                    } else if (action.equals("exit")) {
//                        Platform.exit();
                        System.exit(1);
                    }
                }
            };
            // create a popup menu
            PopupMenu popup = new PopupMenu();
            // create menu item for the default action
            MenuItem item1 = new MenuItem("item1");
            item1.setActionCommand("item1_action");
            item1.addActionListener(listener);
            popup.add(item1);

            MenuItem item2 = new MenuItem("item1");
            item2.setActionCommand("item2_action");
            item2.addActionListener(listener);
            popup.add(item2);

            MenuItem exit = new MenuItem("exit");
            exit.setActionCommand("exit");
            exit.addActionListener(listener);
            popup.add(exit);

            java.awt.event.ActionListener trayListener = new ActionListener() {
                public void actionPerformed(java.awt.event.ActionEvent e) {
                    if (primaryStage.isShowing()) {
                        Platform.runLater(new Runnable() {
                            @Override
                            public void run() {
                                primaryStage.hide();
                            }
                        });
                    } else {
                        Platform.runLater(new Runnable() {
                            @Override
                            public void run() {
                                primaryStage.show();
                            }
                        });
                    }
                }
            };

            // construct a TrayIcon
            trayIcon = new TrayIcon(image, "Tray Demo", popup);
            // set the TrayIcon properties
            trayIcon.addActionListener(trayListener);
            // add the tray image
            try {
                tray.add(trayIcon);
            } catch (AWTException e) {
                System.err.println(e);
            }
        } else {
            // disable tray option in your application or
            // perform other actions
        }
    }

最后提醒一下:Windows的托盘图标最好是16*16的PNG图像。

10.创建一个子窗口

在JavaFX中,窗口一般用Stage代表,所以创建一个子窗口也就是创建一个新的Stage,然后显示这个Stage。

创建Stage也有两种方法,要么直接写代码来布局Scene,要么写fxml文档,然后读入此布局文件。两种方法的代码如下:

/**
 * 创建一个新的Stage,即新的窗口:使用代码来描绘窗口
 * @param actionEvent
 */
public void createStage(ActionEvent actionEvent) {
    final Stage stage = new Stage();

    //create root node of scene, i.e. group
    Group rootGroup = new Group();

    //create scene with set width, height and color
    Scene scene = new Scene(rootGroup, 200, 200, Color.WHITESMOKE);

    //set scene to stage
    stage.setScene(scene);

    //set title to stage
    stage.setTitle("New stage");

    //center stage on screen
    stage.centerOnScreen();

    //show the stage
    stage.show();

    //add some node to scene
    Text text = new Text(20, 110, "JavaFX");
    text.setFill(Color.DODGERBLUE);
    text.setEffect(new Lighting());
    text.setFont(Font.font(Font.getDefault().getFamily(), 50));

    //add text to the main root group
    rootGroup.getChildren().add(text);
}

/**
 * 创建一个Stage,这个Stage使用fxml描绘
 * @param actionEvent
 */
public void createFxmlStage(ActionEvent actionEvent) throws IOException {
    final Stage stage = new Stage();

    FXMLLoader loader = new FXMLLoader(getClass().getResource("sonWindow.fxml")        );
    Parent root = loader.load();

    stage.setTitle("new Stage");
    stage.setScene(new Scene(root, 640, 480));
    stage.show();
}

11.为各种控件设置图标/图形

主窗口图标的设置代码如下:

Image image = new Image(getClass().getResourceAsStream("/main.png"));
primaryStage.getIcons().add(image);

为Button设置图形的代码:

Image image = new Image(getClass().getResourceAsStream("/main.png"));
usrBtn.setGraphic(new ImageView(image));

为ComboBox的每一个选择项设置不同图标的代码(注意,不同的图标有不用的文件名,例如icon_1.png,icon_2.png...,默认的用icon_na.png):

statusComboBox.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
    public ListCell<String> call(ListView<String> p) {
        return new ListCell<String>() {
            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                setText(item);
                if (item == null || empty) {
                    setGraphic(null);
                } else {
                    Image icon;
                    try {
                        int iconNumber = this.getIndex() + 1;
                        String iconPath = "/icon_" + iconNumber + ".png";
                        icon = new Image(getClass().getResourceAsStream(iconPath));
                    } catch (NullPointerException ex) {
                        // in case the above image doesn't exist, use a default one
                        String iconPath = "/icon_na.png";
                        icon = new Image(getClass().getResourceAsStream(iconPath));
                    }
                    ImageView iconImageView = new ImageView(icon);
                    iconImageView.setFitHeight(30);
                    iconImageView.setPreserveRatio(true);
                    setGraphic(iconImageView);
                }
            }
        };
    }
});

12.自定义TreeView或ListView的Cell界面

在Android开发中经常碰到这种情况,一般是写一个layout,然后在适当的代码位置处加载此layout。在JavaFx中,情况略有不同。其流程一般是:

1.调用treeView的setCellFactory方法,设置CellFactory。

2.自定义一个类继承TreeCell<> 类,并重载其updateItem方法;

3.在updateItem方法中,用代码写好界面(一般是把几个控件组合到一起,放入一个界面容器中),然后调用setGraphic方法将此容器设置为Cell的界面。

我修改了一段网上的代码,效果如下:

代码中,我将一个Button和一个Label组合到一个HBox中,然后将HBox设置为Cell的界面。其中还添加了Button的响应函数和ContextMenu,代码如下:

import java.util.Arrays;
import java.util.List;

import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.ActionEvent;
import javafx.scene.layout.VBox;

public class TreeViewSample extends Application {

    private final Node rootIcon =
            new ImageView(new Image(getClass().getResourceAsStream("/my_res/root.png")));
    private final Image depIcon =
            new Image(getClass().getResourceAsStream("/my_res/hello.png"));
    List<Employee> employees = Arrays.<Employee>asList(
            new Employee("Jacob Smith", "Accounts Department"),
            new Employee("Isabella Johnson", "Accounts Department"),
            new Employee("Ethan Williams", "Sales Department"),
            new Employee("Emma Jones", "Sales Department"),
            new Employee("Michael Brown", "Sales Department"),
            new Employee("Anna Black", "Sales Department"),
            new Employee("Rodger York", "Sales Department"),
            new Employee("Susan Collins", "Sales Department"),
            new Employee("Mike Graham", "IT Support"),
            new Employee("Judy Mayer", "IT Support"),
            new Employee("Gregory Smith", "IT Support"));
    TreeItem<String> rootNode =
            new TreeItem<>("MyCompany Human Resources", rootIcon);

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

    @Override
    public void start(Stage stage) {
        rootNode.setExpanded(true);
        for (Employee employee : employees) {
            TreeItem<String> empLeaf = new TreeItem<>(employee.getName());
            boolean found = false;
            for (TreeItem<String> depNode : rootNode.getChildren()) {
                if (depNode.getValue().contentEquals(employee.getDepartment())) {
                    depNode.getChildren().add(empLeaf);
                    found = true;
                    break;
                }
            }
            if (!found) {
                TreeItem depNode = new TreeItem(employee.getDepartment(), new ImageView(depIcon));
                rootNode.getChildren().add(depNode);
                depNode.getChildren().add(empLeaf);
            }
        }

        stage.setTitle("Tree View Sample");
        VBox box = new VBox();
        final Scene scene = new Scene(box, 400, 300);
        scene.setFill(Color.LIGHTBLUE);

        TreeView<String> treeView = new TreeView<>(rootNode);
        treeView.setEditable(true);
        treeView.setCellFactory((TreeView<String> p) ->
                new MyTreeCellImpl());

        box.getChildren().add(treeView);
        stage.setScene(scene);
        stage.show();
    }

    private final class MyTreeCellImpl extends TreeCell<String> {
        private Label textField;
        private Button button;
        private HBox hBox;
        private final ContextMenu contextMenu = new ContextMenu();

        public MyTreeCellImpl() {
            //自定义TreeCell的界面
            hBox = new HBox();
            button = new Button("Alex");
            textField = new Label();
            hBox.getChildren().add(button);
            hBox.getChildren().add(textField);
            button.setOnAction((ActionEvent t) -> {
                Alert alert = new Alert(Alert.AlertType.INFORMATION);
                alert.setTitle("PressButton");
                alert.setHeaderText("PressButton");
                alert.setContentText("Button in a TreeCell, index = " + this.getIndex());
                alert.show();
            });
            MenuItem addMenuItem = new MenuItem("Add Employee");
            MenuItem delMenuItem = new MenuItem("Delete Employee");
            contextMenu.getItems().add(addMenuItem);
            contextMenu.getItems().add(delMenuItem);
            addMenuItem.setOnAction((ActionEvent t) -> {
                TreeItem newEmployee =                        new TreeItem<>("New Employee");
                getTreeItem().getChildren().add(newEmployee);
            });
            delMenuItem.setOnAction((ActionEvent t) -> {
                TreeItem node = getTreeView().getSelectionModel().getSelectedItem();
                TreeItem parent = node.getParent();
                parent.getChildren().remove(node);
            });
        }

        /**
         * 重载此方法来自定义Cell的界面
         * @param item
         * @param empty
         */
        @Override
        protected void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            if (empty || item == null) {
                setText(null);
                setGraphic(null);
            } else {
                textField.setText(item.toString());
                //此处将hBox设置为Cell的界面
                setGraphic(hBox);
                setContextMenu(contextMenu);
            }
        }
    }

    public static class Employee {
        private final SimpleStringProperty name;
        private final SimpleStringProperty department;

        private Employee(String name, String department) {
            this.name = new SimpleStringProperty(name);
            this.department = new SimpleStringProperty(department);
        }

        public String getName() {
            return name.get();
        }

        public void setName(String fName) {
            name.set(fName);
        }

        public String getDepartment() {
            return department.get();
        }

        public void setDepartment(String fName) {
            department.set(fName);
        }
    }
}

由于本文太长,已经不适合阅读,因此第一篇到此结束,明天开第二篇,继续挖坑。

 
posted @ 2021-06-29 19:30  小弟阿德  阅读(1209)  评论(0)    收藏  举报