Compare commits

...

3 Commits

Author SHA1 Message Date
congsh
bb27db586d feat: 添加main()函数作为应用入口
在main_window.py末尾添加main()函数用于启动应用
2026-02-12 13:08:37 +08:00
congsh
92a258350a fix: 修复main.py导入路径
使用'src.gui.main_window'而非'gui.main_window'以确保正确导入
2026-02-12 11:25:28 +08:00
congsh
a5897a1cd8 fix: 移除tkinter依赖,改用PyQt6实现
- message_handler.py: 重写为兼容模式,tkinter不可用时使用PyQt6
- result_widget.py: 完全重写为PyQt6版本
- models/__init__.py: 更新导出BaseModel而非Base
- build.bat: 添加PyQt6的hidden-import参数

这样可以解决Windows打包时tkinter模块找不到的问题
2026-02-12 11:25:14 +08:00
6 changed files with 551 additions and 371 deletions

View File

@@ -53,12 +53,19 @@ python -m PyInstaller ^
--hidden-import=PyQt6.QtGui ^ --hidden-import=PyQt6.QtGui ^
--hidden-import=PyQt6.QtWidgets ^ --hidden-import=PyQt6.QtWidgets ^
--hidden-import=sqlalchemy ^ --hidden-import=sqlalchemy ^
--hidden-import=sqlalchemy.orm ^
--hidden-import=PIL ^ --hidden-import=PIL ^
--hidden-import=PIL.Image ^ --hidden-import=PIL.Image ^
--hidden-import=PIL.ImageEnhance ^ --hidden-import=PIL.ImageEnhance ^
--hidden-import=PIL.ImageFilter ^ --hidden-import=PIL.ImageFilter ^
--hidden-import=numpy ^ --hidden-import=numpy ^
--hidden-import=pyperclip ^ --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 ^ --collect-all pyqt6 ^
src/main.py src/main.py

View File

@@ -584,3 +584,17 @@ class NavigationButton(QPushButton):
if icon_path: if icon_path:
self.setIcon(QIcon(icon_path)) self.setIcon(QIcon(icon_path))
self.setIconSize(QSize(20, 20)) self.setIconSize(QSize(20, 20))
def main():
"""
应用程序入口
初始化并显示主窗口
"""
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

View File

