Javafx进展不够快

时间:2017-02-07 19:56:26

标签: java javafx

我正在逐行解析大文件,其中文件大小可以从几千行到几百万行。当文件很小(例如,几万行)时,所有组件(下图中用红色箭头指示的那些组件)都会逐行更新,从而真实地指示过程的位置。当文件很大时,更新会有很大的延迟,在指标反映进度之前可能需要30秒!我可以手动调整大小以刷新显示。

enter image description here

逐行传输文件时调用的方法是updateProgress()。在那里,progressPrevVal从0变为1(每个增量为1.0 / 2,675,150),如果我们在图像中使用示例,则lineNum从1变为2,675,150。

以下是整个代码:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;

public class DxProgressIndicator extends Application {

private static ProgressBar pb;
private static ProgressIndicator pi;
private static TextField tfNumbers;
private static Label lblLineNumVal;
private static Label lblAllLines;
private static double progressIncrement;
private static double progressPrevVal;
private static int lineNum;
private static Label lblFile;

@Override
public void start(Stage stage) {
    Group root = new Group();
    Scene scene = new Scene(root);
    stage.setScene(scene);
    stage.setTitle("Dexter Parser");

    lblFile = new Label();
    final HBox hbFile = new HBox();
    hbFile.setAlignment(Pos.CENTER_LEFT);
    Label lblCurrentFile = new Label("Current file: ");
    lblCurrentFile.setMinWidth(90);
    lblCurrentFile.setFont(Font.font(null, FontWeight.BOLD, 13));
    hbFile.getChildren().addAll(lblCurrentFile, lblFile);

    pb = new ProgressBar(0);
    pb.setMinWidth(450);
    pi = new ProgressIndicator(0);

    final HBox hbPis = new HBox(10);
    hbPis.setAlignment(Pos.CENTER_LEFT);
    hbPis.getChildren().addAll(pb, pi);

    lblLineNumVal = new Label();
    lblLineNumVal.setMaxWidth(200);

    Label slash = new Label("  /  ");

    lblAllLines = new Label();

    final HBox hbLines = new HBox();
    hbLines.setAlignment(Pos.CENTER_LEFT);
    Label lblLineNum = new Label("    Currently parsing Line # :   ");
    lblLineNum.setFont(Font.font(null, FontWeight.BOLD, 12));
    hbLines.getChildren().addAll(lblLineNum, lblLineNumVal, slash, lblAllLines);

    tfNumbers = new TextField();
    tfNumbers.setEditable(false);
    tfNumbers.setMaxWidth(100);

    final HBox hbTrxValues = new HBox();
    hbTrxValues.setAlignment(Pos.CENTER_LEFT);
    Label lblNumbers = new Label("(transaction, step, record) = ");
    lblNumbers.setFont(Font.font(null, FontWeight.BOLD, 12));
    hbTrxValues.getChildren().addAll(lblNumbers, tfNumbers);

    final VBox vb = new VBox(10);
    vb.getChildren().addAll(hbFile, hbPis, hbLines, hbTrxValues);
    vb.setPadding(new Insets(10));

    scene.setRoot(vb);
    stage.show();
}

public static void updateProgress() {
    Platform.runLater(() -> {
        progressPrevVal += progressIncrement;
        pb.setProgress(progressPrevVal);
        pi.setProgress(progressPrevVal);
        lblLineNumVal.setText(Integer.toString(lineNum++));
    });
}

public static void setFileMetadata(String str) {
    Platform.runLater(() -> {
        lblAllLines.setText(str);
        progressIncrement = 1 / Double.valueOf(str);
        progressPrevVal = 0d;
        lineNum = 1;
    });     
}

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

从客户端的主要方法,一旦我在自己的线程中启动GUI,就像这样:

    Runnable task = () -> {
        DxProgressIndicator.main(null);
    };
    Thread thread = new Thread(task);
    thread.start();

然后我可以浏览文件,逐行流式传输:

    try (Stream<String> stream = Files.lines(file)) {
        stream.forEach(line -> {
            DxProgressIndicator.updateProgress();
            // a bunch of stuff to do with the line
        });
    } catch (IOException e) {
    }

那么,我错过了什么?我如何强制刷新以便组件在每次迭代时平滑更新,而不是在处理非常大的文件时每隔几分钟更新一次?

谢谢。

2 个答案:

答案 0 :(得分:1)

使用Task,并调用updateProgress()以便以限制Platform.runLater()的调用的方式更新进度,以免过多地使FX应用程序线程泛滥要执行的更新。

所以你可以这样做:

public class ParseTask extends Task<Void> {

    private final Path file ;
    private long totalLines ;

    public ParseTask(...) {
        file = ... ;
        totalLines = ... ;
    }

    public Void call() throws IOException {

        // better to use AtomicLong here in case you parallelize the parsing 
        // at any point...
        AtomicLong linesRead = new AtomicLong() ;

        try (Stream<String> stream = Files.lines(file)) {
            stream.forEach(line -> {
                updateProgress(linesRead.incrementAndGet(), totalLines);
                // do stuff with line...
            });
        }

        return null ;
    }

