文章目录
- 分离日志类和界面类的完整代码
- 1. `logger.py` - 日志类实现
- 2. `main_window.py` - 界面类实现
- 代码结构说明
分离日志类和界面类的完整代码
以下是将日志类和界面类分开的完整代码结构,包含两个主要文件:logger.py
和 main_window.py
。
1. logger.py
- 日志类实现
import os
import datetime
import inspect
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QMutexclass Logger(QObject):"""线程安全的日志记录器,支持日志级别和GUI显示"""log_message = pyqtSignal(str, int) # 信号:日志消息,日志级别# 日志级别定义DEBUG = 0INFO = 1WARNING = 2ERROR = 3CRITICAL = 4# 日志级别文本和颜色LOG_LEVELS = {DEBUG: ("DEBUG", (128, 128, 128)), # 灰色INFO: ("INFO", (0, 0, 0)), # 黑色WARNING: ("WARNING", (255, 128, 0)), # 橙色ERROR: ("ERROR", (255, 0, 0)), # 红色CRITICAL: ("CRITICAL", (255, 0, 255)) # 紫色}def __init__(self, parent=None):super().__init__(parent)self.log_file = Noneself.log_level = self.INFOself.mutex = QMutex() # 用于线程安全self.max_log_size = 10 * 1024 * 1024 # 10MBself.log_to_file = Falseself.show_caller_info = True # 是否显示调用者信息def set_log_file(self, file_path):"""设置日志文件路径"""self.log_file = file_pathself.log_to_file = Trueself._rotate_log_file()def set_log_level(self, level):"""设置日志级别"""self.log_level = leveldef set_show_caller_info(self, show=True):"""设置是否显示调用者信息"""self.show_caller_info = showdef 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_backfunction_name = frame.f_code.co_namefile_name = os.path.basename(frame.f_code.co_filename)line_number = frame.f_linenoreturn 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:# 发送日志消息到GUIself.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 = 1backup_file = f"{base_name}.{counter}{ext}"while os.path.exists(backup_file):counter += 1backup_file = f"{base_name}.{counter}{ext}"# 重命名当前日志文件os.rename(self.log_file, backup_file)
2. main_window.py
- 界面类实现
import sys
import datetime
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, QLabel, QComboBox, QFileDialog, QAction, QMenuBar, QStatusBar)
from PyQt5.QtGui import QFont, QColor, QTextCursor, QTextCharFormat
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QThread, QMutex, QMetaObject
from logger import Loggerclass 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()r, g, b = Logger.LOG_LEVELS[level][1]char_format.setForeground(QColor(r, g, b))# 移动到文本末尾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 = 10000if 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 = loggerself.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 = loggerdef 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_())
代码结构说明
-
logger.py
- 包含完整的Logger类实现
- 负责日志记录的核心功能,包括日志级别控制、日志文件管理、线程安全等
- 提供日志信号用于与GUI通信
-
main_window.py
- 包含所有界面相关的类实现
- LogViewer: 日志显示组件
- LogLevelFilter: 日志级别过滤组件
- LogSaver: 日志保存组件
- LogTester: 测试按钮组件
- LoggerDemo: 主窗口类
-
依赖关系
- 界面类通过导入Logger类来使用日志功能
- 日志类通过信号与界面类通信,实现日志的实时显示
这种分离方式使代码更加模块化,日志类可以独立于界面使用,也便于后续维护和扩展。