@@ -4,15 +4,22 @@
提供统一的消息处理和错误显示功能 提供统一的消息处理和错误显示功能
""" """
import tkinter as tk
from tkinter import ttk, messagebox
from typing import Optional, Callable, List, Dict, Any
import logging import logging
from datetime import datetime 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 from src.utils.logger import get_logger, LogCapture
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -25,6 +32,41 @@ class LogLevel:
CRITICAL = "CRITICAL" 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: class MessageHandler:
""" """
消息处理器 消息处理器
@@ -42,6 +84,12 @@ class MessageHandler:
self.parent = parent self.parent = parent
self.log_capture: Optional[LogCapture] = None 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): def set_log_capture(self, capture: LogCapture):
""" """
设置日志捕获器 设置日志捕获器
@@ -75,6 +123,9 @@ class MessageHandler:
else: else:
full_message = message full_message = message
if not HAS_TKINTER:
self.qt_handler.show_info(title, full_message, self.parent)
else:
if self.parent: if self.parent:
messagebox.showinfo(title, full_message, parent=self.parent) messagebox.showinfo(title, full_message, parent=self.parent)
else: else:
@@ -104,6 +155,9 @@ class MessageHandler:
else: else:
full_message = message full_message = message
if not HAS_TKINTER:
self.qt_handler.show_warning(title, full_message, self.parent)
else:
if self.parent: if self.parent:
messagebox.showwarning(title, full_message, parent=self.parent) messagebox.showwarning(title, full_message, parent=self.parent)
else: else:
@@ -139,6 +193,9 @@ class MessageHandler:
if details: if details:
full_message += f"\n\n详细信息:\n{details}" full_message += f"\n\n详细信息:\n{details}"
if not HAS_TKINTER:
self.qt_handler.show_error(title, full_message, self.parent)
else:
if self.parent: if self.parent:
messagebox.showerror(title, full_message, parent=self.parent) messagebox.showerror(title, full_message, parent=self.parent)
else: else:
@@ -161,11 +218,13 @@ class MessageHandler:
Returns: Returns:
用户选择True=是False=否) 用户选择True=是False=否)
""" """
if not HAS_TKINTER:
result = self.qt_handler.ask_yes_no(title, message, default, self.parent)
else:
if self.parent: if self.parent:
result = messagebox.askyesno(title, message, parent=self.parent, default=default) result = messagebox.askyesno(title, message, parent=self.parent, default=default)
else: else:
result = messagebox.askyesno(title, message, default=default) result = messagebox.askyesno(title, message, default=default)
logger.info(f"用户选择: {'' if result else ''} ({message})") logger.info(f"用户选择: {'' if result else ''} ({message})")
return result return result
@@ -186,11 +245,13 @@ class MessageHandler:
Returns: Returns:
用户选择True=确定False=取消) 用户选择True=确定False=取消)
""" """
if not HAS_TKINTER:
result = self.qt_handler.ask_ok_cancel(title, message, default, self.parent)
else:
if self.parent: if self.parent:
result = messagebox.askokcancel(title, message, parent=self.parent, default=default) result = messagebox.askokcancel(title, message, parent=self.parent, default=default)
else: else:
result = messagebox.askokcancel(title, message, default=default) result = messagebox.askokcancel(title, message, default=default)
logger.info(f"用户选择: {'确定' if result else '取消'} ({message})") logger.info(f"用户选择: {'确定' if result else '取消'} ({message})")
return result return result
@@ -211,6 +272,50 @@ class MessageHandler:
Returns: Returns:
用户选择True=重试False=取消None=关闭) 用户选择True=重试False=取消None=关闭)
""" """
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:
if self.parent: if self.parent:
result = messagebox.askretrycancel(title, message, parent=self.parent, default=default == "retry") result = messagebox.askretrycancel(title, message, parent=self.parent, default=default == "retry")
else: else:
@@ -226,9 +331,42 @@ class MessageHandler:
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: 窗口标题 title: 窗口标题
errors: 错误列表 errors: 错误列表
""" """
super().__init__(parent) self.parent = parent
self.title(title)
self.geometry("800x600")
self.errors = errors or [] 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() self._load_errors()
def _create_ui(self): def _create_tk_ui(self):
"""创建 UI""" """创建 tkinter UI"""
from tkinter import ttk
import tkinter as tk
# 工具栏 # 工具栏
toolbar = ttk.Frame(self) toolbar = ttk.Frame(self)
toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
ttk.Label(toolbar, text="日志级别:").pack(side=tk.LEFT, padx=5) ttk.Label(toolbar, text="日志级别:").pack(side=tk.LEFT, padx=5)
self.level_var = tk.StringVar(value="ERROR") self.level_var = tk.StringVar(value="ERROR")
level_combo = ttk.Combobox( level_combo = ttk.Combobox(
toolbar, toolbar,
@@ -297,21 +488,85 @@ class ErrorLogViewer(tk.Toplevel):
scrollbar.pack(side=tk.RIGHT, fill=tk.Y) scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.text_widget.config(yscrollcommand=scrollbar.set) self.text_widget.config(yscrollcommand=scrollbar.set)
# 配置标签 def _on_filter_change(self, event=None):
self.text_widget.tag_config("timestamp", foreground="#7f8c8d") """过滤器改变"""
self.text_widget.tag_config("ERROR", foreground="#e74c3c", font=("Consolas", 9, "bold")) self._load_errors()
self.text_widget.tag_config("WARNING", foreground="#f39c12")
self.text_widget.tag_config("INFO", foreground="#3498db")
self.text_widget.tag_config("DEBUG", foreground="#95a5a6")
# 状态栏 def _on_clear(self):
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) self.errors.clear()
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):
"""导出日志"""
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(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:
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):
"""
添加错误
Args:
level: 日志级别
message: 消息
timestamp: 时间戳
"""
if timestamp is None:
timestamp = datetime.now()
self.errors.append({
"level": level,
"message": message,
"timestamp": timestamp
})
self._load_errors()
def _load_errors(self): def _load_errors(self):
"""加载错误""" """加载错误"""
level_filter = self.level_var.get() level_filter = self.level_combo.currentText() if not HAS_TKINTER else self.level_var.get()
if not HAS_TKINTER:
self.text_widget.clear()
import tkinter as tk
else:
self.text_widget.delete("1.0", tk.END) self.text_widget.delete("1.0", tk.END)
count = 0 count = 0
@@ -334,65 +589,18 @@ class ErrorLogViewer(tk.Toplevel):
time_str = str(timestamp) 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"[{time_str}] ", "timestamp")
self.text_widget.insert(tk.END, f"[{level}] ", level) self.text_widget.insert(tk.END, f"[{level}] ", level)
self.text_widget.insert(tk.END, f"{message}\n") self.text_widget.insert(tk.END, f"{message}\n")
else:
self.text_widget.append(f"[{time_str}] [{level}] {message}")
self.status_label.config(text=f"显示 {count} 条日志")
def _on_filter_change(self, event=None): class ProgressDialog:
"""过滤器改变"""
self._load_errors()
def _on_clear(self):
"""清空日志"""
self.errors.clear()
self.text_widget.delete("1.0", tk.END)
self.status_label.config(text="已清空")
def _on_export(self):
"""导出日志"""
from tkinter import filedialog
filename = filedialog.asksaveasfilename(
parent=self,
title="导出日志",
defaultextension=".txt",
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
)
if filename:
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(self.text_widget.get("1.0", tk.END))
messagebox.showinfo("导出成功", f"日志已导出到:\n{filename}")
except Exception as e:
messagebox.showerror("导出失败", f"导出失败:\n{e}")
def add_error(self, level: str, message: str, timestamp: Optional[datetime] = None):
""" """
添加错误 进度对话框(选择 Tkinter 或 PyQt6 实现)
Args:
level: 日志级别
message: 消息
timestamp: 时间戳
"""
if timestamp is None:
timestamp = datetime.now()
self.errors.append({
"level": level,
"message": message,
"timestamp": timestamp
})
self._load_errors()
class ProgressDialog(tk.Toplevel):
"""
进度对话框
显示处理进度和状态 显示处理进度和状态
""" """
@@ -415,29 +623,138 @@ class ProgressDialog(tk.Toplevel):
cancelable: 是否可取消 cancelable: 是否可取消
on_cancel: 取消回调 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.title(title)
self.geometry("400x150") self.geometry("400x150")
self.resizable(False, False) self.resizable(False, False)
# 设置为模态对话框
self.transient(parent) self.transient(parent)
self.grab_set() 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)
layout = QVBoxLayout()
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.on_cancel_callback = on_cancel
self.cancelled = False self.cancelled = False
self._create_ui(message, cancelable) # 启动进度条动画
if HAS_TKINTER:
self._impl.progress_bar.start(10)
else:
from PyQt6.QtCore import QPropertyAnimation
# PyQt6 不需要手动启动动画
# 居中显示 def set_message(self, message: str):
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}") 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.progress_bar = None
def _create_ui(self, message: str, cancelable: bool): def _create_ui(self, message: str, cancelable: bool):
"""创建 UI""" """创建 UI"""
import tkinter as tk
from tkinter import ttk
# 主容器 # 主容器
main_frame = ttk.Frame(self, padding=20) main_frame = ttk.Frame(self, padding=20)
main_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) main_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
@@ -463,39 +780,16 @@ class ProgressDialog(tk.Toplevel):
# 取消按钮 # 取消按钮
if cancelable: if cancelable:
self.cancel_button = ttk.Button( cancel_btn = ttk.Button(main_frame, text="取消", command=self._on_cancel)
main_frame, cancel_btn.pack(side=tk.TOP)
text="取消",
command=self._on_cancel
)
self.cancel_button.pack(side=tk.TOP)
def set_message(self, message: str): def set_message(self, message: str):
"""
设置消息
Args:
message: 消息内容
"""
self.message_label.config(text=message) self.message_label.config(text=message)
def set_detail(self, detail: str): def set_detail(self, detail: str):
"""
设置详细信息
Args:
detail: 详细信息
"""
self.detail_label.config(text=detail) self.detail_label.config(text=detail)
def set_progress(self, value: float, maximum: float = 100): def set_progress(self, value: float, maximum: float = 100):
"""
设置进度值
Args:
value: 当前进度值
maximum: 最大值
"""
self.progress_bar.config(mode='determinate') self.progress_bar.config(mode='determinate')
self.progress_bar.config(maximum=maximum) self.progress_bar.config(maximum=maximum)
self.progress_bar.config(value=value) self.progress_bar.config(value=value)
@@ -505,21 +799,9 @@ class ProgressDialog(tk.Toplevel):
self.cancelled = True self.cancelled = True
if self.on_cancel_callback: if self.on_cancel_callback:
self.on_cancel_callback() self.on_cancel_callback()
self.destroy() # 关闭对话框
import tkinter as tk
def is_cancelled(self) -> bool: self.destroy() # tkinter 的 Toplevel 有 destroy 方法
"""
检查是否已取消
Returns:
是否已取消
"""
return self.cancelled
def close(self):
"""关闭对话框"""
self.progress_bar.stop()
self.destroy()
# 便捷函数 # 便捷函数

