multithreading - JavaFX使条形图通过UI线程中的延迟更改条形图

标签 multithreading sorting user-interface javafx task

我有JavaFX主线程,在这里创建了新线程,该线程扩展了Task并进行了排序和替换。一切都很好,但是我想在替换时进行一些延迟(例如100ms),以逐步显示排序,也许是动画。问题是当我使用Thread.sleep()或TranslateTransition()时,它只是将所有延迟毫秒数加在一起,就变成了在更改小节之前发生的一大延迟。如何使延迟在UI线程中正常工作?

在主要类(class):

       Sorting sorting = new Sorting();
       sortThread = new Thread(sorting, "sort");
       sortThread.start();
       sortThread.join();

而我的类排序扩展了Task
  public class Sorting extends Task<Void> {

  //some stuff here

    @Override
    protected Void call() throws Exception {


        taskThread = new Thread(counter, "time");
        taskThread.setDaemon(true);
        taskThread.start();

        int n = array_tmp.length;
        int  temp;
        for (int i = 0; i < n; i++) {
            for (int j = 1; j < (n - i); j++) {
                if (array_tmp[j - 1] > array_tmp[j]) {

                        //replacing bars
                        Node n1 = barChart.getData().get(j-1).getData().get(0).getNode();
                        Node n2 = barChart.getData().get(j).getData().get(0).getNode();

                        double x1 = n1.getTranslateX() + ((barChart.getWidth()-69)/array_tmp.length);
                        double x2 = n2.getTranslateX() - ((barChart.getWidth()-69)/array_tmp.length);

                        n1.setTranslateX(x1);
                        n2.setTranslateX(x2);

                        barChart.getData().get(j-1).getData().get(0).setNode(n2);
                        barChart.getData().get(j).getData().get(0).setNode(n1);


                    temp = array_tmp[j - 1];
                    array_tmp[j - 1] = array_tmp[j];
                    array_tmp[j] = temp;
                }
            }
        }
  }
     }

最佳答案

JavaFX中有两个基本的线程化规则:

