功能说明
1. LogTester类:
- 新增组件,包含五个测试按钮,分别对应五种日志级别
- 每个按钮有不同的背景颜色,便于直观区分
2. 按钮样式:
- DEBUG: 灰色背景
- INFO: 黑色背景
- WARNING: 橙色背景
- ERROR: 红色背景
- CRITICAL: 紫色背景
3. 按钮功能:
- 点击按钮会发送对应级别的日志消息
- 日志消息包含调用函数名和行号
4. 界面布局:
- 测试按钮位于窗口顶部,方便快速测试
- 保持原有功能不变,同时增加了新的测试方式
现在你可以通过点击这五个按钮来快速测试不同级别的日志功能,观察它们在日志显示区域的不同颜色和格式。
5.代码
import sys
import os
import time
import datetime
import inspect
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QTextEdit, QPushButton, QLabel, QComboBox,
QFileDialog, QSplitter, QAction, QMenuBar,
QToolBar, QStatusBar, QMessageBox, QDockWidget)
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QThread, QMutex, QMetaObject
from PyQt5.QtGui import QFont, QColor, QTextCursor, QTextCharFormat
class Logger(QObject):
"""
线程安全的日志记录器,支持日志级别和GUI显示
"""
log_message = pyqtSignal(str, int) # 信号:日志消息,日志级别
# 日志级别定义
DEBUG = 0
INFO = 1
WARNING = 2
ERROR = 3
CRITICAL = 4
# 日志级别文本和颜色
LOG_LEVELS = {
DEBUG: ("DEBUG", QColor(128, 128, 128)), # 灰色
INFO: ("INFO", QColor(0, 0, 0)), # 黑色
WARNING: ("WARNING", QColor(255, 128, 0)), # 橙色
ERROR: ("ERROR", QColor(255, 0, 0)), # 红色
CRITICAL: ("CRITICAL", QColor(255, 0, 255)) # 紫色
}
def __init__(self, parent=None):
super().__init__(parent)
self.log_file = None
self.log_level = self.INFO
self.mutex = QMutex() # 用于线程安全
self.max_log_size = 10 * 1024 * 1024 # 10MB
self.log_to_file = False
self.show_caller_info = True # 是否显示调用者信息
def set_log_file(self, file_path):
"""设置日志文件路径"""
self.log_file = file_path
self.log_to_file = True
self._rotate_log_file()
def set_log_level(self, level):
"""设置日志级别"""
self.log_level = level
def set_show_caller_info(self, show=True):
"""设置是否显示调用者信息"""
self.show_caller_info = show
def debug(self, message):
"""记录调试级别的日志"""
self._log(self.DEBUG, message)
def info(self, message):
"""记录信息级别的日志"""
self._log(self.INFO, message)
def warning(self, message):
"""记录警告级别的日志"""
self._log(self.WARNING, message)
def error(self, message):
"""记录错误级别的日志"""
self._log(self.ERROR, message)
def critical(self, message):
"""记录严重错误级别的日志"""
self._log(self.CRITICAL, message)
def _get_caller_info(self):
"""获取调用者信息(函数名和行号)"""
try:
# 从栈中获取调用者信息
# 0: _get_caller_info, 1: _log, 2: debug/info/warning/error/critical, 3: 实际调用位置
frame = inspect.currentframe().f_back.f_back.f_back
function_name = frame.f_code.co_name
file_name = os.path.basename(frame.f_code.co_filename)
line_number = frame.f_lineno
return f"{file_name}:{function_name}():{line_number}"
except Exception as e:
return "unknown:unknown():0"
def _log(self, level, message):
"""内部日志记录方法"""
if level < self.log_level:
return
# 获取当前时间
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
# 获取调用者信息
caller_info = self._get_caller_info() if self.show_caller_info else ""
# 构建完整的日志消息
level_text = self.LOG_LEVELS[level][0]
# 如果有调用者信息,添加到日志消息中
if caller_info:
full_message = f"[{timestamp}] [{level_text}] [{caller_info}] {message}"
else:
full_message = f"[{timestamp}] [{level_text}] {message}"
# 线程安全
self.mutex.lock()
try:
# 发送日志消息到GUI
self.log_message.emit(full_message, level)
# 写入日志文件
if self.log_to_file and self.log_file:
self._write_to_file(full_message)
finally:
self.mutex.unlock()
def _write_to_file(self, message):
"""写入日志到文件"""
try:
# 检查文件大小并旋转日志
self._rotate_log_file()
# 写入日志
with open(self.log_file, "a", encoding="utf-8") as f:
f.write(f"{message}\n")
except Exception as e:
# 如果写入日志文件失败,尝试发送错误消息
error_msg = f"写入日志文件失败: {str(e)}"
if hasattr(self, 'log_message'):
self.log_message.emit(f"[ERROR] {error_msg}", self.ERROR)
def _rotate_log_file(self):
"""旋转日志文件(当日志文件达到最大大小时创建新文件)"""
if not self.log_to_file or not self.log_file:
return
# 检查文件是否存在并且超过最大大小
if os.path.exists(self.log_file):
file_size = os.path.getsize(self.log_file)
if file_size >= self.max_log_size:
# 获取文件名和扩展名
base_name, ext = os.path.splitext(self.log_file)
# 查找可用的备份文件名
counter = 1
backup_file = f"{base_name}.{counter}{ext}"
while os.path.exists(backup_file):
counter += 1
backup_file = f"{base_name}.{counter}{ext}"
# 重命名当前日志文件
os.rename(self.log_file, backup_file)
class LogViewer(QWidget):
"""
日志查看器组件,用于显示日志消息
"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
# 创建主布局
layout = QVBoxLayout(self)
# 创建日志显示区域
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setUndoRedoEnabled(False) # 禁用撤销/重做,节省内存
self.log_text.setLineWrapMode(QTextEdit.NoWrap)
# 设置字体,使用等宽字体便于查看
font = QFont("Consolas", 10)
font.setFixedPitch(True)
self.log_text.setFont(font)
# 添加到布局
layout.addWidget(self.log_text)
def append_log(self, message, level):
"""添加日志消息到显示区域"""
# 创建字符格式
char_format = QTextCharFormat()
char_format.setForeground(Logger.LOG_LEVELS[level][1])
# 移动到文本末尾
cursor = self.log_text.textCursor()
cursor.movePosition(QTextCursor.End)
# 设置字符格式
cursor.setCharFormat(char_format)
# 插入新日志
cursor.insertText(f"{message}\n")
# 滚动到底部
self.log_text.setTextCursor(cursor)
self.log_text.ensureCursorVisible()
# 限制日志行数,防止内存溢出
max_lines = 10000
if self.log_text.document().blockCount() > max_lines:
cursor.setPosition(0)
cursor.movePosition(QTextCursor.EndOfLine)
cursor.removeSelectedText()
cursor.deleteChar()
self.log_text.setTextCursor(cursor)
class LogLevelFilter(QWidget):
"""
日志级别过滤器组件
"""
log_level_changed = pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
# 创建布局
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# 添加标签
label = QLabel("日志级别:")
layout.addWidget(label)
# 添加下拉框
self.level_combo = QComboBox()
self.level_combo.addItems(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])
self.level_combo.setCurrentIndex(Logger.INFO) # 默认显示INFO级别
self.level_combo.currentIndexChanged.connect(self.on_level_changed)
layout.addWidget(self.level_combo)
# 添加拉伸
layout.addStretch()
def on_level_changed(self, index):
"""日志级别变更处理"""
self.log_level_changed.emit(index)
def set_log_level(self, level):
"""设置当前日志级别"""
self.level_combo.setCurrentIndex(level)
class LogSaver(QWidget):
"""
日志保存组件
"""
save_log_requested = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
# 创建布局
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# 添加保存按钮
self.save_button = QPushButton("保存日志")
self.save_button.clicked.connect(self.on_save_clicked)
layout.addWidget(self.save_button)
# 添加拉伸
layout.addStretch()
def on_save_clicked(self):
"""保存按钮点击处理"""
self.save_log_requested.emit()
class LogTester(QWidget):
"""
日志测试组件,包含五个测试按钮
"""
def __init__(self, logger, parent=None):
super().__init__(parent)
self.logger = logger
self.init_ui()
def init_ui(self):
# 创建布局
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# 添加五个测试按钮
self.debug_button = QPushButton("DEBUG")
self.debug_button.setStyleSheet("background-color: #808080; color: white;")
self.debug_button.clicked.connect(self.on_debug_clicked)
layout.addWidget(self.debug_button)
self.info_button = QPushButton("INFO")
self.info_button.setStyleSheet("background-color: #000000; color: white;")
self.info_button.clicked.connect(self.on_info_clicked)
layout.addWidget(self.info_button)
self.warning_button = QPushButton("WARNING")
self.warning_button.setStyleSheet("background-color: #FF8000; color: white;")
self.warning_button.clicked.connect(self.on_warning_clicked)
layout.addWidget(self.warning_button)
self.error_button = QPushButton("ERROR")
self.error_button.setStyleSheet("background-color: #FF0000; color: white;")
self.error_button.clicked.connect(self.on_error_clicked)
layout.addWidget(self.error_button)
self.critical_button = QPushButton("CRITICAL")
self.critical_button.setStyleSheet("background-color: #FF00FF; color: white;")
self.critical_button.clicked.connect(self.on_critical_clicked)
layout.addWidget(self.critical_button)
def on_debug_clicked(self):
"""DEBUG按钮点击处理"""
self.logger.debug("这是一条DEBUG级别的测试日志")
def on_info_clicked(self):
"""INFO按钮点击处理"""
self.logger.info("这是一条INFO级别的测试日志")
def on_warning_clicked(self):
"""WARNING按钮点击处理"""
self.logger.warning("这是一条WARNING级别的测试日志")
def on_error_clicked(self):
"""ERROR按钮点击处理"""
self.logger.error("这是一条ERROR级别的测试日志")
def on_critical_clicked(self):
"""CRITICAL按钮点击处理"""
self.logger.critical("这是一条CRITICAL级别的测试日志")
class LoggerDemo(QMainWindow):
"""
日志类演示主窗口
"""
def __init__(self):
super().__init__()
self.logger = Logger()
self.init_ui()
self.setup_logger()
def init_ui(self):
# 设置窗口属性
self.setWindowTitle("PyQt5 日志类演示")
self.setGeometry(100, 100, 800, 600)
# 创建中央部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 创建日志测试组件
self.log_tester = LogTester(self.logger)
main_layout.addWidget(self.log_tester)
# 创建日志查看器
self.log_viewer = LogViewer()
# 创建日志级别过滤器
self.log_filter = LogLevelFilter()
self.log_filter.log_level_changed.connect(self.on_log_level_changed)
# 创建日志保存组件
self.log_saver = LogSaver()
self.log_saver.save_log_requested.connect(self.on_save_log)
# 创建控制区域
control_layout = QHBoxLayout()
control_layout.addWidget(self.log_filter)
control_layout.addWidget(self.log_saver)
# 添加到主布局
main_layout.addLayout(control_layout)
main_layout.addWidget(self.log_viewer)
# 创建菜单栏
self.create_menu_bar()
# 创建状态栏
self.statusBar().showMessage("就绪")
def create_menu_bar(self):
"""创建菜单栏"""
menu_bar = self.menuBar()
# 文件菜单
file_menu = menu_bar.addMenu("文件")
# 保存日志动作
save_action = QAction("保存日志", self)
save_action.setShortcut("Ctrl+S")
save_action.triggered.connect(self.on_save_log)
file_menu.addAction(save_action)
# 设置日志文件动作
set_log_file_action = QAction("设置日志文件", self)
set_log_file_action.triggered.connect(self.on_set_log_file)
file_menu.addAction(set_log_file_action)
# 退出动作
exit_action = QAction("退出", self)
exit_action.setShortcut("Ctrl+Q")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 日志菜单
log_menu = menu_bar.addMenu("日志")
# 清空日志动作
clear_action = QAction("清空日志", self)
clear_action.triggered.connect(self.on_clear_log)
log_menu.addAction(clear_action)
# 模拟日志动作
simulate_menu = log_menu.addMenu("模拟日志")
# 添加各种级别的模拟日志动作
levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
for level in levels:
action = QAction(f"发送{level}日志", self)
action.triggered.connect(lambda checked, l=level: self.on_simulate_log(l))
simulate_menu.addAction(action)
def setup_logger(self):
"""设置日志记录器"""
# 连接日志信号到日志查看器
self.logger.log_message.connect(self.log_viewer.append_log)
# 设置初始日志级别
self.logger.set_log_level(Logger.INFO)
# 记录启动信息
self.logger.info("日志系统已启动")
def on_log_level_changed(self, level):
"""日志级别变更处理"""
self.logger.set_log_level(level)
self.statusBar().showMessage(f"日志级别已设置为: {Logger.LOG_LEVELS[level][0]}")
def on_save_log(self):
"""保存日志文件"""
file_path, _ = QFileDialog.getSaveFileName(
self, "保存日志", f"log_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.txt",
"文本文件 (*.txt);;所有文件 (*)"
)
if file_path:
try:
with open(file_path, "w", encoding="utf-8") as f:
f.write(self.log_viewer.log_text.toPlainText())
self.logger.info(f"日志已保存到: {file_path}")
except Exception as e:
self.logger.error(f"保存日志失败: {str(e)}")
def on_set_log_file(self):
"""设置日志文件路径"""
file_path, _ = QFileDialog.getSaveFileName(
self, "设置日志文件", "application.log",
"日志文件 (*.log);;文本文件 (*.txt);;所有文件 (*)"
)
if file_path:
self.logger.set_log_file(file_path)
self.logger.info(f"日志文件已设置为: {file_path}")
def on_clear_log(self):
"""清空日志显示"""
self.log_viewer.log_text.clear()
self.logger.info("日志已清空")
def on_simulate_log(self, level):
"""模拟发送不同级别的日志"""
level_map = {
"DEBUG": self.logger.debug,
"INFO": self.logger.info,
"WARNING": self.logger.warning,
"ERROR": self.logger.error,
"CRITICAL": self.logger.critical
}
if level in level_map:
message = f"这是一条{level}级别的测试日志,时间: {datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3]}"
level_map[level](message)
else:
self.logger.warning(f"未知日志级别: {level}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = LoggerDemo()
window.show()
# 模拟多线程日志记录
class LogThread(QThread):
def __init__(self, logger):
super().__init__()
self.logger = logger
def run(self):
for i in range(10):
self.logger.debug(f"线程 {self.currentThreadId()} 发送调试消息 {i}")
self.msleep(500)
self.logger.info(f"线程 {self.currentThreadId()} 发送信息消息 {i}")
self.msleep(500)
thread1 = LogThread(window.logger)
thread2 = LogThread(window.logger)
thread1.start()
thread2.start()
sys.exit(app.exec_())