7

I have a javafx application where I have multiple tabs (timer, stopwatch, clock) each with a separate Controller, and the user is able to add and independently start multiple timers using a start/stop button.

I tried binding a TextField to a property of another class MyTimer which keeps track of the elapsed time, but it eventually (after running for a couple of seconds) starts throwing an error. (If you check the code below, note that it only happens if the "Thread.sleep" is set to 10ms - when i increase the delay to 100ms, it kept running for about a minute and did not crash - I did not test further, since I would like to solve the root cause instead of increasing the delay).

Just so you have a quick idea: app image

MyTimer class:

    public class MyTimer implements Startable {

...

    private long startNanoTime, storedElapsedTime, totalTime;
    private TimerStates state;
    private StringProperty timerStringProperty = new SimpleStringProperty(DEFAULT_TIMER_STRING_VALUE); 

    public MyTimer() {
        //constructor
    }

    public long getRemainingTime() {
        //returns remaining time
    }

    public StringProperty timerStringPropertyProperty() {
        return timerStringProperty;
    }

    @Override
        public boolean start() {
            if (this.state.isRunning() ) {
                System.out.println("Already running.");
                return false;
            }

            this.startNanoTime = System.nanoTime();
            this.state = TimerStates.RUNNING;

            Runnable startTimerRunnable = new Runnable() {
                @Override
                public void run() {
                    while(state.isRunning()) {
                        timerStringProperty.set(MyFormatter.longMillisecondsTimeToTimeString(getRemainingTime())); //The parameter passed is simply the remaining time formatted to a String
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            Thread daemonTimer = new Thread(startTimerRunnable);
            daemonTimer.setDaemon(true);
            daemonTimer.start();

            return true;
        }
    }

While trying to implement the binding, I tried to bind the one default TextProperty which exists without any user interaction at application startup to the property representing the remaining time from the MyTimer class to the value in the Controller:

public class TimerTabController {
    ...

    @FXML
    private Tab timerTab;
    @FXML
    private HBox defaultTimerHBox;
    @FXML
    private TextField defaultTimerTextField;

    private Map<HBox, MyTimer> timers = new HashMap<>();

    @FXML
    protected void initialize() {
        MyTimer defaultTimer = new MyTimer();
        timers.put(defaultTimerHBox, defaultTimer);
        defaultTimerTextField.textProperty().bind(defaultTimer.timerStringPropertyProperty());
    }
}

The Main method which starts it all up is fairly standard, but I'll include it anyway:

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("fxml/mainWindow.fxml"));
        primaryStage.setTitle("Mortimer");
        primaryStage.setScene(new Scene(root, 800, 700));
        primaryStage.show();
    }

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

And finally, the stack trace: First, there's this:

Exception in thread "Thread-3" java.lang.NullPointerException
    at javafx.graphics/com.sun.javafx.text.PrismTextLayout.createLine(PrismTextLayout.java:893)
    at javafx.graphics/com.sun.javafx.text.PrismTextLayout.layout(PrismTextLayout.java:1193)
    at javafx.graphics/com.sun.javafx.text.PrismTextLayout.ensureLayout(PrismTextLayout.java:222)
    at javafx.graphics/com.sun.javafx.text.PrismTextLayout.getBounds(PrismTextLayout.java:245)
    at javafx.graphics/javafx.scene.text.Text.getLogicalBounds(Text.java:430)
    at javafx.graphics/javafx.scene.text.Text.getYRendering(Text.java:1085)
    at javafx.graphics/javafx.scene.text.Text.access$4400(Text.java:127)
    at javafx.graphics/javafx.scene.text.Text$TextAttribute$11.computeValue(Text.java:1764)
    at javafx.graphics/javafx.scene.text.Text$TextAttribute$11.computeValue(Text.java:1756)
    at javafx.base/javafx.beans.binding.ObjectBinding.get(ObjectBinding.java:151)
    at javafx.base/javafx.beans.binding.ObjectExpression.getValue(ObjectExpression.java:49)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.get(ObjectPropertyBase.java:133)
    at javafx.controls/javafx.scene.control.skin.TextFieldSkin.lambda$new$4(TextFieldSkin.java:252)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:106)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:113)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.access$000(ObjectPropertyBase.java:52)
    at javafx.base/javafx.beans.property.ObjectPropertyBase$Listener.invalidated(ObjectPropertyBase.java:234)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.base/javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:170)
    at javafx.graphics/javafx.scene.text.Text.doGeomChanged(Text.java:842)
    at javafx.graphics/javafx.scene.text.Text.access$500(Text.java:127)
    at javafx.graphics/javafx.scene.text.Text$1.doGeomChanged(Text.java:158)
    at javafx.graphics/com.sun.javafx.scene.shape.TextHelper.geomChangedImpl(TextHelper.java:106)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.geomChanged(NodeHelper.java:137)
    at javafx.graphics/javafx.scene.text.Text.needsTextLayout(Text.java:266)
    at javafx.graphics/javafx.scene.text.Text.needsFullTextLayout(Text.java:261)
    at javafx.graphics/javafx.scene.text.Text.access$900(Text.java:127)
    at javafx.graphics/javafx.scene.text.Text$3.invalidated(Text.java:461)
    at javafx.base/javafx.beans.property.StringPropertyBase.markInvalid(StringPropertyBase.java:110)
    at javafx.base/javafx.beans.property.StringPropertyBase.access$000(StringPropertyBase.java:50)
    at javafx.base/javafx.beans.property.StringPropertyBase$Listener.invalidated(StringPropertyBase.java:231)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.base/javafx.beans.binding.StringBinding.invalidate(StringBinding.java:169)
    at javafx.base/com.sun.javafx.binding.BindingHelperObserver.invalidated(BindingHelperObserver.java:52)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:348)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.controls/javafx.scene.control.TextInputControl$TextProperty.fireValueChangedEvent(TextInputControl.java:1430)
    at javafx.controls/javafx.scene.control.TextInputControl$TextProperty.markInvalid(TextInputControl.java:1434)
    at javafx.controls/javafx.scene.control.TextInputControl$TextProperty.controlContentHasChanged(TextInputControl.java:1373)
    at javafx.controls/javafx.scene.control.TextInputControl$TextProperty.access$1600(TextInputControl.java:1341)
    at javafx.controls/javafx.scene.control.TextInputControl.lambda$new$0(TextInputControl.java:144)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.controls/javafx.scene.control.TextField$TextFieldContent.insert(TextField.java:87)
    at javafx.controls/javafx.scene.control.TextInputControl.replaceText(TextInputControl.java:1244)
    at javafx.controls/javafx.scene.control.TextInputControl.filterAndSet(TextInputControl.java:1211)
    at javafx.controls/javafx.scene.control.TextInputControl.access$900(TextInputControl.java:80)
    at javafx.controls/javafx.scene.control.TextInputControl$TextProperty.doSet(TextInputControl.java:1451)
    at javafx.controls/javafx.scene.control.TextInputControl$TextProperty.access$1200(TextInputControl.java:1341)
    at javafx.controls/javafx.scene.control.TextInputControl$TextProperty$Listener.invalidated(TextInputControl.java:1474)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.base/javafx.beans.property.StringPropertyBase.fireValueChangedEvent(StringPropertyBase.java:104)
    at javafx.base/javafx.beans.property.StringPropertyBase.markInvalid(StringPropertyBase.java:111)
    at javafx.base/javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:145)
    at javafx.base/javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:50)
    at MorTimer/sample.MyTimer$1.run(MyTimer.java:106)
    at java.base/java.lang.Thread.run(Thread.java:834)