只能从JavaFX应用程序线程访问作为实际显示的场景图一部分的

  • UI组件(节点)。其他一些操作(例如,创建新的Stage)也要遵守此规则。
  • 任何长时间运行或阻塞的操作都应在后台线程(即不是JavaFX应用程序线程)上运行。这是因为JavaFX应用程序线程是呈现UI和响应用户交互所需的线程。因此,如果您阻止了FX Application Thread,则无法呈现UI,并且应用程序将变得无响应,直到您的操作完成为止。

  • javafx.concurrent API提供了用于管理可在后台线程上运行的代码以及在FX应用程序线程上执行回调的工具。

    javafx.animation API还提供允许在特定时间在JavaFX应用程序线程上执行UI代码的类。请注意,动画API完全避免创建后台线程。

    因此,对于您的用例,如果要对条形图中的两个条形进行动画处理,可以使用animation API进行。创建执行这种交换的动画的通用方法可能如下所示:
    private <T> Animation createSwapAnimation(Data<?, T> first, Data<?, T> second) {
        double firstX = first.getNode().getParent().localToScene(first.getNode().getBoundsInParent()).getMinX();
        double secondX = first.getNode().getParent().localToScene(second.getNode().getBoundsInParent()).getMinX();
    
        double firstStartTranslate = first.getNode().getTranslateX();
        double secondStartTranslate = second.getNode().getTranslateX();
    
        TranslateTransition firstTranslate = new TranslateTransition(Duration.millis(500), first.getNode());
        firstTranslate.setByX(secondX - firstX);
        TranslateTransition secondTranslate = new TranslateTransition(Duration.millis(500), second.getNode());
        secondTranslate.setByX(firstX - secondX);
        ParallelTransition translate = new ParallelTransition(firstTranslate, secondTranslate);
    
        translate.statusProperty().addListener((obs, oldStatus, newStatus) -> {
            if (oldStatus == Animation.Status.RUNNING) {
                T temp = first.getYValue();
                first.setYValue(second.getYValue());
                second.setYValue(temp);
                first.getNode().setTranslateX(firstStartTranslate);
                second.getNode().setTranslateX(secondStartTranslate);
            }
        });
    
        return translate;
    }
    

    这里的基本思想很简单:我们在两个节点之间的x坐标中测量距离;记下它们当前的translateX属性,然后创建两个过渡来移动节点,使它们彼此占据位置。这两个转换是并行执行的。过渡完成后(由过渡状态从RUNNING更改为其他状态表示),图表中的值将交换,并且translateX属性重置为先前的值(这些效果将在视觉上抵消,但现在图表数据将反射(reflect)出两者已被交换的事实)。

    如果您想执行一个排序算法,使排序过程中的交换动起来,并在算法的每个步骤之间暂停,则可以使用背景线程来执行此操作(您也可以通过动画来执行此操作-但这似乎很简单并且可能更具指导性)。

    这里的想法是创建一个Task,其call()方法执行排序算法,在各个点处暂停以使用户可以查看正在发生的事情。因为我们正在暂停(阻塞),所以不能在FX Application Thread上运行它,因为阻塞会阻止UI更新,直到整个过程完成为止。

    这是冒泡排序的实现(为简单起见)。在排序的每个迭代中,我们:
  • 以绿色突出显示要比较的两个条形*
  • 暂停,以便用户可以看到
  • 如果需要交换值:
  • 获取上面定义的动画并运行它*
  • 再次暂停,然后
  • 重置颜色*。

  • 上面的伪代码中标记为*的步骤更改了UI,因此它们必须在FX Application线程上执行,因此需要将它们包装在对 Platform.runLater(...) 的调用中,这将使所提供的代码在FX Application线程上执行。

    最后一个棘手的部分(这是非常棘手的)是动画当然需要一些时间来执行。因此,我们必须安排背景线程等待动画完成。为此,我们创建了一个CountDownLatch(计数为1)。动画完成后,我们将闩锁递减计数。然后,在将动画提交给Platform.runLater(..)之后,我们的后台线程通过调用latch.await()来等待闩锁递减计数,然后继续操作。后台线程需要等待某些东西在FX Application Thread上运行是很不寻常的,但这是一种在需要时执行此操作的技术。

    因此,冒泡排序的实现看起来像
    private Task<Void> createSortingTask(Series<String, Number> series) {
        return new Task<Void>() {
            @Override
            protected Void call() throws Exception {
    
                ObservableList<Data<String, Number>> data = series.getData();
                for (int i = data.size() - 1; i >= 0; i--) {
                    for (int j = 0 ; j < i; j++) {
    
                        Data<String, Number> first = data.get(j);
                        Data<String, Number> second = data.get(j + 1);
    
                        Platform.runLater(() -> {
                            first.getNode().setStyle("-fx-background-color: green ;");
                            second.getNode().setStyle("-fx-background-color: green ;");
                        });
    
                        Thread.sleep(500);
    
                        if (first.getYValue().doubleValue() > second.getYValue().doubleValue()) {
                            CountDownLatch latch = new CountDownLatch(1);
                            Platform.runLater(() -> {
                                Animation swap = createSwapAnimation(first, second);
                                swap.setOnFinished(e -> latch.countDown());
                                swap.play();
                            });
                            latch.await();
                        }
                        Thread.sleep(500);
    
                        Platform.runLater(() -> {
                            first.getNode().setStyle("");
                            second.getNode().setStyle("");
                        });
                    }
                }
                return null;
            }
        };
    }
    

    这是一个完整的演示。由于排序算法及其暂停被封装为Task,因此我们可以根据需要利用其回调和状态属性。例如,我们在启动任务之前禁用按钮,并使用onSucceeded处理程序在完成任务时再次启用它们。添加“取消”选项也很容易。
    import java.util.Random;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    import javafx.animation.Animation;
    import javafx.animation.ParallelTransition;
    import javafx.animation.TranslateTransition;
    import javafx.application.Application;
    import javafx.application.Platform;
    import javafx.collections.ObservableList;
    import javafx.concurrent.Task;
    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.chart.BarChart;
    import javafx.scene.chart.CategoryAxis;
    import javafx.scene.chart.NumberAxis;
    import javafx.scene.chart.XYChart.Data;
    import javafx.scene.chart.XYChart.Series;
    import javafx.scene.control.Button;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.HBox;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    public class AnimatedBubbleSort extends Application {
    
        private Random rng = new Random();
    
        private ExecutorService exec = Executors.newCachedThreadPool(runnable -> {
            Thread t = new Thread(runnable);
            t.setDaemon(true);
            return t;
        });
    
        @Override
        public void start(Stage primaryStage) {
            BarChart<String, Number> chart = new BarChart<>(new CategoryAxis(), new NumberAxis());
            chart.setAnimated(false);
            Series<String, Number> series = generateRandomIntegerSeries(10);
            chart.getData().add(series);
    
            Button sort = new Button("Sort");
    
            Button reset = new Button("Reset");
            reset.setOnAction(e -> chart.getData().set(0, generateRandomIntegerSeries(10)));
    
            HBox buttons = new HBox(5, sort, reset);
            buttons.setAlignment(Pos.CENTER);
            buttons.setPadding(new Insets(5));
    
            sort.setOnAction(e -> {
                Task<Void> animateSortTask = createSortingTask(chart.getData().get(0));
                buttons.setDisable(true);
                animateSortTask.setOnSucceeded(event -> buttons.setDisable(false));
                exec.submit(animateSortTask);
            });
    
            BorderPane root = new BorderPane(chart);
            root.setBottom(buttons);
            Scene scene = new Scene(root);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        private Task<Void> createSortingTask(Series<String, Number> series) {
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
    
                    ObservableList<Data<String, Number>> data = series.getData();
                    for (int i = data.size() - 1; i >= 0; i--) {
                        for (int j = 0 ; j < i; j++) {
    
                            Data<String, Number> first = data.get(j);
                            Data<String, Number> second = data.get(j + 1);
    
                            Platform.runLater(() -> {
                                first.getNode().setStyle("-fx-background-color: green ;");
                                second.getNode().setStyle("-fx-background-color: green ;");
                            });
    
                            Thread.sleep(500);
    
                            if (first.getYValue().doubleValue() > second.getYValue().doubleValue()) {
                                CountDownLatch latch = new CountDownLatch(1);
                                Platform.runLater(() -> {
                                    Animation swap = createSwapAnimation(first, second);
                                    swap.setOnFinished(e -> latch.countDown());
                                    swap.play();
                                });
                                latch.await();
                            }
                            Thread.sleep(500);
    
                            Platform.runLater(() -> {
                                first.getNode().setStyle("");
                                second.getNode().setStyle("");
                            });
                        }
                    }
                    return null;
                }
            };
        }
    
        private <T> Animation createSwapAnimation(Data<?, T> first, Data<?, T> second) {
            double firstX = first.getNode().getParent().localToScene(first.getNode().getBoundsInParent()).getMinX();
            double secondX = first.getNode().getParent().localToScene(second.getNode().getBoundsInParent()).getMinX();
    
            double firstStartTranslate = first.getNode().getTranslateX();
            double secondStartTranslate = second.getNode().getTranslateX();
    
            TranslateTransition firstTranslate = new TranslateTransition(Duration.millis(500), first.getNode());
            firstTranslate.setByX(secondX - firstX);
            TranslateTransition secondTranslate = new TranslateTransition(Duration.millis(500), second.getNode());
            secondTranslate.setByX(firstX - secondX);
            ParallelTransition translate = new ParallelTransition(firstTranslate, secondTranslate);
    
            translate.statusProperty().addListener((obs, oldStatus, newStatus) -> {
                if (oldStatus == Animation.Status.RUNNING) {
                    T temp = first.getYValue();
                    first.setYValue(second.getYValue());
                    second.setYValue(temp);
                    first.getNode().setTranslateX(firstStartTranslate);
                    second.getNode().setTranslateX(secondStartTranslate);
                }
            });
    
            return translate;
        }
    
        private Series<String, Number> generateRandomIntegerSeries(int n) {
            Series<String, Number> series = new Series<>();
            for (int i = 1; i <= n; i++) {
                series.getData().add(new Data<>(Integer.toString(i), rng.nextInt(90) + 10));
            }
            return series;
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    enter image description here

    关于multithreading - JavaFX使条形图通过UI线程中的延迟更改条形图,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48789811/

    相关文章:

    java - OS X Java 每个进程的最大线程数

    arrays - Julia:使用自定义比较器按行对矩阵进行排序

    java - 使用非总排序标准对 java 流进行排序。

    user-interface - mask UI 图像/原始图像

    c# - 在 C# 中持续运行进程的最佳实践

    winapi - 如何在win32中使用线程功能显示进度条?

    linux - *_r UNIX 调用是可重入的(异步信号安全的)、线程安全的还是两者兼而有之?

    sql-server - 在递归 SQL 查询上对多个父/子进行排序

    python - 您使用哪种 IDE(如果有)构建 python GUI 项目?

    ios - 如何在预定义的矩形中显示图像的缩放部分?