javaFX通用富文本日志界面组件 LogView

有两个View 一个核心的显示组件,一个带类型切换的组合组件
2024.1.13更新:添加类idea控制台的自动滚动功能

首先贴出实现效果:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
核心显示组件:

import cn.hutool.http.HtmlUtil;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.concurrent.Worker;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LogArea extends StackPane {
    private final WebView webView;
    private final int max_line; // 指定最大行数
    private boolean autoScrollTo = true;
    private final LogArea logArea;

    public void setAutoScrollTo(boolean autoScrollTo) {
        this.autoScrollTo = autoScrollTo;
    }

    public boolean isAutoScrollTo() {
        return autoScrollTo;
    }

    /**
     * @param max_line 指定最大显示行数
     */
    public LogArea(@NamedArg("max_line") int max_line) {
        logArea = this;
        this.max_line = max_line;
        webView = new WebView();
        init();
        ScrollPane scrollPane = new ScrollPane(webView);
        scrollPane.setFitToWidth(true); // 自适应宽度
        scrollPane.setFitToHeight(true); // 自适应高度
        this.getChildren().add(scrollPane);
        webView.setOnScroll(event -> {
            autoScrollTo = false;
            Integer scrollHeight = (Integer) webView.getEngine().executeScript("document.body.scrollHeight");
            double webViewHeight = webView.getHeight();
            Integer currentScrollY = (Integer) webView.getEngine().executeScript("window.scrollY");
            if (scrollHeight - (currentScrollY + webViewHeight) <= 20) {
                // 已经到达最底部
                autoScrollTo = true;
            }
            logArea.fireEvent(new ScrollEvent(autoScrollTo));
        });
    }
    private void init() {
        WebEngine webEngine = webView.getEngine();
        webEngine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue == Worker.State.SUCCEEDED) {
                webEngine.executeScript("window.scrollTo(0, document.body.scrollHeight)");
            }
        });
        // 隐藏默认的 WebView 上下文菜单
        webView.setContextMenuEnabled(false);
        // 设置 CSS 样式,使 WebView 充满整个区域,并设置文字大小
        webView.setStyle("-fx-background-color: transparent; -fx-border-color: transparent; -fx-font-size: 9px;");
    }
    public void logInfo(String message) {
        message = HtmlUtil.escape(message);
        String finalMessage = message;
        Platform.runLater(() -> {
            try {
                String jsCode = String.format("document.body.innerHTML += \"<p style='font-size: 12px;'>%s</p>\";", finalMessage);
                showHtml(jsCode);
            } catch (Exception e) {
                log.error("LogArea.logInfo {}", e.getMessage());
            }
        });
    }

    public void logError(String message) {
        message = HtmlUtil.escape(message);
        String finalMessage = message;
        Platform.runLater(() -> {
            try {
                String jsCode = String.format("document.body.innerHTML += \"<p style='font-size: 12px;color: red;'>%s</p>\";", finalMessage);
                showHtml(jsCode);
            } catch (Exception e) {
                log.error("LogArea.logError {}", e.getMessage());
            }
        });
    }

    public void logSuccess(String message) {
        message = HtmlUtil.escape(message);
        String finalMessage = message;
        Platform.runLater(() -> {
            try {
                String jsCode = String.format("document.body.innerHTML += \"<p style='font-size: 12px;color: green;'>%s</p>\";", finalMessage);
                showHtml(jsCode);
            } catch (Exception e) {
                log.error("LogArea.logError {}", e.getMessage());
            }
        });
    }

    /**
     * 限制日志行数
     */
    private void truncateLog() {
        String jsCode = "var paragraphs = document.getElementsByTagName('p');"
                + "if (paragraphs.length > " + max_line + ") {"
                + "  var diff = paragraphs.length - " + max_line + ";"
                + "  for (var i = 0; i < diff; i++) {"
                + "    paragraphs[i].remove();"
                + "  }"
                + "}";
        webView.getEngine().executeScript(jsCode);
    }

    public void clear() {
        webView.getEngine().loadContent("<!DOCTYPE html>\n" +
                "<html lang=\"zn\" xmlns:th=\"http://www.thymeleaf.org\">\n" +
                "<head>\n" +
                "    <meta charset=\"UTF-8\">\n" +
                "    <title>Title</title>\n" +
                "</head>\n" +
                "<body>\n" +
                "\n" +
                "</body>\n" +
                "</html>"); // 加载空的HTML页面
    }

    private void showHtml(String jsCode) {
        Platform.runLater(() -> {
            truncateLog();
            webView.getEngine().executeScript(jsCode);
            if (autoScrollTo) {
                webView.getEngine().executeScript("window.scrollTo(0, document.body.scrollHeight)");
            }
        });
    }

    /**
     * 自定义滚动触发事件
     */
    public static class  ScrollEvent extends Event {
        public static final EventType<ScrollEvent> SCROLL = new EventType<>("SCROLL");
        private final boolean autoScroll;
        public ScrollEvent(boolean autoScroll) {
            super(SCROLL);
            this.autoScroll = autoScroll;
        }
        public boolean isAutoScroll() {
            return autoScroll;
        }
    }
}

带有日志类型切换及类idea自动滚动控制的加强组件:


import awb.game.gateway.view.bean.FixedLengthList;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ReUtil;
import javafx.beans.NamedArg;
import javafx.geometry.Pos;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import lombok.extern.slf4j.Slf4j;

import javax.annotation.PreDestroy;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;

@Slf4j
/**
 * 带有类型切换及自动滚动的日志显示控件
 * @param <T> 类型
 */
