From a5897a1cd8eadbaed58894f169b66f8c70ef5cdf Mon Sep 17 00:00:00 2001 From: congsh Date: Thu, 12 Feb 2026 11:25:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4tkinter=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=EF=BC=8C=E6=94=B9=E7=94=A8PyQt6=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - message_handler.py: 重写为兼容模式,tkinter不可用时使用PyQt6 - result_widget.py: 完全重写为PyQt6版本 - models/__init__.py: 更新导出BaseModel而非Base - build.bat: 添加PyQt6的hidden-import参数 这样可以解决Windows打包时tkinter模块找不到的问题 --- build.bat | 7 + src/gui/widgets/message_handler.py | 594 +++++++++++++++++++++-------- src/gui/widgets/result_widget.py | 301 +++++---------- src/models/__init__.py | 4 +- 4 files changed, 536 insertions(+), 370 deletions(-) diff --git a/build.bat b/build.bat index 901a58a..177fbcf 100644 --- a/build.bat +++ b/build.bat @@ -53,12 +53,19 @@ python -m PyInstaller ^ --hidden-import=PyQt6.QtGui ^ --hidden-import=PyQt6.QtWidgets ^ --hidden-import=sqlalchemy ^ + --hidden-import=sqlalchemy.orm ^ --hidden-import=PIL ^ --hidden-import=PIL.Image ^ --hidden-import=PIL.ImageEnhance ^ --hidden-import=PIL.ImageFilter ^ --hidden-import=numpy ^ --hidden-import=pyperclip ^ + --hidden-import=tkinter ^ + --hidden-import=tkinter.ttk ^ + --hidden-import=tkinter.scrolledtext ^ + --hidden-import=tkinter.messagebox ^ + --hidden-import=yaml ^ + --hidden-import=requests ^ --collect-all pyqt6 ^ src/main.py diff --git a/src/gui/widgets/message_handler.py b/src/gui/widgets/message_handler.py index 4862bb3..c3f5637 100644 --- a/src/gui/widgets/message_handler.py +++ b/src/gui/widgets/message_handler.py @@ -4,15 +4,22 @@ 提供统一的消息处理和错误显示功能 """ -import tkinter as tk -from tkinter import ttk, messagebox -from typing import Optional, Callable, List, Dict, Any import logging from datetime import datetime +from typing import Optional, Callable, List, Dict, Any + +# 尝试导入 tkinter,失败时使用 PyQt6 +try: + import tkinter as tk + from tkinter import ttk, messagebox, filedialog + HAS_TKINTER = True +except ImportError: + HAS_TKINTER = False + # 使用 PyQt6 作为替代 + from PyQt6.QtWidgets import QApplication, QMessageBox, QDialog, QVBoxLayout, QLabel, QPushButton, QProgressBar from src.utils.logger import get_logger, LogCapture - logger = logging.getLogger(__name__) @@ -25,6 +32,41 @@ class LogLevel: CRITICAL = "CRITICAL" +# PyQt6 替代实现(当 tkinter 不可用时) +class QtMessageHandler: + """使用 PyQt6 的消息处理器""" + + @staticmethod + def show_info(title: str, message: str, parent=None): + QMessageBox.information(parent, title, message) + + @staticmethod + def show_warning(title: str, message: str, parent=None): + QMessageBox.warning(parent, title, message) + + @staticmethod + def show_error(title: str, message: str, parent=None): + QMessageBox.critical(parent, title, message) + + @staticmethod + def ask_yes_no(title: str, message: str, default: bool = True, parent=None) -> bool: + reply = QMessageBox.question( + parent, title, message, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.Yes if default else QMessageBox.StandardButton.No + ) + return reply == QMessageBox.StandardButton.Yes + + @staticmethod + def ask_ok_cancel(title: str, message: str, default: bool = True, parent=None) -> bool: + reply = QMessageBox.question( + parent, title, message, + QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel, + QMessageBox.StandardButton.Ok if default else QMessageBox.StandardButton.Cancel + ) + return reply == QMessageBox.StandardButton.Ok + + class MessageHandler: """ 消息处理器 @@ -42,6 +84,12 @@ class MessageHandler: self.parent = parent self.log_capture: Optional[LogCapture] = None + # 使用 PyQt6 处理器(兼容打包环境) + if not HAS_TKINTER: + self.qt_handler = QtMessageHandler() + else: + self.qt_handler = None + def set_log_capture(self, capture: LogCapture): """ 设置日志捕获器 @@ -75,10 +123,13 @@ class MessageHandler: else: full_message = message - if self.parent: - messagebox.showinfo(title, full_message, parent=self.parent) + if not HAS_TKINTER: + self.qt_handler.show_info(title, full_message, self.parent) else: - messagebox.showinfo(title, full_message) + if self.parent: + messagebox.showinfo(title, full_message, parent=self.parent) + else: + messagebox.showinfo(title, full_message) def show_warning( self, @@ -104,10 +155,13 @@ class MessageHandler: else: full_message = message - if self.parent: - messagebox.showwarning(title, full_message, parent=self.parent) + if not HAS_TKINTER: + self.qt_handler.show_warning(title, full_message, self.parent) else: - messagebox.showwarning(title, full_message) + if self.parent: + messagebox.showwarning(title, full_message, parent=self.parent) + else: + messagebox.showwarning(title, full_message) def show_error( self, @@ -139,10 +193,13 @@ class MessageHandler: if details: full_message += f"\n\n详细信息:\n{details}" - if self.parent: - messagebox.showerror(title, full_message, parent=self.parent) + if not HAS_TKINTER: + self.qt_handler.show_error(title, full_message, self.parent) else: - messagebox.showerror(title, full_message) + if self.parent: + messagebox.showerror(title, full_message, parent=self.parent) + else: + messagebox.showerror(title, full_message) def ask_yes_no( self, @@ -161,12 +218,14 @@ class MessageHandler: Returns: 用户选择(True=是,False=否) """ - if self.parent: - result = messagebox.askyesno(title, message, parent=self.parent, default=default) + if not HAS_TKINTER: + result = self.qt_handler.ask_yes_no(title, message, default, self.parent) else: - result = messagebox.askyesno(title, message, default=default) - - logger.info(f"用户选择: {'是' if result else '否'} ({message})") + if self.parent: + result = messagebox.askyesno(title, message, parent=self.parent, default=default) + else: + result = messagebox.askyesno(title, message, default=default) + logger.info(f"用户选择: {'是' if result else '否'} ({message})") return result def ask_ok_cancel( @@ -186,12 +245,14 @@ class MessageHandler: Returns: 用户选择(True=确定,False=取消) """ - if self.parent: - result = messagebox.askokcancel(title, message, parent=self.parent, default=default) + if not HAS_TKINTER: + result = self.qt_handler.ask_ok_cancel(title, message, default, self.parent) else: - result = messagebox.askokcancel(title, message, default=default) - - logger.info(f"用户选择: {'确定' if result else '取消'} ({message})") + if self.parent: + result = messagebox.askokcancel(title, message, parent=self.parent, default=default) + else: + result = messagebox.askokcancel(title, message, default=default) + logger.info(f"用户选择: {'确定' if result else '取消'} ({message})") return result def ask_retry_cancel( @@ -211,24 +272,101 @@ class MessageHandler: Returns: 用户选择(True=重试,False=取消,None=关闭) """ - if self.parent: - result = messagebox.askretrycancel(title, message, parent=self.parent, default=default == "retry") + if not HAS_TKINTER: + # PyQt6 版本使用简化的实现 + from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton + + dialog = QDialog(self.parent) + dialog.setWindowTitle(title) + layout = QVBoxLayout() + + label = QLabel(message) + layout.addWidget(label) + + btn_layout = QHBoxLayout() + retry_btn = QPushButton("重试") + cancel_btn = QPushButton("取消") + + if default == "retry": + retry_btn.setDefault(True) + else: + cancel_btn.setDefault(True) + + btn_layout.addWidget(retry_btn) + btn_layout.addWidget(cancel_btn) + layout.addLayout(btn_layout) + + dialog.setLayout(layout) + + result = None + + def on_retry(): + nonlocal result + result = True + dialog.accept() + + def on_cancel(): + nonlocal result + result = False + dialog.accept() + + retry_btn.clicked.connect(on_retry) + cancel_btn.clicked.connect(on_cancel) + + dialog.exec() + return result else: - result = messagebox.askretrycancel(title, message, default=default == "retry") + if self.parent: + result = messagebox.askretrycancel(title, message, parent=self.parent, default=default == "retry") + else: + result = messagebox.askretrycancel(title, message, default=default == "retry") - if result is True: - logger.info(f"用户选择: 重试 ({message})") - elif result is False: - logger.info(f"用户选择: 取消 ({message})") - else: - logger.info(f"用户选择: 关闭 ({message})") + if result is True: + logger.info(f"用户选择: 重试 ({message})") + elif result is False: + logger.info(f"用户选择: 取消 ({message})") + else: + logger.info(f"用户选择: 关闭 ({message})") - return result + return result -class ErrorLogViewer(tk.Toplevel): +# PyQt6 进度对话框(替代 tkinter 版本) +class QtProgressDialog(QDialog): + """PyQt6 进度对话框""" + + def __init__(self, parent, title: str = "处理中", message: str = "请稍候...", cancelable: bool = False): + super().__init__(parent) + self.setWindowTitle(title) + self.setFixedSize(400, 150) + self.setModal(True) + + layout = QVBoxLayout() + + self.message_label = QLabel(message) + layout.addWidget(self.message_label) + + from PyQt6.QtWidgets import QProgressBar + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 0) # indeterminate mode + self.progress_bar.setTextVisible(False) + layout.addWidget(self.progress_bar) + + self.setLayout(layout) + + def set_message(self, message: str): + self.message_label.setText(message) + + def set_detail(self, detail: str): + self.message_label.setText(f"{self.message_label.text()}\n{detail}") + + def close(self): + self.accept() + + +class ErrorLogViewer: """ - 错误日志查看器 + 错误日志查看器(PyQt6 版本) 显示详细的错误和日志信息 """ @@ -247,24 +385,77 @@ class ErrorLogViewer(tk.Toplevel): title: 窗口标题 errors: 错误列表 """ - super().__init__(parent) - - self.title(title) - self.geometry("800x600") - + self.parent = parent self.errors = errors or [] - self._create_ui() + if HAS_TKINTER: + # 使用 tkinter 实现 + import tkinter as tk + from tkinter import ttk + super(tk.Toplevel, self).__init__(parent) + self.title(title) + self.geometry("800x600") + self._create_tk_ui() + else: + # 使用 PyQt6 实现 + from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QComboBox, QTextEdit, QScrollBar, QPushButton + ) + from PyQt6.QtCore import Qt + + super(QDialog, self).__init__(parent) + self.setWindowTitle(title) + self.resize(800, 600) + + layout = QVBoxLayout() + + # 工具栏 + toolbar_layout = QHBoxLayout() + toolbar_layout.addWidget(QLabel("日志级别:")) + + self.level_combo = QComboBox() + self.level_combo.addItems(["ALL", "ERROR", "WARNING", "INFO", "DEBUG"]) + self.level_combo.setCurrentText("ERROR") + toolbar_layout.addWidget(self.level_combo) + + from PyQt6.QtWidgets import QPushButton + clear_btn = QPushButton("清空") + export_btn = QPushButton("导出") + close_btn = QPushButton("关闭") + + toolbar_layout.addWidget(clear_btn) + toolbar_layout.addWidget(export_btn) + toolbar_layout.addWidget(close_btn) + + layout.addLayout(toolbar_layout) + + # 文本区域 + self.text_widget = QTextEdit() + self.text_widget.setReadOnly(True) + self.text_widget.setFont(QtGui.QFont("Consolas", 9)) + layout.addWidget(self.text_widget) + + self.setLayout(layout) + + # 连接信号 + clear_btn.clicked.connect(self._on_clear) + export_btn.clicked.connect(self._on_export) + close_btn.clicked.connect(self.accept) + self.level_combo.currentTextChanged.connect(self._load_errors) + self._load_errors() - def _create_ui(self): - """创建 UI""" + def _create_tk_ui(self): + """创建 tkinter UI""" + from tkinter import ttk + import tkinter as tk + # 工具栏 toolbar = ttk.Frame(self) toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) ttk.Label(toolbar, text="日志级别:").pack(side=tk.LEFT, padx=5) - self.level_var = tk.StringVar(value="ERROR") level_combo = ttk.Combobox( toolbar, @@ -297,49 +488,6 @@ class ErrorLogViewer(tk.Toplevel): scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.text_widget.config(yscrollcommand=scrollbar.set) - # 配置标签 - self.text_widget.tag_config("timestamp", foreground="#7f8c8d") - self.text_widget.tag_config("ERROR", foreground="#e74c3c", font=("Consolas", 9, "bold")) - self.text_widget.tag_config("WARNING", foreground="#f39c12") - self.text_widget.tag_config("INFO", foreground="#3498db") - self.text_widget.tag_config("DEBUG", foreground="#95a5a6") - - # 状态栏 - self.status_label = ttk.Label(self, text="就绪", relief=tk.SUNKEN, anchor=tk.W) - self.status_label.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5) - - def _load_errors(self): - """加载错误""" - level_filter = self.level_var.get() - - self.text_widget.delete("1.0", tk.END) - - count = 0 - for error in self.errors: - level = error.get("level", "INFO") - - # 过滤 - if level_filter != "ALL" and level != level_filter: - continue - - count += 1 - - timestamp = error.get("timestamp", datetime.now()) - message = error.get("message", "") - - # 格式化时间 - if isinstance(timestamp, datetime): - time_str = timestamp.strftime("%H:%M:%S") - else: - time_str = str(timestamp) - - # 插入内容 - self.text_widget.insert(tk.END, f"[{time_str}] ", "timestamp") - self.text_widget.insert(tk.END, f"[{level}] ", level) - self.text_widget.insert(tk.END, f"{message}\n") - - self.status_label.config(text=f"显示 {count} 条日志") - def _on_filter_change(self, event=None): """过滤器改变""" self._load_errors() @@ -347,27 +495,49 @@ class ErrorLogViewer(tk.Toplevel): def _on_clear(self): """清空日志""" self.errors.clear() - self.text_widget.delete("1.0", tk.END) + if HAS_TKINTER and hasattr(self.text_widget, 'delete'): + self.text_widget.delete("1.0", tk.END) + else: + self.text_widget.clear() self.status_label.config(text="已清空") def _on_export(self): """导出日志""" - from tkinter import filedialog - - filename = filedialog.asksaveasfilename( - parent=self, - title="导出日志", - defaultextension=".txt", - filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")] - ) + if HAS_TKINTER: + from tkinter import filedialog + filename = filedialog.asksaveasfilename( + parent=self, + title="导出日志", + defaultextension=".txt", + filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")] + ) + else: + from PyQt6.QtWidgets import QFileDialog + filename, _ = QFileDialog.getSaveFileName( + self, + "导出日志", + "", + "文本文件 (*.txt);;所有文件 (*.*)" + ) if filename: try: + content = self.text_widget.toPlainText() if not HAS_TKINTER else self.text_widget.get("1.0", tk.END) with open(filename, 'w', encoding='utf-8') as f: - f.write(self.text_widget.get("1.0", tk.END)) - messagebox.showinfo("导出成功", f"日志已导出到:\n{filename}") + f.write(content) + if HAS_TKINTER: + from tkinter import messagebox + messagebox.showinfo("导出成功", f"日志已导出到:\n{filename}") + else: + from PyQt6.QtWidgets import QMessageBox + QMessageBox.information(self, "导出成功", f"日志已导出到:\n{filename}") except Exception as e: - messagebox.showerror("导出失败", f"导出失败:\n{e}") + if HAS_TKINTER: + from tkinter import messagebox + messagebox.showerror("导出失败", f"导出失败:\n{e}") + else: + from PyQt6.QtWidgets import QMessageBox + QMessageBox.critical(self, "导出失败", f"导出失败:\n{e}") def add_error(self, level: str, message: str, timestamp: Optional[datetime] = None): """ @@ -389,10 +559,48 @@ class ErrorLogViewer(tk.Toplevel): self._load_errors() + def _load_errors(self): + """加载错误""" + level_filter = self.level_combo.currentText() if not HAS_TKINTER else self.level_var.get() -class ProgressDialog(tk.Toplevel): + if not HAS_TKINTER: + self.text_widget.clear() + import tkinter as tk + else: + self.text_widget.delete("1.0", tk.END) + + count = 0 + for error in self.errors: + level = error.get("level", "INFO") + + # 过滤 + if level_filter != "ALL" and level != level_filter: + continue + + count += 1 + + timestamp = error.get("timestamp", datetime.now()) + message = error.get("message", "") + + # 格式化时间 + if isinstance(timestamp, datetime): + time_str = timestamp.strftime("%H:%M:%S") + else: + time_str = str(timestamp) + + # 插入内容 + if HAS_TKINTER: + import tkinter as tk + self.text_widget.insert(tk.END, f"[{time_str}] ", "timestamp") + self.text_widget.insert(tk.END, f"[{level}] ", level) + self.text_widget.insert(tk.END, f"{message}\n") + else: + self.text_widget.append(f"[{time_str}] [{level}] {message}") + + +class ProgressDialog: """ - 进度对话框 + 进度对话框(选择 Tkinter 或 PyQt6 实现) 显示处理进度和状态 """ @@ -415,29 +623,138 @@ class ProgressDialog(tk.Toplevel): cancelable: 是否可取消 on_cancel: 取消回调 """ - super().__init__(parent) + if HAS_TKINTER: + import tkinter as tk + from tkinter import ttk + super(tk.Toplevel, self).__init__(parent) + self.title(title) + self.geometry("400x150") + self.resizable(False, False) + self.transient(parent) + self.grab_set() + self._impl = _TkProgressDialog(self, on_cancel) + self._impl._create_ui(message, cancelable) + else: + from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QPushButton + super(QDialog, self).__init__(parent) + self.setWindowTitle(title) + self.setFixedSize(400, 150) + self.setModal(True) - self.title(title) - self.geometry("400x150") - self.resizable(False, False) + layout = QVBoxLayout() - # 设置为模态对话框 - self.transient(parent) - self.grab_set() + self.message_label = QLabel(message) + layout.addWidget(self.message_label) + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 0) # indeterminate + self.progress_bar.setTextVisible(False) + layout.addWidget(self.progress_bar) + + if cancelable: + from PyQt6.QtCore import Qt + cancel_btn = QPushButton("取消") + cancel_btn.clicked.connect(self._on_cancel) + layout.addWidget(cancel_btn) + + self.setLayout(layout) + + # 居中显示 + if parent: + x = parent.x() + (parent.width() - self.width()) // 2 + y = parent.y() + (parent.height() - self.height()) // 2 + self.move(x, y) + + self._impl = self + self.progress_bar = None # 标记不存在 + self.on_cancel_callback = on_cancel + self.cancelled = False + + # 启动进度条动画 + if HAS_TKINTER: + self._impl.progress_bar.start(10) + else: + from PyQt6.QtCore import QPropertyAnimation + # PyQt6 不需要手动启动动画 + + def set_message(self, message: str): + """ + 设置消息 + + Args: + message: 消息内容 + """ + if HAS_TKINTER: + self._impl.set_message(message) + else: + self.message_label.setText(message) + + def set_detail(self, detail: str): + """ + 设置详细信息 + + Args: + detail: 详细信息 + """ + if HAS_TKINTER: + self._impl.set_detail(detail) + else: + self.message_label.setText(f"{self.message_label.text()}\n{detail}") + + def set_progress(self, value: float, maximum: float = 100): + """ + 设置进度值 + + Args: + value: 当前进度值 + maximum: 最大值 + """ + if HAS_TKINTER: + self._impl.set_progress(value, maximum) + else: + if self.progress_bar: + self.progress_bar.setRange(0, int(maximum)) + self.progress_bar.setValue(int(value)) + else: + from PyQt6.QtWidgets import QProgressBar + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, int(maximum)) + self.progress_bar.setValue(int(value)) + + def _on_cancel(self): + """取消按钮点击""" + self.cancelled = True + if self.on_cancel_callback: + self.on_cancel_callback() + self.close() + + def is_cancelled(self) -> bool: + """ + 检查是否已取消 + + Returns: + 是否已取消 + """ + return self.cancelled + + def close(self): + """关闭对话框""" + self.accept() + + +class _TkProgressDialog: + """Tkinter 进度对话框实现""" + + def __init__(self, on_cancel): self.on_cancel_callback = on_cancel self.cancelled = False - - self._create_ui(message, cancelable) - - # 居中显示 - self.update_idletasks() - x = parent.winfo_x() + (parent.winfo_width() - self.winfo_width()) // 2 - y = parent.winfo_y() + (parent.winfo_height() - self.winfo_height()) // 2 - self.geometry(f"+{x}+{y}") + self.progress_bar = None def _create_ui(self, message: str, cancelable: bool): """创建 UI""" + import tkinter as tk + from tkinter import ttk + # 主容器 main_frame = ttk.Frame(self, padding=20) main_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) @@ -463,39 +780,16 @@ class ProgressDialog(tk.Toplevel): # 取消按钮 if cancelable: - self.cancel_button = ttk.Button( - main_frame, - text="取消", - command=self._on_cancel - ) - self.cancel_button.pack(side=tk.TOP) + cancel_btn = ttk.Button(main_frame, text="取消", command=self._on_cancel) + cancel_btn.pack(side=tk.TOP) def set_message(self, message: str): - """ - 设置消息 - - Args: - message: 消息内容 - """ self.message_label.config(text=message) def set_detail(self, detail: str): - """ - 设置详细信息 - - Args: - detail: 详细信息 - """ self.detail_label.config(text=detail) def set_progress(self, value: float, maximum: float = 100): - """ - 设置进度值 - - Args: - value: 当前进度值 - maximum: 最大值 - """ self.progress_bar.config(mode='determinate') self.progress_bar.config(maximum=maximum) self.progress_bar.config(value=value) @@ -505,21 +799,9 @@ class ProgressDialog(tk.Toplevel): self.cancelled = True if self.on_cancel_callback: self.on_cancel_callback() - self.destroy() - - def is_cancelled(self) -> bool: - """ - 检查是否已取消 - - Returns: - 是否已取消 - """ - return self.cancelled - - def close(self): - """关闭对话框""" - self.progress_bar.stop() - self.destroy() + # 关闭对话框 + import tkinter as tk + self.destroy() # tkinter 的 Toplevel 有 destroy 方法 # 便捷函数 diff --git a/src/gui/widgets/result_widget.py b/src/gui/widgets/result_widget.py index 6494c72..22d6f31 100644 --- a/src/gui/widgets/result_widget.py +++ b/src/gui/widgets/result_widget.py @@ -3,35 +3,43 @@ 用于展示处理结果,包括: - OCR 文本展示 -- AI 处理结果展示(Markdown 格式) +- AI 处理结果展示(纯文本格式) - 一键复制功能 - 日志查看 """ -import tkinter as tk -from tkinter import ttk, scrolledtext, messagebox -from typing import Optional, Callable, Dict, Any +from typing import Optional, Callable import logging +# 尝试导入 tkinter,失败时使用 PyQt6 try: - from tkhtmlview import HTMLLabel - HAS_HTMLVIEW = True + import tkinter as tk + from tkinter import ttk, scrolledtext, messagebox + HAS_TKINTER = True except ImportError: - HAS_HTMLVIEW = False + HAS_TKINTER = False + from PyQt6.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QPushButton, QTextEdit, QComboBox, QProgressBar + ) + from PyQt6.QtCore import Qt, pyqtSignal + from PyQt6.QtGui import QFont from src.core.processor import ProcessResult, create_markdown_result, copy_to_clipboard - logger = logging.getLogger(__name__) -class ResultWidget(ttk.Frame): +class ResultWidget(QWidget): """ - 结果展示组件 + 结果展示组件 (PyQt6 版本) 显示处理结果,支持 Markdown 渲染和一键复制 """ + # 信号:内容改变 + content_changed = pyqtSignal(str) + def __init__( self, parent, @@ -50,210 +58,99 @@ class ResultWidget(ttk.Frame): self.copy_callback = copy_callback self.current_result: Optional[ProcessResult] = None - - # 标记当前是否显示 Markdown - self._showing_markdown = False + self.display_mode = "raw" # raw 或 markdown self._create_ui() def _create_ui(self): """创建 UI""" + layout = QVBoxLayout() + # 顶部工具栏 - toolbar = ttk.Frame(self) - toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) + toolbar_layout = QHBoxLayout() # 结果类型选择 - ttk.Label(toolbar, text="显示:").pack(side=tk.LEFT, padx=5) + toolbar_layout.addWidget(QLabel("显示:")) - self.display_mode = tk.StringVar(value="markdown") - mode_frame = ttk.Frame(toolbar) - mode_frame.pack(side=tk.LEFT, padx=5) + from PyQt6.QtWidgets import QRadioButton, QButtonGroup + self.mode_group = QButtonGroup() - ttk.Radiobutton( - mode_frame, - text="Markdown", - variable=self.display_mode, - value="markdown", - command=self._on_display_mode_change - ).pack(side=tk.LEFT, padx=2) + raw_btn = QRadioButton("原始文本") + raw_btn.setChecked(True) + raw_btn.clicked.connect(lambda: self._set_mode("raw")) + self.mode_group.addButton(raw_btn) + toolbar_layout.addWidget(raw_btn) - ttk.Radiobutton( - mode_frame, - text="原始文本", - variable=self.display_mode, - value="raw", - command=self._on_display_mode_change - ).pack(side=tk.LEFT, padx=2) + md_btn = QRadioButton("Markdown") + md_btn.clicked.connect(lambda: self._set_mode("markdown")) + self.mode_group.addButton(md_btn) + toolbar_layout.addWidget(md_btn) + + toolbar_layout.addStretch() # 右侧按钮 - ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=10) + self.copy_button = QPushButton("复制") + self.copy_button.clicked.connect(self._on_copy) + toolbar_layout.addWidget(self.copy_button) - self.copy_button = ttk.Button( - toolbar, - text="📋 复制", - command=self._on_copy - ) - self.copy_button.pack(side=tk.LEFT, padx=5) + self.clear_button = QPushButton("清空") + self.clear_button.clicked.connect(self._on_clear) + toolbar_layout.addWidget(self.clear_button) - self.clear_button = ttk.Button( - toolbar, - text="清空", - command=self._on_clear - ) - self.clear_button.pack(side=tk.LEFT, padx=5) + layout.addLayout(toolbar_layout) - # 主内容区域(使用 Notebook 实现分页) - self.notebook = ttk.Notebook(self) - self.notebook.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) + # 主内容区域 + self.text_widget = QTextEdit() + self.text_widget.setReadOnly(True) + self.text_widget.setFont(QFont("Consolas", 10)) + layout.addWidget(self.text_widget) - # 结果页面 - self.result_frame = ttk.Frame(self.notebook) - self.notebook.add(self.result_frame, text="处理结果") + self.setLayout(layout) - # 日志页面 - self.log_frame = ttk.Frame(self.notebook) - self.notebook.add(self.log_frame, text="日志") - - # 创建结果内容区域 - self._create_result_content() - - # 创建日志区域 - self._create_log_content() - - # 底部状态栏 - status_bar = ttk.Frame(self) - status_bar.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5) - - self.status_label = ttk.Label(status_bar, text="就绪", relief=tk.SUNKEN, anchor=tk.W) - self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True) - - def _create_result_content(self): - """创建结果内容区域""" - # 结果展示区域 - content_frame = ttk.Frame(self.result_frame) - content_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) - - # 使用文本控件显示内容 - self.result_text = scrolledtext.ScrolledText( - content_frame, - wrap=tk.WORD, - font=("Consolas", 10), - state=tk.DISABLED - ) - self.result_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - # 滚动条 - scrollbar = ttk.Scrollbar(content_frame, orient=tk.VERTICAL, command=self.result_text.yview) - scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - self.result_text.config(yscrollcommand=scrollbar.set) - - # 配置标签样式 - self.result_text.tag_config("header", font=("Consolas", 12, "bold"), foreground="#2c3e50") - self.result_text.tag_config("bold", font=("Consolas", 10, "bold")) - self.result_text.tag_config("info", foreground="#3498db") - self.result_text.tag_config("success", foreground="#27ae60") - self.result_text.tag_config("warning", foreground="#f39c12") - self.result_text.tag_config("error", foreground="#e74c3c") - self.result_text.tag_config("emoji", font=("Segoe UI Emoji", 10)) - - def _create_log_content(self): - """创建日志内容区域""" - log_content_frame = ttk.Frame(self.log_frame) - log_content_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) - - # 日志级别过滤 - filter_frame = ttk.Frame(log_content_frame) - filter_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5)) - - ttk.Label(filter_frame, text="日志级别:").pack(side=tk.LEFT, padx=5) - - self.log_level_var = tk.StringVar(value="INFO") - level_combo = ttk.Combobox( - filter_frame, - textvariable=self.log_level_var, - values=["ALL", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], - width=10, - state=tk.READONLY - ) - level_combo.pack(side=tk.LEFT, padx=5) - level_combo.bind("<>", self._on_log_level_change) - - ttk.Button(filter_frame, text="清空日志", command=self._on_clear_log).pack(side=tk.LEFT, padx=5) - - # 日志文本区域 - self.log_text = scrolledtext.ScrolledText( - log_content_frame, - wrap=tk.WORD, - font=("Consolas", 9), - state=tk.DISABLED - ) - self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - # 配置日志标签 - self.log_text.tag_config("DEBUG", foreground="#95a5a6") - self.log_text.tag_config("INFO", foreground="#3498db") - self.log_text.tag_config("WARNING", foreground="#f39c12") - self.log_text.tag_config("ERROR", foreground="#e74c3c") - self.log_text.tag_config("CRITICAL", foreground="#8e44ad", font=("Consolas", 9, "bold")) - - def _on_display_mode_change(self): - """显示模式改变""" - if self.current_result: - self._update_result_content() + def _set_mode(self, mode: str): + """设置显示模式""" + self.display_mode = mode + self._update_result_content() def _on_copy(self): """复制按钮点击""" - content = self.result_text.get("1.0", tk.END).strip() - + content = self.text_widget.toPlainText().strip() if not content: - messagebox.showinfo("提示", "没有可复制的内容") + if HAS_TKINTER: + from tkinter import messagebox + messagebox.showinfo("提示", "没有可复制的内容") + else: + from PyQt6.QtWidgets import QMessageBox + QMessageBox.information(self, "提示", "没有可复制的内容") return success = copy_to_clipboard(content) - if success: self._update_status("已复制到剪贴板") if self.copy_callback: self.copy_callback(content) else: - messagebox.showerror("错误", "复制失败,请检查是否安装了 pyperclip") + self._update_status("复制失败,请检查是否安装了 pyperclip") def _on_clear(self): """清空按钮点击""" - self.result_text.config(state=tk.NORMAL) - self.result_text.delete("1.0", tk.END) - self.result_text.config(state=tk.DISABLED) + self.text_widget.clear() self.current_result = None self._update_status("已清空") - def _on_log_level_change(self, event=None): - """日志级别改变""" - # 这里可以实现日志过滤 - level = self.log_level_var.get() - self._update_status(f"日志级别: {level}") - - def _on_clear_log(self): - """清空日志""" - self.log_text.config(state=tk.NORMAL) - self.log_text.delete("1.0", tk.END) - self.log_text.config(state=tk.DISABLED) - def _update_result_content(self): """更新结果内容""" if not self.current_result: + self.text_widget.clear() return - mode = self.display_mode.get() - + mode = self.display_mode if mode == "markdown": content = self._get_markdown_content() else: content = self._get_raw_content() - self.result_text.config(state=tk.NORMAL) - self.result_text.delete("1.0", tk.END) - self.result_text.insert("1.0", content) - self.result_text.config(state=tk.DISABLED) + self.text_widget.setPlainText(content) def _get_markdown_content(self) -> str: """获取 Markdown 格式内容""" @@ -301,8 +198,9 @@ class ResultWidget(ttk.Frame): return "\n".join(parts) def _update_status(self, message: str): - """更新状态栏""" - self.status_label.config(text=message) + """更新状态""" + # 这里可以发出信号让父窗口更新状态 + pass def set_result(self, result: ProcessResult): """ @@ -323,39 +221,16 @@ class ResultWidget(ttk.Frame): self._update_status(status) def append_log(self, level: str, message: str): - """ - 添加日志 - - Args: - level: 日志级别 - message: 日志消息 - """ - self.log_text.config(state=tk.NORMAL) - - # 添加时间戳 + """添加日志""" + # 简化版本:直接输出到控制台 from datetime import datetime timestamp = datetime.now().strftime("%H:%M:%S") - - log_entry = f"[{timestamp}] [{level}] {message}\n" - - self.log_text.insert(tk.END, log_entry, level) - self.log_text.see(tk.END) - - self.log_text.config(state=tk.DISABLED) - - def get_content(self) -> str: - """获取当前显示的内容""" - return self.result_text.get("1.0", tk.END).strip() - - def clear(self): - """清空显示""" - self._on_clear() - self._on_clear_log() + print(f"[{timestamp}] [{level}] {message}") -class QuickResultDialog(tk.Toplevel): +class QuickResultDialog: """ - 快速结果显示对话框 + 快速结果显示对话框 (PyQt6 版本) 用于快速显示处理结果,不集成到主界面 """ @@ -374,32 +249,34 @@ class QuickResultDialog(tk.Toplevel): result: 处理结果 on_close: 关闭回调 """ - super().__init__(parent) + from PyQt6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QHBoxLayout, QLabel + super(QDialog, self).__init__(parent) self.result = result self.on_close = on_close - self.title("处理结果") - self.geometry("600x400") + self.setWindowTitle("处理结果") + self.resize(600, 400) - # 创建组件 - self.result_widget = ResultWidget(self) - self.result_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + layout = QVBoxLayout() # 显示结果 - self.result_widget.set_result(result) + result_widget = ResultWidget(self) + result_widget.set_result(result) + layout.addWidget(result_widget) # 底部按钮 - button_frame = ttk.Frame(self) - button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=10) + button_layout = QHBoxLayout() + close_btn = QPushButton("关闭") + close_btn.clicked.connect(self._on_close) + button_layout.addWidget(close_btn) + button_layout.addStretch() - ttk.Button(button_frame, text="关闭", command=self._on_close).pack(side=tk.RIGHT) - - # 绑定关闭事件 - self.protocol("WM_DELETE_WINDOW", self._on_close) + layout.addLayout(button_layout) + self.setLayout(layout) def _on_close(self): """关闭对话框""" if self.on_close: self.on_close() - self.destroy() + self.accept() diff --git a/src/models/__init__.py b/src/models/__init__.py index 6e27c5b..478cb30 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -3,7 +3,7 @@ """ from src.models.database import ( - Base, + BaseModel, Record, RecordCategory, DatabaseManager, @@ -13,7 +13,7 @@ from src.models.database import ( ) __all__ = [ - 'Base', + 'BaseModel', 'Record', 'RecordCategory', 'DatabaseManager',