Javafx中的计时器创建FX线程空指针和越界异常

时间:2015-08-20 02:00:54

标签: timer javafx

我试图在Javafx中使用GUI和Timer类创建一个简单的计时器。当按下开始按钮时,它应该从输入时间倒计时并在达到0时停止。

我有剩余的时间(以毫秒为单位)在GUI上的TextField中更新,但它只运行一个随机数毫秒,通常在100-200之间,然后冻结并抛出非常多的异常。

我试图查明错误的来源,并发现还存在并发修改异常。

这是我的Timer类:

import java.sql.Time;

/**
 * Created by Josh on 19/8/2015.
 */
public class Timer {

    private long endTimeMillis;

    public Timer(long hours, long minutes, long seconds){

        long currentTime = System.currentTimeMillis();

        endTimeMillis = ((seconds*1000 + minutes*1000*60 + hours*1000*60*60) + currentTime);

    }

    public boolean isFinished(){

        long currentTime = System.currentTimeMillis();

        if(endTimeMillis - currentTime <= 0){
            return true;
        }else{
            return false;
        }

    }

    public long getRemainingTimeMillis(){

        long currentTime = System.currentTimeMillis();

        return endTimeMillis - currentTime;

    }

}

这是GUI

 import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;

/**
 * Created by Josh on 19/8/2015.
 */
public class GUI extends Application {

    private BorderPane root;

    Label hoursLabel, minutesLabel, secondsLabel;
    TextField hoursTF, minutesTF, secondsTF;

    Button startButton;

    Label remainingTimeLabel;
    TextField remainingTimeTF;

    long remainingTime;

    @Override
    public void start(Stage primaryStage) throws Exception {

        root = new BorderPane();
        primaryStage.setScene(new Scene(root, 1300, 125));
        primaryStage.show();
        primaryStage.setResizable(true);
        primaryStage.setTitle("Timer");
        primaryStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
            @Override
            public void handle(WindowEvent event) {
                event.consume();
                System.exit(0);
            }
        });

        FlowPane center = new FlowPane();
        hoursLabel = new Label("     Hours: ");
        hoursTF = new TextField();
        minutesLabel = new Label("     Minutes: ");
        minutesTF = new TextField();
        secondsLabel = new Label("     Seconds: ");
        secondsTF = new TextField();
        center.getChildren().addAll(hoursLabel, hoursTF, minutesLabel, minutesTF, secondsLabel, secondsTF);
        root.setCenter(center);

        FlowPane bottom = new FlowPane();
        bottom.setAlignment(Pos.CENTER);
        startButton = new Button("Start");
        startButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                start();
            }
        });
        remainingTimeLabel = new Label("     Time Remaining: ");
        remainingTimeTF = new TextField();
        bottom.getChildren().addAll(startButton, remainingTimeLabel, remainingTimeTF);
        root.setBottom(bottom);


    }



    public void start(){

        Task timer = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                long hours = 0, minutes = 0, seconds = 0;

                try{
                    if(hoursTF.getText() == null || hoursTF.getText().equals("")){
                        hours = 0;
                    }else{
                        hours = Long.parseLong(hoursTF.getText());
                    }
                    if(minutesTF.getText() == null || minutesTF.getText().equals("")){
                        minutes = 0;
                    }else{
                        minutes = Long.parseLong(minutesTF.getText());
                    }
                    if(secondsTF.getText() == null || secondsTF.getText().equals("")){
                        seconds = 0;
                    }else{
                        seconds = Long.parseLong(secondsTF.getText());
                    }
                }catch(NumberFormatException e){
                    System.out.println("Error");

                }

                Timer timer = new Timer(hours, minutes, seconds);

                while(!timer.isFinished()){
                    remainingTimeTF.setText(Long.toString(timer.getRemainingTimeMillis()));
                }


                return null;
            }
        };
        new Thread(timer).start();

    }

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

任何帮助将不胜感激!

新堆栈跟踪:

    x.beans.property.StringPropertyBase.set(StringPropertyBase.java:49)
    at javafx.beans.property.StringProperty.setValue(StringProperty.java:65)
    at javafx.scene.control.Labeled.setText(Labeled.java:145)
    at GUI$3.call(GUI.java:109)
    at GUI$3.call(GUI.java:80)
    at javafx.concurrent.Task$TaskCallable.call(Task.java:1423)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.lang.Thread.run(Thread.java:745)
