2014-03-31 37 views
26

我正在開發一個應用程序,其中有幾個TextField對象需要更新以反映關聯的後端屬性中的更改。 TextField不可編輯,只有後端可能會更改其內容。從JavaFX中的不同線程更新UI

據我所知,正確的方法是在單獨的線程上運行繁重的計算,以免阻塞UI。我使用javafx.concurrent.Task做了這個,並使用updateMessage()將單個值傳回到JavaFX線程,這很有效。但是,隨着後端的搗鼓,我需要更新一個以上的值。

由於後端值存儲爲JavaFX屬性,因此我嘗試將它們綁定到每個GUI元素的textProperty,並讓綁定完成工作。然而,這不起作用;在運行一會兒之後,即使後端任務仍在運行,TextField停止更新。沒有例外。

我也嘗試使用Platform.runLater()積極更新TextField s而不是綁定。這裏的問題是,runLater()任務的計劃速度比平臺能夠運行得更快,因此即使後端任務完成後,GUI也變得呆滯,需要時間來「趕上」。

我發現這裏有幾個問題:

Logger entries translated to the UI stops being updated with time

Multithreading in JavaFX hangs the UI

,但我的問題仍然存在。

總結:我有一個後端更改屬性,我希望這些更改顯示在GUI上。後端是一種遺傳算法,因此它的運行被分解成不連續的世代。我希望TextField在兩代之間至少刷新一次,即使這會延誤下一代。更重要的是,GUI的響應比GA運行速度快。

如果我沒有清楚問題,我可以發表一些代碼示例。

UPDATE

我設法做以下James_D的建議。爲了解決後臺不得不等待控制檯打印的問題,我實現了一個緩衝控制檯。它存儲要打印的字符串StringBuffer,並在調用flush()方法時將它們附加到TextArea。我使用AtomicBoolean來防止下一代發生,直到刷新完成,因爲它是由Platform.runLater()可運行的。另外請注意,這個解決方案是難以置信的慢。

+0

