【问题标题】:Timer in Javafx Creating FX Thread null pointers and out of bounds exceptionsJavafx中的计时器创建FX线程空指针和越界异常
【发布时间】:2015-08-20 02:00:53
【问题描述】:

我试图在 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;

    }

}

这是图形用户界面

 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)

【问题讨论】:

  • 你能发布堆栈跟踪吗?我认为这会给出一个IllegalStateException,因为您从后台线程修改了场景图,但您可能首先遇到了其他错误。
  • 堆栈跟踪非常长,但我会添加一小部分
  • 确保包含引用您编写的代码的第一部分(并确定它从您的代码中指向的行)
  • OK 一秒跟踪太长,顶部实际上不是顶部
  • 是的,很奇怪。显然TextFields 不会检查您是否遵守线程规则。将remainingTimeTF 更改为标签,您将获得更有用的异常。

标签: timer javafx


【解决方案1】:

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

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

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

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

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

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

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

【讨论】:

  • 是的,你是对的,我会试试这个,这是个好主意!
  • 完美运行,谢谢!
【解决方案2】:

首先,如果您确实需要以您正在执行的方式更新该 TextField,我建议您添加 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。

希望这会有所帮助!

【讨论】:

  • 是的,如果您在此处使用Platform.runLater(...),则必须包含Thread.sleep(...)。否则,糟糕的 FX 应用程序线程仍将尝试执行更新很长时间。但是,最好使用来自Task 的内置工具,它可以为您解决很多此类问题。
  • 是的!我在没有 Thread.sleep() 的情况下尝试过这个,我没有收到任何错误,但我的程序只是冻结了哈哈!现在一切都说得通了
  • 很高兴这有帮助!继续学习,正如 James 建议的那样,最好坚持使用 API 为您提供而不是重新发明轮子 :)
猜你喜欢
  • 1970-01-01
  • 2012-04-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-18
  • 2016-07-11
相关资源
最近更新 更多