通过具有简单自定义日志记录框架的线程将消息记录到JavaFX TextArea的最有效方法

我有一个简单的自定义日志框架,如下所示:

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;
    }
}

现在我想要做的是扩展它,以便能够通过线程正确处理消息的记录.我尝试过像using message queues with an AnimationTimer这样的解决方案.它可以工作,但它会降低GUI的速度.

我还尝试使用scheduled service运行一个线程,该线程从消息队列中读取消息,连接它们,并将它们附加到TextArea(textArea.appendText(stringBuilder.toString())).问题是TextArea控件变得不稳定,即您必须使用Ctrl-A突出显示所有文本,并尝试调整窗口大小以使其显示良好.还有一些显示在浅蓝色背景中,不知道是什么原因造成的.我在这里的第一个猜测是竞争条件可能不允许控件从新字符串中很好地更新.值得注意的是,textarea包裹在ScrollPane中,因此如果TextArea实际上是有问题或ScrollPane的话,它会增加混乱.我还要提一下,这种方法不会使TextArea控件快速更新消息.

我想将TextArea.TextProperty()绑定到执行更新的内容但是我不确定如何正确地知道消息的收集器(无论是服务还是单独的线程)仍将运行不同于GUI线程.

我试图查看其他已知的日志框架解决方案,如log4j和here中引用的一些内容,但它们似乎都没有给出通过线程记录到TextArea的明显方法.我也不喜欢在它们之上构建我的日志记录系统的想法,因为它们已经有了预定义的机制,如日志记录级别等.

我也见过this.它意味着使用SwingUtilities.invokeLater(Runnable)更新控件,但我已经尝试使用javafx.application.platform.runLater()在工作线程上执行类似的方法.我不确定是否有什么东西我做错了但它只是挂了.它可以产生消息,但不会产生足够的攻击性.我估计以纯粹同步的方式运行的工作线程实际上可以产生大约20或更多的平均每秒线数,并且当它处于调试模式时更多.一个可能的解决方法是添加消息队列,但这不再有意义.

最佳答案
登录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<LogRecord> 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<LogRecord> {
    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<Level> 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<LogRecord> logItems = FXCollections.observableArrayList();

    public BooleanProperty showTimeStampProperty() {
        return showTimestamp;
    }

    public ObjectProperty<Level> 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<LogRecord>(
                            logItems,
                            logRecord ->
                                logRecord.getLevel().ordinal() >=
                                filterLevel.get().ordinal()
                    )
            );
        });
        filterLevel.set(Level.DEBUG);

        setCellFactory(param -> new ListCell<LogRecord>() {
            {
                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<Level> 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);
    }
}

以下关于可选文本的部分是对上面发布的解决方案的补充.如果您不需要可选文本,则可以忽略下面的选择.

Is it possible to make the text selectable?

有几个不同的选择:

>它是一个ListView,因此您可以使用multipe selection model,确保CSS配置为根据需要适当地设置所选行的样式.这将逐行选择,而不是直接文本选择.您可以向选择模型中的选定项添加侦听器,并在更改时执行适当的处​​理.
>您可以使用ListView工厂将每个单元格设置为适当的样式只读文本字段.这将允许某人只选择行中的一部分文本而不是整行.但他们无法一次性跨多行选择文本.

> Copiable Label/TextField/LabeledText in JavaFX

>而不是ListView,您可以将实现基于第三方只读RichTextFX控件,这将允许跨多行选择文本.

尝试实施适合您的文本选择方法,如果无法使其工作,请使用mcve创建特定于可选文本日志的新问题.

转载注明原文:通过具有简单自定义日志记录框架的线程将消息记录到JavaFX TextArea的最有效方法 - 代码日志