相關問題:([登錄消息經由螺紋的JavaFX的TextArea最有效的方法] http://stackoverflow.com/questions/24116858/most-efficient-way-to-log-messages-to-javafx -textarea-via-threads-with-simple-cu) – jewelsea

回答

27

不知道我是否完全理解,但我認爲這可能有幫助。

使用Platform.runLater(...)是一個適當的方法。

避免淹沒FX應用程序線程的技巧是使用Atomic變量來存儲您感興趣的值。在Platform.runLater(...)方法中,檢索它並將其設置爲sentinel值。在你的後臺線程中,更新Atomic變量,但是如果它已經被設置回它的sentinel值,則只發佈一個新的Platform.runLater(...)。

我通過查看source code for Task來解決這個問題。看看updateMessage(..)方法(在編寫本文時爲第1131行)是如何實現的。

下面是使用相同技術的示例。這只是一個(繁忙)後臺線程,它可以計算儘可能快的速度,更新一個IntegerProperty。觀察者觀察該屬性並用新值更新AtomicInteger。如果AtomicInteger的當前值是-1,它將調度一個Platform.runLater()。

在Platform.runLater中,我檢索AtomicInteger的值並使用它來更新Label,並在該過程中將該值設置回-1。這表示我已準備好進行其他UI更新。

import java.text.NumberFormat; 
import java.util.concurrent.atomic.AtomicInteger; 
import javafx.application.Application; 
import javafx.application.Platform; 
import javafx.beans.property.IntegerProperty; 
import javafx.beans.property.SimpleIntegerProperty; 
import javafx.beans.value.ChangeListener; 
import javafx.beans.value.ObservableValue; 
import javafx.event.ActionEvent; 
import javafx.event.EventHandler; 
import javafx.scene.Scene; 
import javafx.scene.control.Button; 
import javafx.scene.control.Label; 
import javafx.scene.layout.AnchorPane; 
import javafx.stage.Stage; 

public class ConcurrentModel extends Application { 

    @Override 
    public void start(Stage primaryStage) { 

    final AtomicInteger count = new AtomicInteger(-1); 

    final AnchorPane root = new AnchorPane(); 
    final Label label = new Label(); 
    final Model model = new Model(); 
    final NumberFormat formatter = NumberFormat.getIntegerInstance(); 
    formatter.setGroupingUsed(true); 
    model.intProperty().addListener(new ChangeListener<Number>() { 
     @Override 
     public void changed(final ObservableValue<? extends Number> observable, 
      final Number oldValue, final Number newValue) { 
     if (count.getAndSet(newValue.intValue()) == -1) { 
      Platform.runLater(new Runnable() { 
      @Override 
      public void run() { 
       long value = count.getAndSet(-1); 
       label.setText(formatter.format(value)); 
      } 
      });   
     } 

     } 
    }); 
    final Button startButton = new Button("Start"); 
    startButton.setOnAction(new EventHandler<ActionEvent>() { 
     @Override 
     public void handle(ActionEvent event) { 
     model.start(); 
     } 
    }); 

    AnchorPane.setTopAnchor(label, 10.0); 
    AnchorPane.setLeftAnchor(label, 10.0); 
    AnchorPane.setBottomAnchor(startButton, 10.0); 
    AnchorPane.setLeftAnchor(startButton, 10.0); 
    root.getChildren().addAll(label, startButton); 

    Scene scene = new Scene(root, 100, 100); 
    primaryStage.setScene(scene); 
    primaryStage.show(); 
    } 

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

    public class Model extends Thread { 
    private IntegerProperty intProperty; 

    public Model() { 
     intProperty = new SimpleIntegerProperty(this, "int", 0); 
     setDaemon(true); 
    } 

    public int getInt() { 
     return intProperty.get(); 
    } 

    public IntegerProperty intProperty() { 
     return intProperty; 
    } 

    @Override 
    public void run() { 
     while (true) { 
     intProperty.set(intProperty.get() + 1); 
     } 
    } 
    } 
} 

如果你真的想「驅動器」,從UI後端:即油門後端實現,所以你看到的所有更新的速度,考慮使用AnimationTimerAnimationTimer有一個handle(...),每幀渲染一次。因此,您可以阻止後端實現(例如,通過使用阻塞隊列),並在每次調用句柄方法時釋放一次。在FX應用程序線程上調用handle(...)方法。

handle(...)方法需要一個參數,它是一個時間戳(以納秒爲單位),因此您可以使用它來進一步減慢更新速度,如果每幀一次太快。

例如:

import java.util.concurrent.ArrayBlockingQueue; 
import java.util.concurrent.BlockingQueue; 

import javafx.animation.AnimationTimer; 
import javafx.application.Application; 
import javafx.beans.property.LongProperty; 
import javafx.beans.property.SimpleLongProperty; 
import javafx.geometry.Insets; 
import javafx.geometry.Pos; 
import javafx.stage.Stage; 
import javafx.scene.Scene; 
import javafx.scene.control.Button; 
import javafx.scene.control.TextArea; 
import javafx.scene.layout.BorderPane; 
import javafx.scene.layout.HBox; 


public class Main extends Application { 
    @Override 
    public void start(Stage primaryStage) { 

     final BlockingQueue<String> messageQueue = new ArrayBlockingQueue<>(1); 

     TextArea console = new TextArea(); 

     Button startButton = new Button("Start"); 
     startButton.setOnAction(event -> { 
      MessageProducer producer = new MessageProducer(messageQueue); 
      Thread t = new Thread(producer); 
      t.setDaemon(true); 
      t.start(); 
     }); 

     final LongProperty lastUpdate = new SimpleLongProperty(); 

     final long minUpdateInterval = 0 ; // nanoseconds. Set to higher number to slow output. 

     AnimationTimer timer = new AnimationTimer() { 

      @Override 
      public void handle(long now) { 
       if (now - lastUpdate.get() > minUpdateInterval) { 
        final String message = messageQueue.poll(); 
        if (message != null) { 
         console.appendText("\n" + message); 
        } 
        lastUpdate.set(now); 
       } 
      } 

     }; 

     timer.start(); 

     HBox controls = new HBox(5, startButton); 
     controls.setPadding(new Insets(10)); 
     controls.setAlignment(Pos.CENTER); 

     BorderPane root = new BorderPane(console, null, null, controls, null); 
     Scene scene = new Scene(root,600,400); 
     primaryStage.setScene(scene); 
     primaryStage.show(); 
    } 

    private static class MessageProducer implements Runnable { 
     private final BlockingQueue<String> messageQueue ; 

     public MessageProducer(BlockingQueue<String> messageQueue) { 
      this.messageQueue = messageQueue ; 
     } 

     @Override 
     public void run() { 
      long messageCount = 0 ; 
      try { 
       while (true) { 
        final String message = "Message " + (++messageCount); 
        messageQueue.put(message); 
       } 
      } catch (InterruptedException exc) { 
       System.out.println("Message producer interrupted: exiting."); 
      } 
     } 
    } 

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

我認爲API文檔中提到的合併更加複雜,我想我應該先看看哈哈。無論如何,這種方法可以防止GUI凍結,這很好,但正如我在最後一段中所說的那樣,我還想要一種強制後端等待GUI更新的方式,任何想法?我之所以要這樣做,是因爲我有一個'TextArea'作爲控制檯,但打印它非常耗時,並且任意跳過打印是不好的。就像我所說的GA性能是次要的,如果系統受到'TextArea'控制檯瓶頸的影響就沒有問題。 –

+0

更新了用戶界面限制後端的示例。有可能有其他方法來做到這一點。 –

+0

感謝節流示例,我沒有想到使用這種JFX計時器。 'put()'過程中可能發生的異常使得與GA一起使用有點棘手,所以我決定現在堅持使用布爾變量。謝謝您的幫助! –

5

在執行這一點的最佳方式是通過在JavaFX的Task用法。這是迄今爲止我所遇到的用於更新JavaFx中的UI控件的最佳技術。

Task task = new Task<Void>() { 
    @Override public Void run() { 
     static final int max = 1000000; 
     for (int i=1; i<=max; i++) { 
      updateProgress(i, max); 
     } 
     return null; 
    } 
}; 
ProgressBar bar = new ProgressBar(); 
bar.progressProperty().bind(task.progressProperty()); 
new Thread(task).start(); 
+4

正如我在問題中所述,我正在使用'Task'。一年前解決的問題是,我有多個值可以與主線交流。'Task'提供了一些線程安全的更新方法,比如'updateProgress',但是我是在一個可擴展的解決方案之後不受限於進度,消息,價值和標題更新。 –