public class LogAreaType<T> extends VBox {
    /**
     * 日志类型选择框
     */
    private final ComboBox<T> logTypeComboBox;
    // 日志显示的框架
    private final LogArea logArea;
    // 日志数据
    private final Map<T, FixedLengthList<String>> logdataMap = new ConcurrentHashMap<>();
    private final int max_line;
    private final ExecutorService executor = ThreadUtil.newFixedExecutor(1, 1, "日志显示", true); // 专门来切换显示内容
    /**
     * 当前显示的日志类型
     */
    private T thisType;
    private T allType;
    /**
     * 所有的日志类型及其级别
     */
    private final Map<String, Level> levelMap = new ConcurrentHashMap<>();

    /**
     * 显示的最大行数
     *
     * @param max_line
     */
    public LogAreaType(@NamedArg("max_line") int max_line) {
        this.max_line = max_line;
        HBox hbox = new HBox();
        //alignment="CENTER_RIGHT" spacing="5"
        hbox.setSpacing(5);
        hbox.setAlignment(Pos.CENTER_RIGHT); // 设置下拉框居中靠右
        Label label = new Label("日志类型:");
        logTypeComboBox = new ComboBox<>();
        //添加一个手动的跟随轮动的开关
        ToggleButton antoScrollButton = new ToggleButton("跟随滚动");
        hbox.getChildren().addAll(antoScrollButton, label, logTypeComboBox);
        logArea = new LogArea(max_line);
        //注册滚动事件,当自动滚动时,切换开关的显示状态
        logArea.addEventHandler(LogArea.ScrollEvent.SCROLL, event -> antoScrollButton.setSelected(event.isAutoScroll()));
        // 设置logArea的高度跟随父容器
        VBox.setVgrow(logArea, Priority.ALWAYS);
        // 设置logArea的宽度跟随父容器
        HBox.setHgrow(logArea, Priority.ALWAYS);
        getChildren().addAll(hbox, logArea);
        logTypeComboBox.setOnAction(event -> showLog(logTypeComboBox.getValue()));
        // 设置初始状态
        antoScrollButton.setSelected(logArea.isAutoScrollTo());
        // 主动根据按钮设置自动滚动
        antoScrollButton.setOnAction(event -> logArea.setAutoScrollTo(antoScrollButton.isSelected()));
    }

    /**
     * 添加日志
     *
     * @param logStr 日志
     * @param save   是否保存到日志文件中
     */
    private void addLog(String logStr, T type, boolean save) {
        try {
            if (!levelMap.containsKey(type.toString()))
                throw new RuntimeException("未知日志类型,请先对类型进行注册! addLogType");
            if (save) {
                logStr = "[" + DateUtil.formatTime(new Date()) + "][" + type + "] " + logStr;
                logdataMap.get(allType).add(logStr);
                if (type != allType)
                    logdataMap.get(type).add(logStr);
            }
            //如果当前显示的是这个日志类型,则显示
            if (type == thisType || thisType == allType) {
                //获取该条日志的级别
                String typeStr = ReUtil.get("\\[.*?\\]\\[(.*?)\\]", logStr, 1);
                Level level;
                try {
                    level = levelMap.get(typeStr);
                } catch (Exception e) {
                    throw new RuntimeException("未知日志类型:" + typeStr);
                }
                switch (level) {
                    case info:
                        logArea.logInfo(logStr);
                        break;
                    case success:
                        logArea.logSuccess(logStr);
                        break;
                    case error:
                        logArea.logError(logStr);
                        break;
                    default:
                        throw new RuntimeException("未知日志级别");
                }
            }
        } catch (Exception e) {
            log.error("日志添加失败! {}", e.getLocalizedMessage());
        }
    }

    public void addLog(String log, T type) {
        addLog(log, type, true);
    }

    private void showLog(T type) {
        logArea.clear();
        thisType = type;
        executor.execute(() -> {
            ThreadUtil.sleep(100);
            for (String log : logdataMap.get(type).getSafeList()) {
                addLog(log, type, false);
            }
        });
    }

    /**
     * 添加一个日志类型(显示)
     *
     * @param type  日志类型
     * @param level 类型的显示级别
     */
    public void addLogType(T type, Level level) {
        if (levelMap.containsKey(type.toString())) throw new RuntimeException("重复添加日志类型! addLogType");
        logTypeComboBox.getItems().add(type);
        if (thisType == null) {//初始化日志类型{
            thisType = type;
            allType = type;
            logTypeComboBox.getSelectionModel().select(type); //默认选中第一个类型
        }
        logdataMap.put(type, new FixedLengthList<>(max_line));
        levelMap.put(type.toString(), level);
    }

    /**
     * 添加多个日志类型(显示)
     *
     * @param types 日志类型
     * @param level 这些类型的显示级别
     */
    public void addLogTypeAll(List<T> types, Level level) {
        for (T type : types)
            addLogType(type, level);
    }

    @PreDestroy
    public void destroy() {
        executor.shutdownNow();
    }

    public enum Level {
        info,
        success,
        error
    }
}


如何使用》

fxml直接将组件加入

<LogAreaType fx:id="logAreaType" max_line="200" style="-fx-padding: 10px;" spacing="5"/>

然后对其添加日志类型及显示级别

logAreaType.addLogType(AppLogType.网关所有, LogAreaType.Level.info );
logAreaType.addLogType(AppLogType.网关错误, LogAreaType.Level.error );
logAreaType.addLogType(AppLogType.事件成功, LogAreaType.Level.success );
logAreaType.addLogType(AppLogType.事件失败, LogAreaType.Level.error );

最后直接调用方法即可

logAreaType.addLog(finalMessage,AppView.AppLogType.网关所有);

Ps: 类型是用的toString进行显示及保存的

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值