Maneira mais eficiente de registrar mensagens no TextArea JavaFX por meio de encadeamentos com estruturas de registro customizadas simples

Eu tenho um quadro de registro personalizado simples 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; } } 

Agora, o que eu quero fazer é estendê-lo para que seja capaz de manipular corretamente o registro de mensagens por meio de threads. Eu tentei soluções como usar filas de mensagens com um AnimationTimer . Funciona, mas diminui a GUI.

Também tentei usar um serviço planejado que executa um thread que lê mensagens da fila de mensagens, concatena-as e as anexa a TextArea ( textArea.appendText(stringBuilder.toString()) ). O problema é que o controle TextArea fica instável, ou seja, você precisa realçar todos os textos com Ctrl-A e tentar resize a janela para que ela apareça bem. Há também alguns deles sendo exibidos em um fundo azul claro, não tendo certeza do que está causando isso. Meu primeiro palpite aqui é que a condição de corrida pode não permitir que o controle se atualize bem das novas strings. Também é importante notar que a textarea é envolvida em torno de um ScrollPane, de forma que acrescente a confusão se TextArea for realmente o problema ou o ScrollPane. Eu tenho que mencionar também que esta abordagem não faz o controle de TextArea atualizar-se com mensagens rapidamente.

Pensei em binding TextArea.TextProperty() a algo que faz a atualização, mas não sei como faria isso apropriadamente, sabendo que o coletor de mensagens (seja por um serviço ou um thread solitário) ainda estaria sendo executado de forma diferente o thread da GUI.

Eu tentei olhar para cima em outras soluções de estrutura de log conhecidos como log4j e alguns materiais referidos aqui, mas nenhum deles parece dar uma aparente abordagem para o log através de threads para TextArea. Eu também não gosto da ideia de construir meu sistema de logging sobre eles já que eles já têm seus mecanismos pré-definidos como nível de registro, etc.

Eu vi isso também. Isso implica o uso de SwingUtilities.invokeLater(Runnable) para atualizar o controle, mas eu já tentei uma abordagem semelhante usando javafx.application.platform.runLater() que é executado no thread de trabalho. Eu não tenho certeza se havia algo que eu estava fazendo errado, mas apenas trava. Pode produzir mensagens, mas não quando elas são agressivas o suficiente. Eu estimo que o encadeamento do trabalhador sendo executado de maneira puramente síncrona pode realmente produzir cerca de 20 ou mais linhas médias por segundo e mais quando está no modo de debugging. Uma solução possível seria adicionar enfileiramento de mensagens também, mas isso não faz mais 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< ? super LogRecord> 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); } } 

A seção abaixo no texto selecionável é suplementar à solução postada acima. Se você não precisa de um texto selecionável, ignore a seleção abaixo.

É possível tornar o texto selecionável?

Existem algumas opções diferentes:

  1. É um ListView, portanto, você poderia usar um modelo de seleção de multipeças , garantindo que o CSS seja configurado para apropriadamente estilizar as linhas selecionadas como desejar. Isso fará uma seleção de linha por linha, não uma seleção de texto direto. Você pode adicionar um ouvinte aos itens selecionados no modelo de seleção e fazer o processamento apropriado quando isso for alterado.
  2. Você poderia usar uma fábrica para o ListView que define cada célula para um campo de texto de somente leitura de estilos apropriados. Isso permitiria que alguém selecionasse apenas uma parte do texto dentro de uma linha em vez de uma linha inteira. Mas eles não conseguiriam selecionar texto em várias linhas de uma só vez.
    • Rótulo copiável / TextField / LabeledText no JavaFX
  3. Em vez de um ListView, você poderia basear a implementação em um controle RichTextFX de somente leitura de terceiros, o que permitiria a seleção de texto em várias linhas.

Tente implementar a abordagem de seleção de texto que é apropriada para você e, se você não conseguir fazê-lo funcionar, crie uma nova pergunta específica para logs de texto selecionáveis, com um comando .