And then the following errors keep repeating:

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at javafx.graphics/com.sun.javafx.text.PrismTextLayout.getRuns(PrismTextLayout.java:235)
    at javafx.graphics/javafx.scene.text.Text.getRuns(Text.java:389)
    at javafx.graphics/javafx.scene.text.Text.updatePGText(Text.java:1460)
    at javafx.graphics/javafx.scene.text.Text.doUpdatePeer(Text.java:1490)
    at javafx.graphics/javafx.scene.text.Text.access$100(Text.java:127)
    at javafx.graphics/javafx.scene.text.Text$1.doUpdatePeer(Text.java:137)
    at javafx.graphics/com.sun.javafx.scene.shape.TextHelper.updatePeerImpl(TextHelper.java:75)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.updatePeer(NodeHelper.java:102)
    at javafx.graphics/javafx.scene.Node.syncPeer(Node.java:710)
    at javafx.graphics/javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2366)
    at javafx.graphics/javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2512)
    at javafx.graphics/com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:412)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at javafx.graphics/com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:411)
    at javafx.graphics/com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:438)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:519)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:499)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:492)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:320)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
    at java.base/java.lang.Thread.run(Thread.java:834)
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at javafx.graphics/javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2365)
    at javafx.graphics/javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2512)
    at javafx.graphics/com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:412)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at javafx.graphics/com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:411)
    at javafx.graphics/com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:438)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:519)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:499)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:492)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:320)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
    at java.base/java.lang.Thread.run(Thread.java:834)

