Compare commits
3 Commits
54eeb6f522
...
bb27db586d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb27db586d | ||
|
|
92a258350a | ||
|
|
a5897a1cd8 |
@@ -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
|
||||
|
||||
|
||||
@@ -584,3 +584,17 @@ class NavigationButton(QPushButton):
|
||||
if icon_path:
|
||||
self.setIcon(QIcon(icon_path))
|
||||
self.setIconSize(QSize(20, 20))
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
应用程序入口
|
||||
|
||||
初始化并显示主窗口
|
||||
"""
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -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,6 +123,9 @@ class MessageHandler:
|
||||
else:
|
||||
full_message = message
|
||||
|
||||
if not HAS_TKINTER:
|
||||
self.qt_handler.show_info(title, full_message, self.parent)
|
||||
else:
|
||||
if self.parent:
|
||||
messagebox.showinfo(title, full_message, parent=self.parent)
|
||||
else:
|
||||
@@ -104,6 +155,9 @@ class MessageHandler:
|
||||
else:
|
||||
full_message = message
|
||||
|
||||
if not HAS_TKINTER:
|
||||
self.qt_handler.show_warning(title, full_message, self.parent)
|
||||
else:
|
||||
if self.parent:
|
||||
messagebox.showwarning(title, full_message, parent=self.parent)
|
||||
else:
|
||||
@@ -139,6 +193,9 @@ class MessageHandler:
|
||||
if 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:
|
||||
messagebox.showerror(title, full_message, parent=self.parent)
|
||||
else:
|
||||
@@ -161,11 +218,13 @@ class MessageHandler:
|
||||
Returns:
|
||||
用户选择(True=是,False=否)
|
||||
"""
|
||||
if not HAS_TKINTER:
|
||||
result = self.qt_handler.ask_yes_no(title, message, default, self.parent)
|
||||
else:
|
||||
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
|
||||
|
||||
@@ -186,11 +245,13 @@ class MessageHandler:
|
||||
Returns:
|
||||
用户选择(True=确定,False=取消)
|
||||
"""
|
||||
if not HAS_TKINTER:
|
||||
result = self.qt_handler.ask_ok_cancel(title, message, default, self.parent)
|
||||
else:
|
||||
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
|
||||
|
||||
@@ -211,6 +272,50 @@ class MessageHandler:
|
||||
Returns:
|
||||
用户选择(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:
|
||||
result = messagebox.askretrycancel(title, message, parent=self.parent, default=default == "retry")
|
||||
else:
|
||||
@@ -226,9 +331,42 @@ class MessageHandler:
|
||||
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,21 +488,85 @@ 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")
|
||||
def _on_filter_change(self, event=None):
|
||||
"""过滤器改变"""
|
||||
self._load_errors()
|
||||
|
||||
# 状态栏
|
||||
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 _on_clear(self):
|
||||
"""清空日志"""
|
||||
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):
|
||||
"""加载错误"""
|
||||
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)
|
||||
|
||||
count = 0
|
||||
@@ -334,65 +589,18 @@ class ErrorLogViewer(tk.Toplevel):
|
||||
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}")
|
||||
|
||||
self.status_label.config(text=f"显示 {count} 条日志")
|
||||
|
||||
def _on_filter_change(self, event=None):
|
||||
"""过滤器改变"""
|
||||
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):
|
||||
class ProgressDialog:
|
||||
"""
|
||||
添加错误
|
||||
|
||||
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):
|
||||
"""
|
||||
进度对话框
|
||||
进度对话框(选择 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)
|
||||
|
||||
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.cancelled = False
|
||||
|
||||
self._create_ui(message, cancelable)
|
||||
# 启动进度条动画
|
||||
if HAS_TKINTER:
|
||||
self._impl.progress_bar.start(10)
|
||||
else:
|
||||
from PyQt6.QtCore import QPropertyAnimation
|
||||
# PyQt6 不需要手动启动动画
|
||||
|
||||
# 居中显示
|
||||
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}")
|
||||
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.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 方法
|
||||
|
||||
|
||||
# 便捷函数
|
||||
|
||||
@@ -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("<<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:
|
||||
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:
|
||||
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()
|
||||
|
||||
@@ -26,7 +26,7 @@ def setup_path():
|
||||
|
||||
setup_path()
|
||||
|
||||
from gui.main_window import main
|
||||
from src.gui.main_window import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user