View File

@@ -3,35 +3,43 @@
用于展示处理结果,包括: 用于展示处理结果,包括:
- OCR 文本展示 - OCR 文本展示
- AI 处理结果展示(Markdown 格式) - AI 处理结果展示(纯文本格式)
- 一键复制功能 - 一键复制功能
- 日志查看 - 日志查看
""" """
import tkinter as tk from typing import Optional, Callable
from tkinter import ttk, scrolledtext, messagebox
from typing import Optional, Callable, Dict, Any
import logging import logging
# 尝试导入 tkinter失败时使用 PyQt6
try: try:
from tkhtmlview import HTMLLabel import tkinter as tk
HAS_HTMLVIEW = True from tkinter import ttk, scrolledtext, messagebox
HAS_TKINTER = True
except ImportError: 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 from src.core.processor import ProcessResult, create_markdown_result, copy_to_clipboard
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ResultWidget(ttk.Frame): class ResultWidget(QWidget):
""" """
结果展示组件 结果展示组件 (PyQt6 版本)
显示处理结果,支持 Markdown 渲染和一键复制 显示处理结果,支持 Markdown 渲染和一键复制
""" """
# 信号:内容改变
content_changed = pyqtSignal(str)
def __init__( def __init__(
self, self,
parent, parent,
@@ -50,210 +58,99 @@ class ResultWidget(ttk.Frame):
self.copy_callback = copy_callback self.copy_callback = copy_callback
self.current_result: Optional[ProcessResult] = None self.current_result: Optional[ProcessResult] = None
self.display_mode = "raw" # raw 或 markdown
# 标记当前是否显示 Markdown
self._showing_markdown = False
self._create_ui() self._create_ui()
def _create_ui(self): def _create_ui(self):
"""创建 UI""" """创建 UI"""
layout = QVBoxLayout()
# 顶部工具栏 # 顶部工具栏
toolbar = ttk.Frame(self) toolbar_layout = QHBoxLayout()
toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
# 结果类型选择 # 结果类型选择
ttk.Label(toolbar, text="显示:").pack(side=tk.LEFT, padx=5) toolbar_layout.addWidget(QLabel("显示:"))
self.display_mode = tk.StringVar(value="markdown") from PyQt6.QtWidgets import QRadioButton, QButtonGroup
mode_frame = ttk.Frame(toolbar) self.mode_group = QButtonGroup()
mode_frame.pack(side=tk.LEFT, padx=5)
ttk.Radiobutton( raw_btn = QRadioButton("原始文本")
mode_frame, raw_btn.setChecked(True)
text="Markdown", raw_btn.clicked.connect(lambda: self._set_mode("raw"))
variable=self.display_mode, self.mode_group.addButton(raw_btn)
value="markdown", toolbar_layout.addWidget(raw_btn)
command=self._on_display_mode_change
).pack(side=tk.LEFT, padx=2)
ttk.Radiobutton( md_btn = QRadioButton("Markdown")
mode_frame, md_btn.clicked.connect(lambda: self._set_mode("markdown"))
text="原始文本", self.mode_group.addButton(md_btn)
variable=self.display_mode, toolbar_layout.addWidget(md_btn)
value="raw",
command=self._on_display_mode_change toolbar_layout.addStretch()
).pack(side=tk.LEFT, padx=2)
# 右侧按钮 # 右侧按钮
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( self.clear_button = QPushButton("清空")
toolbar, self.clear_button.clicked.connect(self._on_clear)
text="📋 复制", toolbar_layout.addWidget(self.clear_button)
command=self._on_copy
)
self.copy_button.pack(side=tk.LEFT, padx=5)
self.clear_button = ttk.Button( layout.addLayout(toolbar_layout)
toolbar,
text="清空",
command=self._on_clear
)
self.clear_button.pack(side=tk.LEFT, padx=5)
# 主内容区域(使用 Notebook 实现分页) # 主内容区域
self.notebook = ttk.Notebook(self) self.text_widget = QTextEdit()
self.notebook.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) self.text_widget.setReadOnly(True)
self.text_widget.setFont(QFont("Consolas", 10))
layout.addWidget(self.text_widget)
# 结果页面 self.setLayout(layout)
self.result_frame = ttk.Frame(self.notebook)
self.notebook.add(self.result_frame, text="处理结果")
# 日志页面 def _set_mode(self, mode: str):
self.log_frame = ttk.Frame(self.notebook) """设置显示模式"""
self.notebook.add(self.log_frame, text="日志") self.display_mode = mode
# 创建结果内容区域
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("<<ComboboxSelected>>", 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() self._update_result_content()
def _on_copy(self): def _on_copy(self):
"""复制按钮点击""" """复制按钮点击"""
content = self.result_text.get("1.0", tk.END).strip() content = self.text_widget.toPlainText().strip()
if not content: if not content:
if HAS_TKINTER:
from tkinter import messagebox
messagebox.showinfo("提示", "没有可复制的内容") messagebox.showinfo("提示", "没有可复制的内容")
else:
from PyQt6.QtWidgets import QMessageBox
QMessageBox.information(self, "提示", "没有可复制的内容")
return return
success = copy_to_clipboard(content) success = copy_to_clipboard(content)
if success: if success:
self._update_status("已复制到剪贴板") self._update_status("已复制到剪贴板")
if self.copy_callback: if self.copy_callback:
self.copy_callback(content) self.copy_callback(content)
else: else:
messagebox.showerror("错误", "复制失败,请检查是否安装了 pyperclip") self._update_status("复制失败,请检查是否安装了 pyperclip")
def _on_clear(self): def _on_clear(self):
"""清空按钮点击""" """清空按钮点击"""
self.result_text.config(state=tk.NORMAL) self.text_widget.clear()
self.result_text.delete("1.0", tk.END)
self.result_text.config(state=tk.DISABLED)
self.current_result = None self.current_result = None
self._update_status("已清空") 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): def _update_result_content(self):
"""更新结果内容""" """更新结果内容"""
if not self.current_result: if not self.current_result:
self.text_widget.clear()
return return
mode = self.display_mode.get() mode = self.display_mode
if mode == "markdown": if mode == "markdown":
content = self._get_markdown_content() content = self._get_markdown_content()
else: else:
content = self._get_raw_content() content = self._get_raw_content()
self.result_text.config(state=tk.NORMAL) self.text_widget.setPlainText(content)
self.result_text.delete("1.0", tk.END)
self.result_text.insert("1.0", content)
self.result_text.config(state=tk.DISABLED)
def _get_markdown_content(self) -> str: def _get_markdown_content(self) -> str:
"""获取 Markdown 格式内容""" """获取 Markdown 格式内容"""
@@ -301,8 +198,9 @@ class ResultWidget(ttk.Frame):
return "\n".join(parts) return "\n".join(parts)
def _update_status(self, message: str): def _update_status(self, message: str):
"""更新状态""" """更新状态"""
self.status_label.config(text=message) # 这里可以发出信号让父窗口更新状态
pass
def set_result(self, result: ProcessResult): def set_result(self, result: ProcessResult):
""" """
@@ -323,39 +221,16 @@ class ResultWidget(ttk.Frame):
self._update_status(status) self._update_status(status)
def append_log(self, level: str, message: str): def append_log(self, level: str, message: str):
""" """添加日志"""
添加日志 # 简化版本:直接输出到控制台
Args:
level: 日志级别
message: 日志消息
"""
self.log_text.config(state=tk.NORMAL)
# 添加时间戳
from datetime import datetime from datetime import datetime
timestamp = datetime.now().strftime("%H:%M:%S") timestamp = datetime.now().strftime("%H:%M:%S")
print(f"[{timestamp}] [{level}] {message}")
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()
class QuickResultDialog(tk.Toplevel): class QuickResultDialog:
""" """
快速结果显示对话框 快速结果显示对话框 (PyQt6 版本)
用于快速显示处理结果,不集成到主界面 用于快速显示处理结果,不集成到主界面
""" """
@@ -374,32 +249,34 @@ class QuickResultDialog(tk.Toplevel):
result: 处理结果 result: 处理结果
on_close: 关闭回调 on_close: 关闭回调
""" """
super().__init__(parent) from PyQt6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QHBoxLayout, QLabel
super(QDialog, self).__init__(parent)
self.result = result self.result = result
self.on_close = on_close self.on_close = on_close
self.title("处理结果") self.setWindowTitle("处理结果")
self.geometry("600x400") self.resize(600, 400)
# 创建组件 layout = QVBoxLayout()
self.result_widget = ResultWidget(self)
self.result_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
# 显示结果 # 显示结果
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_layout = QHBoxLayout()
button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=10) 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) layout.addLayout(button_layout)
self.setLayout(layout)
# 绑定关闭事件
self.protocol("WM_DELETE_WINDOW", self._on_close)
def _on_close(self): def _on_close(self):
"""关闭对话框""" """关闭对话框"""
if self.on_close: if self.on_close:
self.on_close() self.on_close()
self.destroy() self.accept()

View File

@@ -26,7 +26,7 @@ def setup_path():
setup_path() setup_path()
from gui.main_window import main from src.gui.main_window import main
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -3,7 +3,7 @@
""" """
from src.models.database import ( from src.models.database import (
Base, BaseModel,
Record, Record,
RecordCategory, RecordCategory,
DatabaseManager, DatabaseManager,
@@ -13,7 +13,7 @@ from src.models.database import (
) )
__all__ = [ __all__ = [
'Base', 'BaseModel',
'Record', 'Record',
'RecordCategory', 'RecordCategory',
'DatabaseManager', 'DatabaseManager',