    public long getTotalLines() {
        return totalLines ;
    }
}

然后

public class DxProgressIndicator {

    private final VBox vb ;
    private ProgressBar pb;
    private ProgressIndicator pi;
    private TextField tfNumbers;
    private Label lblLineNumVal;
    private Label lblAllLines;
    private double progressIncrement;
    private double progressPrevVal;
    private int lineNum;
    private Label lblFile;

    private final DoubleProperty progress = new SimpleDoubleProperty();

    public DoubleProperty progressProperty() {
        return progress ;
    }

    public final double getProgress() {
        return progressProperty().get();
    }

    public final void setProgress(double progress) {
        progressProperty().set(progress);
    }

    public DxProgressIndicator() {

        lblFile = new Label();
        final HBox hbFile = new HBox();
        hbFile.setAlignment(Pos.CENTER_LEFT);
        Label lblCurrentFile = new Label("Current file: ");
        lblCurrentFile.setMinWidth(90);
        lblCurrentFile.setFont(Font.font(null, FontWeight.BOLD, 13));
        hbFile.getChildren().addAll(lblCurrentFile, lblFile);

        pb = new ProgressBar(0);
        pb.setMinWidth(450);

        pi = new ProgressIndicator(0);

        pb.progressProperty().bind(progress);
        pi.progressProperty().bind(progress);

        final HBox hbPis = new HBox(10);
        hbPis.setAlignment(Pos.CENTER_LEFT);
        hbPis.getChildren().addAll(pb, pi);

        lblLineNumVal = new Label();
        lblLineNumVal.setMaxWidth(200);

        Label slash = new Label("  /  ");

        lblAllLines = new Label();

        final HBox hbLines = new HBox();
        hbLines.setAlignment(Pos.CENTER_LEFT);
        Label lblLineNum = new Label("    Currently parsing Line # :   ");
        lblLineNum.setFont(Font.font(null, FontWeight.BOLD, 12));
        hbLines.getChildren().addAll(lblLineNum, lblLineNumVal, slash, lblAllLines);

        tfNumbers = new TextField();
        tfNumbers.setEditable(false);
        tfNumbers.setMaxWidth(100);

        final HBox hbTrxValues = new HBox();
        hbTrxValues.setAlignment(Pos.CENTER_LEFT);
        Label lblNumbers = new Label("(transaction, step, record) = ");
        lblNumbers.setFont(Font.font(null, FontWeight.BOLD, 12));
        hbTrxValues.getChildren().addAll(lblNumbers, tfNumbers);

        vb = new VBox(10);
        vb.getChildren().addAll(hbFile, hbPis, hbLines, hbTrxValues);
        vb.setPadding(new Insets(10));

    }

    public Parent getRoot() {
        return vb ;
    }

    public void setTotalLines(long totalLines) {
        lblAllLines.setText(Long.toString(totalLines));
    }

}

然后只是

public class MyApp extends Application {

    @Override
    public void start(Stage primaryStage) {

        ParseTask task = new ParseTask(...);

        DxProgressIndicator indicator = new DxProgressIndicator();
        indicator.setTotalLines(task.getTotalLines());
        indicator.progressProperty().bind(task.progressProperty());

        Scene scene = new Scene(indicator.getRoot());
        primaryStage.setScene(scene);
        primaryStage.setTitle("Dexter Parser");

        primaryStage.show();

        new Thread(task).start();
    }
}

显然你可能需要修补这个细节 - 例如你可以updateMessage(linesRead + " / " + totalLines);并将标签的文本绑定到任务的messageProperty() - 但它应该给你基本的想法。

答案 1 :(得分:1)

javaFX Task类使用原子引用来合并更新,以便它们不会泛滥事件队列(这会导致您的问题)。您可以轻松地在代码中实现此机制。

首先你需要一个助手类:

private static final class ProgressUpdate {
     private final double workDone;        

     private ProgressUpdate(double p) {
          this.workDone = p;            
     }
}

然后你需要一个存储对这个类的对象的原子引用的字段:

private static final AtomicReference<ProgressUpdate> progressUpdate = new AtomicReference<>();

最后你可以像这样调整你的updateProgress()

public static void updateProgress() {
    progressPrevVal += progressIncrement;
    if(progressUpdate.getAndSet(new ProgressUpdate(progressPrevVal)) == null){      
        Platform.runLater(() -> {
            ProgressUpdate update = progressUpdate.getAndSet(null);
            pb.setProgress(update.workDone);
            pi.setProgress(update.workDone);
            lblLineNumVal.setText(Integer.toString(lineNum++));
        });
    }
}

这样,如果GUI已经消耗了前一个应用程序线程,那么只有一个新的更新排队等候FX应用程序线程。

但是,您最好只是为了了解javaFX如何在内部处理问题,然后以可以使用内置Task类javaFX的方式重新设计代码。 ,这将自动解决您当前和可能的其他一些问题。