Exception in thread "Thread-4" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-4
    at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:204)
    at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:438)
    at javafx.scene.Parent$2.onProposedChange(Parent.java:364)
    at com.sun.javafx.collections.VetoableListDecorator.setAll(VetoableListDecorator.java:113)
    at com.sun.javafx.collections.VetoableListDecorator.setAll(VetoableListDecorator.java:108)
    at com.sun.javafx.scene.control.skin.LabeledSkinBase.updateChildren(LabeledSkinBase.java:575)
    at com.sun.javafx.scene.control.skin.LabeledSkinBase.handleControlPropertyChanged(LabeledSkinBase.java:204)
    at com.sun.javafx.scene.control.skin.LabelSkin.handleControlPropertyChanged(LabelSkin.java:49)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase.lambda$registerChangeListener$61(BehaviorSkinBase.java:197)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase$$Lambda$102/1442917786.call(Unknown Source)
    at com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler$1.changed(MultiplePropertyChangeListenerHandler.java:55)
    at javafx.beans.value.WeakChangeListener.changed(WeakChangeListener.java:89)
    at com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:182)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.beans.property.StringPropertyBase.fireValueChangedEvent(StringPropertyBase.java:103)
    at javafx.beans.property.StringPropertyBase.markInvalid(StringPropertyBase.java:110)
    at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:144)
    at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:49)
    at javafx.beans.property.StringProperty.setValue(StringProperty.java:65)
    at javafx.scene.control.Labeled.setText(Labeled.java:145)
    at GUI$3.call(GUI.java:109)
    at GUI$3.call(GUI.java:80)
    at javafx.concurrent.Task$TaskCallable.call(Task.java:1423)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.lang.Thread.run(Thread.java:745)

2 个答案:

答案 0 :(得分:2)

首先,如果您确实需要以您的方式更新TextField,我建议您在while时添加Thread.sleep(100);(每秒10次更新),甚至Thread.sleep(1000);(每秒1次更新)因为在每个CPU周期更新该文本字段实际上是CPU密集型的,通常你不需要...

其次(可能是你的例外的原因)调用remainingTimeTF.setText();必须发生在FX线程中,请尝试使用此代码:

Platform.runLater(new Runnable() {
    @Override public void run() {
        remainingTimeTF.setText(Long.toString(timer.getRemainingTimeMillis()));
    }
});

现在你在FX Thead之外设置文本,这可能会导致几个例外,因为更新请求发生在工作线程中,实际的UI修改发生在FX线程中。您需要确保每次需要修改UI时,它都会在正确的线程中发生,在这种情况下是带有Platform.runLater()调用的FX线程。

此外,由于您已经在使用Task类,为什么不绑定text属性并使用updateMessage()或updateTitle()方法呢? 看一下this article,它有正确使用Task和从工作线程更新UI的很好的例子。

希望这有帮助!

答案 1 :(得分:1)

您违反了关于不在后台线程上访问实时场景图中节点状态的规则。大多数JavaFX控件实际上会检查这个并在执行此操作时抛出IllegalStateException:显然TextInputControl.setText(...)没有(大概是出于性能原因)。

最简单的解决方法是更新任务的消息属性:

            while(!timer.isFinished()){
              updateMessage(Long.toString(timer.getRemainingTimeMillis()));
              // remainingTimeTF.setText(Long.toString(timer.getRemainingTimeMillis()));
            }

(这导致在FX应用程序线程上更新消息属性),然后将文本字段的text属性绑定到它:

    remainingTimeTF.textProperty().bind(timer.messageProperty());
    new Thread(timer).start();

但是,您的代码有很多违反良好做法的事情。例如,您应该真正计算任务之外的总时间(即在FX应用程序线程上)并将其存储在final变量中;繁忙的while循环非常糟糕(尽管updateMessage(...)将解决与此相关的大部分问题)。

我建议您阅读JavaFX中的多线程。从Task API docs开始。也许这个问题会有所帮助:Using threads to make database requests