que - Actualización de la interfaz de usuario desde diferentes subprocesos en JavaFX
javafx tutorial (2)
La mejor manera de realizar esto es mediante el uso de Task
en JavaFx. Esta es, con mucho, la mejor técnica que he encontrado para actualizar los controles de interfaz de usuario en JavaFx.
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();
Estoy desarrollando una aplicación con varios objetos TextField
que deben actualizarse para reflejar los cambios en las propiedades de back-end asociadas. Los TextField
s no son editables, solo el back-end puede cambiar su contenido.
Según tengo entendido, la forma correcta de hacerlo es ejecutar el cálculo pesado en un hilo separado para no bloquear la IU. Hice esto usando javafx.concurrent.Task
y comuniqué un único valor al hilo de JavaFX usando updateMessage()
, que funcionó bien. Sin embargo, necesito que se actualice más de un valor ya que el back-end hace su crujido.
Dado que los valores de back-end se almacenan como propiedades JavaFX, intenté simplemente vincularlos a la textProperty
de cada elemento GUI y dejar que los enlaces hicieran el trabajo. Esto no funciona, sin embargo; después de ejecutarse por unos momentos, los TextField
dejan de actualizarse aunque la tarea de fondo aún se está ejecutando. No se plantean excepciones.
También intenté usar Platform.runLater()
para actualizar activamente los TextField
lugar de vinculantes. El problema aquí es que las tareas de runLater()
se programan más rápido de lo que la plataforma puede ejecutarlas, por lo que la GUI se vuelve lenta y necesita tiempo para "ponerse al día" incluso después de que la tarea de back-end haya finalizado.
Encontré algunas preguntas aquí:
Las entradas del registrador traducidas a la interfaz de usuario dejan de actualizarse con el tiempo
El subprocesamiento múltiple en JavaFX bloquea la interfaz de usuario
pero mi problema persiste.
En resumen: tengo un back-end realizando cambios en las propiedades, y quiero que esos cambios aparezcan en la GUI. El back-end es un algoritmo genético, por lo que su funcionamiento se divide en generaciones discretas. Lo que me gustaría es que TextField
s se actualice al menos una vez entre generaciones, incluso si esto retrasa la próxima generación. Es más importante que la GUI responda bien que que GA funcione rápido.
Puedo publicar algunos ejemplos de código si no he aclarado el problema.
ACTUALIZAR
Logré hacerlo siguiendo la sugerencia de James_D. Para resolver el problema de que el back-end tuviera que esperar a que la consola imprimiera, implementé una especie de consola de búfer. Almacena las cadenas para imprimir en un StringBuffer
y las agrega a TextArea
cuando se llama a un método flush()
. Utilicé AtomicBoolean para evitar que ocurra la próxima generación hasta que se complete el lavado, ya que lo hace un Platform.runLater()
ejecutable. También tenga en cuenta que esta solución es increíblemente lenta.
No estoy seguro si lo entiendo completamente, pero creo que esto puede ayudar.
Usar Platform.runLater (...) es un enfoque apropiado para esto.
El truco para evitar inundar el hilo de la aplicación FX es usar una variable atómica para almacenar el valor que le interesa. En el método Platform.runLater (...), recupérelo y configúrelo como un valor centinela. Desde su hilo de fondo, actualice la variable Atómica, pero solo emita una nueva Platform.runLater (...) si se ha restablecido a su valor centinela.
Me di cuenta de esto mirando el código fuente de la tarea . Eche un vistazo a cómo se implementa el método updateMessage (..) (línea 1131 en el momento de escribir).
Aquí hay un ejemplo que usa la misma técnica. Esto solo tiene un hilo de fondo (ocupado) que cuenta tan rápido como puede, actualizando un IntegerProperty. Un observador observa esa propiedad y actualiza un AtomicInteger con el nuevo valor. Si el valor actual de AtomicInteger es -1, programa una Platform.runLater ().
En Platform.runLater, recupero el valor de AtomicInteger y lo uso para actualizar una etiqueta, estableciendo el valor en -1 en el proceso. Esto indica que estoy listo para otra actualización de 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);
}
}
}
}
Si realmente desea "conducir" la parte de atrás de la interfaz de usuario: que es acelerar la velocidad de la implementación de back-end para que pueda ver todas las actualizaciones, considere el uso de un AnimationTimer
. Un AnimationTimer
tiene un handle(...)
que se llama una vez por procesamiento de cuadros. Por lo tanto, podría bloquear la implementación de servicios de fondo (por ejemplo, mediante el uso de una cola de bloqueo) y liberarla una vez por cada invocación del método de control. El método handle(...)
se invoca en el hilo de la aplicación FX.
El método handle(...)
toma un parámetro que es una marca de tiempo (en nanosegundos), por lo que puede usarlo para ralentizar aún más las actualizaciones, si una vez por fotograma es demasiado rápido.
Por ejemplo:
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);
}
}