La manera más eficiente de registrar mensajes en JavaFX TextArea a través de hilos con marcos de registro personalizados simples

Tengo un marco de registro personalizado simple como este:

package something; import javafx.scene.control.TextArea; public class MyLogger { public final TextArea textArea; private boolean verboseMode = false; private boolean debugMode = false; public MyLogger(final TextArea textArea) { this.textArea = textArea; } public MyLogger setVerboseMode(boolean value) { verboseMode = value; return this; } public MyLogger setDebugMode(boolean value) { debugMode = value; return this; } public boolean writeMessage(String msg) { textArea.appendText(msg); return true; } public boolean logMessage(String msg) { return writeMessage(msg + "\n"); } public boolean logWarning(String msg) { return writeMessage("Warning: " + msg + "\n"); } public boolean logError(String msg) { return writeMessage("Error: " + msg + "\n"); } public boolean logVerbose(String msg) { return verboseMode ? writeMessage(msg + "\n") : true; } public boolean logDebug(String msg) { return debugMode ? writeMessage("[DEBUG] " + msg + "\n") : true; } } 

Ahora lo que quiero hacer es extenderlo para que pueda manejar adecuadamente el registro de mensajes a través de hilos. He intentado soluciones como usar colas de mensajes con un AnimationTimer . Funciona pero ralentiza la GUI.

También intenté usar un servicio progtwigdo que ejecuta un hilo que lee los mensajes de la cola de mensajes, los concatena y los agrega a TextArea ( textArea.appendText(stringBuilder.toString()) ). El problema es que el control TextArea se vuelve inestable, es decir, que debe resaltar todos los textos con Ctrl-A y tratar de cambiar el tamaño de la ventana para que se vean bien. También hay algunos de ellos que se muestran en un fondo azul claro, no estoy seguro de lo que está causando. Mi primera suposición aquí es que la condición de carrera no permite que el control se actualice bien a partir de las nuevas cadenas. También vale la pena señalar que el área de texto está envuelto alrededor de un ScrollPane por lo que agrega la confusión si TextArea es realmente el que tiene el problema o ScrollPane. Debo mencionar también que este enfoque no hace que el control TextArea se actualice solo con mensajes rápidamente.

Pensé en binding TextArea.TextProperty() a algo que hace la actualización pero no estoy seguro de cómo lo haría correctamente sabiendo que el recostackdor de mensajes (ya sea por un servicio o un único hilo) todavía se estaría ejecutando diferente de el hilo de la GUI.

He intentado buscar otras soluciones conocidas de frameworks de registro como log4j y algunos productos mencionados aquí, pero ninguno de ellos parece dar un enfoque aparente al registro a través de hilos en TextArea. Tampoco me gusta la idea de construir mi sistema de registro sobre ellos ya que ya tienen sus mecanismos predefinidos, como el nivel de registro, etc.

Yo también he visto esto . Implica usar SwingUtilities.invokeLater(Runnable) para actualizar el control, pero ya probé un enfoque similar usando javafx.application.platform.runLater() que se ejecuta en el hilo de trabajo. No estoy seguro de si había algo que estaba haciendo mal, pero simplemente se bloquea. Puede producir mensajes, pero no cuando son lo suficientemente agresivos. Estimo que el subproceso de trabajo que se ejecuta de forma puramente sincrónica en realidad puede producir aproximadamente 20 o más líneas promedio por segundo y más cuando está en modo de depuración. Una posible solución alternativa sería agregarle la cola de mensajes, pero eso ya no tiene sentido.