When I used Platform.runLater() in the MyTimer.start() function, it did work, like this:

    Runnable startTimerRunnable = new Runnable() {
        @Override
        public void run() {
            while(state.isRunning()) {
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        timerStringProperty.set(MyFormatter.longMillisecondsTimeToTimeString(getRemainingTime()));
                    }
                });
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };

    Thread daemonTimer = new Thread(startTimerRunnable);
    daemonTimer.setDaemon(true);
    daemonTimer.start();

But it seems wrong to use Platform.runLater like this, or is it fine? I understand that the GUI should not be updated outside of the FX thread, but I thought that binding properties takes care of this - that updating the property bound to a GUI element does not need to take place in the FX thread, and the GUI update would indeed be handled properly on the FX thread as part of the binding...

Ultimately, my solution which seems to work fairly well is to not use binding, but instead update the fields periodically in the Controller itself (the following method is called upon "onSelectionChanged" of the Tabs themselves) - but I was wondering how to make the binding work, as it seems that binding is the better practice. Anyway, here is the code which also does work:

public void startTimeFieldUpdates() {
    Runnable timeTracker = new Runnable() {
        @Override
        public void run() {
            while(timerTab.isSelected()) {
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        for (HBox hBox : timers.keySet()) {
                            if (hBox.getChildren().get(1) instanceof TextField) {
                                TextField currentField = (TextField) hBox.getChildren().get(1);
                                currentField.setText(MyFormatter.longMillisecondsTimeToTimeString(
                                        timers.get(hBox).getRemainingTime())
                                                    );
                            }
                        }
                    }
                });

                try {
                    Thread.sleep(10);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }

            }
        }
    };
    Thread daemonStopwatch = new Thread(timeTracker);

    daemonStopwatch.setDaemon(true);
    daemonStopwatch.start();
}

My question then is, what is the correct approach to this problem, please?

4

1 回答 1

5

正如对我的问题的评论中所发布的那样,答案确实是属性绑定使用最终在更新属性本身的同一线程上运行的侦听器 - 因此,为避免出现问题,需要在 Java FX 上更新绑定属性应用程序线程(或应寻求其他解决方案,就像我的情况一样)。

对我有用的解决方案 - 正如我对问题的评论中提到的那样,我查看了AnimationTimer它是什么,它似乎正是我正在寻找的并且完美地工作。

如果它对某人有帮助,这是我的实现:

import ...

public class TimerTabController {
    public static final int TIMER_HBOX_TEXTFIELD_INDEX = 1;
    public static final int TIMER_HBOX_STARTSTOP_BUTTON_INDEX = 2;

    @FXML
    private Tab timerTab;
    @FXML
    private HBox defaultTimerHBox;
    @FXML
    private TextField defaultTimerTextField;

    private Map<HBox, MyTimer> timers = new HashMap<>();

    private AnimationTimer timerTabAnimationTimer = new AnimationTimer() {
        @Override
        public void handle(long l) {
        //the GUI updates go here
            for (HBox hBox : timers.keySet()) {
                if (hBox.getChildren().get(TIMER_HBOX_TEXTFIELD_INDEX) instanceof TextField) {
                    TextField currentField = (TextField) hBox.getChildren().get(TIMER_HBOX_TEXTFIELD_INDEX);
                    currentField.setText(MyFormatter.longMillisecondsTimeToTimeString(
                            timers.get(hBox).getRemainingTime())
                                        );
                }
            }
        }
    };

    @FXML
    protected void initialize() {
        timers.put(defaultTimerHBox, new MyTimer());
    }

    @FXML
    void handleSelectionChanged() { //triggered by changing tab selection
        if (timerTab.isSelected()) {
            timerTabAnimationTimer.start();
        } else {
            timerTabAnimationTimer.stop();
        }
    }

    //some more code
}
于 2019-12-29T00:11:13.967 回答