有两个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进行显示及保存的