log-view.css

 .root { -fx-padding: 10px; } .log-view .list-cell { -fx-background-color: null; // removes alternating list gray cells. } .log-view .list-cell:debug { -fx-text-fill: gray; } .log-view .list-cell:info { -fx-text-fill: green; } .log-view .list-cell:warn { -fx-text-fill: purple; } .log-view .list-cell:error { -fx-text-fill: red; } 

LogViewer.java

 import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Application; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.css.PseudoClass; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.util.Duration; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.Random; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; class Log { private static final int MAX_LOG_ENTRIES = 1_000_000; private final BlockingDeque log = new LinkedBlockingDeque<>(MAX_LOG_ENTRIES); public void drainTo(Collection collection) { log.drainTo(collection); } public void offer(LogRecord record) { log.offer(record); } } class Logger { private final Log log; private final String context; public Logger(Log log, String context) { this.log = log; this.context = context; } public void log(LogRecord record) { log.offer(record); } public void debug(String msg) { log(new LogRecord(Level.DEBUG, context, msg)); } public void info(String msg) { log(new LogRecord(Level.INFO, context, msg)); } public void warn(String msg) { log(new LogRecord(Level.WARN, context, msg)); } public void error(String msg) { log(new LogRecord(Level.ERROR, context, msg)); } public Log getLog() { return log; } } enum Level { DEBUG, INFO, WARN, ERROR } class LogRecord { private Date timestamp; private Level level; private String context; private String message; public LogRecord(Level level, String context, String message) { this.timestamp = new Date(); this.level = level; this.context = context; this.message = message; } public Date getTimestamp() { return timestamp; } public Level getLevel() { return level; } public String getContext() { return context; } public String getMessage() { return message; } } class LogView extends ListView { private static final int MAX_ENTRIES = 10_000; private final static PseudoClass debug = PseudoClass.getPseudoClass("debug"); private final static PseudoClass info = PseudoClass.getPseudoClass("info"); private final static PseudoClass warn = PseudoClass.getPseudoClass("warn"); private final static PseudoClass error = PseudoClass.getPseudoClass("error"); private final static SimpleDateFormat timestampFormatter = new SimpleDateFormat("HH:mm:ss.SSS"); private final BooleanProperty showTimestamp = new SimpleBooleanProperty(false); private final ObjectProperty filterLevel = new SimpleObjectProperty<>(null); private final BooleanProperty tail = new SimpleBooleanProperty(false); private final BooleanProperty paused = new SimpleBooleanProperty(false); private final DoubleProperty refreshRate = new SimpleDoubleProperty(60); private final ObservableList logItems = FXCollections.observableArrayList(); public BooleanProperty showTimeStampProperty() { return showTimestamp; } public ObjectProperty filterLevelProperty() { return filterLevel; } public BooleanProperty tailProperty() { return tail; } public BooleanProperty pausedProperty() { return paused; } public DoubleProperty refreshRateProperty() { return refreshRate; } public LogView(Logger logger) { getStyleClass().add("log-view"); Timeline logTransfer = new Timeline( new KeyFrame( Duration.seconds(1), event -> { logger.getLog().drainTo(logItems); if (logItems.size() > MAX_ENTRIES) { logItems.remove(0, logItems.size() - MAX_ENTRIES); } if (tail.get()) { scrollTo(logItems.size()); } } ) ); logTransfer.setCycleCount(Timeline.INDEFINITE); logTransfer.rateProperty().bind(refreshRateProperty()); this.pausedProperty().addListener((observable, oldValue, newValue) -> { if (newValue && logTransfer.getStatus() == Animation.Status.RUNNING) { logTransfer.pause(); } if (!newValue && logTransfer.getStatus() == Animation.Status.PAUSED && getParent() != null) { logTransfer.play(); } }); this.parentProperty().addListener((observable, oldValue, newValue) -> { if (newValue == null) { logTransfer.pause(); } else { if (!paused.get()) { logTransfer.play(); } } }); filterLevel.addListener((observable, oldValue, newValue) -> { setItems( new FilteredList( logItems, logRecord -> logRecord.getLevel().ordinal() >= filterLevel.get().ordinal() ) ); }); filterLevel.set(Level.DEBUG); setCellFactory(param -> new ListCell() { { showTimestamp.addListener(observable -> updateItem(this.getItem(), this.isEmpty())); } @Override protected void updateItem(LogRecord item, boolean empty) { super.updateItem(item, empty); pseudoClassStateChanged(debug, false); pseudoClassStateChanged(info, false); pseudoClassStateChanged(warn, false); pseudoClassStateChanged(error, false); if (item == null || empty) { setText(null); return; } String context = (item.getContext() == null) ? "" : item.getContext() + " "; if (showTimestamp.get()) { String timestamp = (item.getTimestamp() == null) ? "" : timestampFormatter.format(item.getTimestamp()) + " "; setText(timestamp + context + item.getMessage()); } else { setText(context + item.getMessage()); } switch (item.getLevel()) { case DEBUG: pseudoClassStateChanged(debug, true); break; case INFO: pseudoClassStateChanged(info, true); break; case WARN: pseudoClassStateChanged(warn, true); break; case ERROR: pseudoClassStateChanged(error, true); break; } } }); } } class Lorem { private static final String[] IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque hendrerit imperdiet mi quis convallis. Pellentesque fringilla imperdiet libero, quis hendrerit lacus mollis et. Maecenas porttitor id urna id mollis. Suspendisse potenti. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras lacus tellus, semper hendrerit arcu quis, auctor suscipit ipsum. Vestibulum venenatis ante et nulla commodo, ac ultricies purus fringilla. Aliquam lectus urna, commodo eu quam a, dapibus bibendum nisl. Aliquam blandit a nibh tincidunt aliquam. In tellus lorem, rhoncus eu magna id, ullamcorper dictum tellus. Curabitur luctus, justo a sodales gravida, purus sem iaculis est, eu ornare turpis urna vitae dolor. Nulla facilisi. Proin mattis dignissim diam, id pellentesque sem bibendum sed. Donec venenatis dolor neque, ut luctus odio elementum eget. Nunc sed orci ligula. Aliquam erat volutpat.".split(" "); private static final int MSG_WORDS = 8; private int idx = 0; private Random random = new Random(42); synchronized public String nextString() { int end = Math.min(idx + MSG_WORDS, IPSUM.length); StringBuilder result = new StringBuilder(); for (int i = idx; i < end; i++) { result.append(IPSUM[i]).append(" "); } idx += MSG_WORDS; idx = idx % IPSUM.length; return result.toString(); } synchronized public Level nextLevel() { double v = random.nextDouble(); if (v < 0.8) { return Level.DEBUG; } if (v < 0.95) { return Level.INFO; } if (v < 0.985) { return Level.WARN; } return Level.ERROR; } } public class LogViewer extends Application { private final Random random = new Random(42); @Override public void start(Stage stage) throws Exception { Lorem lorem = new Lorem(); Log log = new Log(); Logger logger = new Logger(log, "main"); logger.info("Hello"); logger.warn("Don't pick up alien hitchhickers"); for (int x = 0; x < 20; x++) { Thread generatorThread = new Thread( () -> { for (;;) { logger.log( new LogRecord( lorem.nextLevel(), Thread.currentThread().getName(), lorem.nextString() ) ); try { Thread.sleep(random.nextInt(1_000)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, "log-gen-" + x ); generatorThread.setDaemon(true); generatorThread.start(); } LogView logView = new LogView(logger); logView.setPrefWidth(400); ChoiceBox filterLevel = new ChoiceBox<>( FXCollections.observableArrayList( Level.values() ) ); filterLevel.getSelectionModel().select(Level.DEBUG); logView.filterLevelProperty().bind( filterLevel.getSelectionModel().selectedItemProperty() ); ToggleButton showTimestamp = new ToggleButton("Show Timestamp"); logView.showTimeStampProperty().bind(showTimestamp.selectedProperty()); ToggleButton tail = new ToggleButton("Tail"); logView.tailProperty().bind(tail.selectedProperty()); ToggleButton pause = new ToggleButton("Pause"); logView.pausedProperty().bind(pause.selectedProperty()); Slider rate = new Slider(0.1, 60, 60); logView.refreshRateProperty().bind(rate.valueProperty()); Label rateLabel = new Label(); rateLabel.textProperty().bind(Bindings.format("Update: %.2f fps", rate.valueProperty())); rateLabel.setStyle("-fx-font-family: monospace;"); VBox rateLayout = new VBox(rate, rateLabel); rateLayout.setAlignment(Pos.CENTER); HBox controls = new HBox( 10, filterLevel, showTimestamp, tail, pause, rateLayout ); controls.setMinHeight(HBox.USE_PREF_SIZE); VBox layout = new VBox( 10, controls, logView ); VBox.setVgrow(logView, Priority.ALWAYS); Scene scene = new Scene(layout); scene.getStylesheets().add( this.getClass().getResource("log-view.css").toExternalForm() ); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } } 

La siguiente sección sobre texto seleccionable es complementaria a la solución publicada anteriormente. Si no necesita texto seleccionable, puede ignorar la selección a continuación.

¿Es posible hacer que el texto sea seleccionable?

Hay algunas opciones diferentes:

  1. Es un ListView, por lo que podría usar un modelo de selección de múltiples , asegurando que el CSS esté configurado para darle un estilo apropiado a las filas seleccionadas como lo desee. Eso hará una selección fila por fila, no una selección de texto recto. Puede agregar un oyente a los elementos seleccionados en el modelo de selección y hacer el procesamiento apropiado cuando eso cambie.
  2. Puede usar una fábrica para ListView que establece cada celda en un campo de texto de solo lectura de estilos apropiados. Eso le permitiría a alguien seleccionar solo una parte del texto dentro de una fila en lugar de una fila completa. Pero no podrían seleccionar texto en múltiples filas de una vez.
    • Etiqueta / TextField / LabeledText copiables en JavaFX
  3. En lugar de un ListView, podría basar la implementación en un control de RichTextFX de solo lectura de terceros, lo que permitiría la selección de texto en varias filas.

Intente implementar el enfoque de selección de texto que sea apropiado para usted y, si no puede hacerlo funcionar, cree una nueva pregunta específica para los registros de texto seleccionables, con un mcve .