""" 滚动截屏OCR工具 - PySide6 GUI界面 美观现代的界面设计 """ import sys import time from pathlib import Path from typing import Optional from datetime import datetime from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTextEdit, QLabel, QFrame, QProgressBar, QSystemTrayIcon, QMenu, QStyle ) from PySide6.QtCore import Qt, QThread, Signal, QTimer, QSize from PySide6.QtGui import QFont, QIcon, QColor, QPalette, QFontDatabase from loguru import logger # 导入核心逻辑 from main import ScrollCaptureOCR, Config class LogHandler: """日志处理器,将日志输出到GUI""" def __init__(self, signal): self.signal = signal def write(self, message): if message.strip(): self.signal.emit(message.strip()) def flush(self): pass class CaptureWorker(QThread): """后台工作线程,执行截屏OCR任务""" log_signal = Signal(str) progress_signal = Signal(int) status_signal = Signal(str) finished_signal = Signal() error_signal = Signal(str) div_processed_signal = Signal(int, int) # (当前div序号, 总div数) def __init__(self, scroll_capture: ScrollCaptureOCR): super().__init__() self.scroll_capture = scroll_capture self.is_running = False def run(self): """运行截屏任务""" self.is_running = True try: self.status_signal.emit("运行中") # 重置状态 self.scroll_capture.previous_ocr_result = [] self.scroll_capture.scroll_count = 0 self.scroll_capture.all_results = [] self.scroll_capture.processed_divs = [] self.scroll_capture.last_div_signature = None self.scroll_capture.total_scroll_distance = 0 self.scroll_capture.is_first_capture = True # 循环处理 while self.is_running and self.scroll_capture.process_once(): progress = min(self.scroll_capture.scroll_count * 10, 90) self.progress_signal.emit(progress) self.log_signal.emit(f"✓ 第 {self.scroll_capture.scroll_count} 次截屏完成," f"累计滚动 {self.scroll_capture.total_scroll_distance} 像素") # 保存最终结果 if self.scroll_capture.all_results: self.scroll_capture.save_final_result() total_divs = sum(len(result.get('texts', [])) for result in self.scroll_capture.all_results) self.log_signal.emit(f"✓ 共处理 {len(self.scroll_capture.all_results)} 次截屏," f"识别 {total_divs} 个内容区域") self.progress_signal.emit(100) self.status_signal.emit("完成") except Exception as e: self.error_signal.emit(str(e)) self.status_signal.emit("错误") finally: self.is_running = False self.finished_signal.emit() def stop(self): """停止任务""" self.is_running = False self.status_signal.emit("已停止") class ModernButton(QPushButton): """现代风格按钮""" def __init__(self, text, parent=None, primary=True): super().__init__(text, parent) self.primary = primary self.setMinimumHeight(40) self.setFont(QFont("Microsoft YaHei", 11, QFont.Bold if primary else QFont.Normal)) self.setCursor(Qt.PointingHandCursor) self.update_style() def update_style(self): if self.primary: self.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; border: none; border-radius: 8px; padding: 10px 30px; } QPushButton:hover { background-color: #45a049; } QPushButton:pressed { background-color: #3d8b40; } QPushButton:disabled { background-color: #cccccc; color: #666666; } """) else: self.setStyleSheet(""" QPushButton { background-color: #f5f5f5; color: #333333; border: 2px solid #dddddd; border-radius: 8px; padding: 10px 30px; } QPushButton:hover { background-color: #e8e8e8; border-color: #cccccc; } QPushButton:pressed { background-color: #d8d8d8; } """) class LogTextEdit(QTextEdit): """带样式的日志显示框""" def __init__(self, parent=None): super().__init__(parent) self.setReadOnly(True) self.setFont(QFont("Consolas", 10)) self.setStyleSheet(""" QTextEdit { background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e3e; border-radius: 8px; padding: 10px; } """) self.setPlaceholderText("日志信息将显示在这里...") def append_log(self, message: str, level: str = "INFO"): """添加带颜色的日志""" timestamp = datetime.now().strftime("%H:%M:%S") color_map = { "INFO": "#4CAF50", "WARNING": "#FF9800", "ERROR": "#F44336", "DEBUG": "#2196F3" } color = color_map.get(level, "#d4d4d4") html = f'[{timestamp}] {message}' self.append(html) # 自动滚动到底部 scrollbar = self.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) class MainWindow(QMainWindow): """主窗口""" def __init__(self): super().__init__() self.setWindowTitle("滚动截屏OCR工具") self.setMinimumSize(800, 600) self.resize(900, 700) # 初始化核心逻辑 self.scroll_capture = ScrollCaptureOCR() self.worker: Optional[CaptureWorker] = None # 设置应用样式 self.setup_styles() # 创建UI self.setup_ui() # 设置系统托盘 self.setup_tray() # 重定向日志 self.setup_logging() def setup_styles(self): """设置应用样式""" self.setStyleSheet(""" QMainWindow { background-color: #f8f9fa; } QLabel { color: #333333; } QFrame { border: none; } """) def setup_ui(self): """设置UI界面""" # 中央部件 central_widget = QWidget() self.setCentralWidget(central_widget) # 主布局 main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(20) main_layout.setContentsMargins(30, 30, 30, 30) # === 标题区域 === title_layout = QHBoxLayout() title_label = QLabel("滚动截屏OCR工具") title_label.setFont(QFont("Microsoft YaHei", 20, QFont.Bold)) title_label.setStyleSheet("color: #2c3e50;") title_layout.addWidget(title_label) title_layout.addStretch() # 状态标签 self.status_label = QLabel("就绪") self.status_label.setFont(QFont("Microsoft YaHei", 11)) self.status_label.setStyleSheet(""" QLabel { color: #4CAF50; background-color: #e8f5e9; padding: 6px 16px; border-radius: 16px; } """) title_layout.addWidget(self.status_label) main_layout.addLayout(title_layout) # === 信息卡片 === info_card = QFrame() info_card.setStyleSheet(""" QFrame { background-color: white; border-radius: 12px; padding: 15px; } """) info_layout = QHBoxLayout(info_card) info_layout.setSpacing(30) # 使用说明 help_text = QLabel( "使用步骤:
" "1. 点击「开始」按钮
" "2. 按住鼠标左键拖动选择区域
" "3. 程序自动分割div并逐个OCR
" "4. 智能计算滚动距离,自动翻页
" "5. 完成后点击「结束」按钮" ) help_text.setFont(QFont("Microsoft YaHei", 10)) help_text.setStyleSheet("color: #555555; line-height: 1.6;") info_layout.addWidget(help_text) info_layout.addStretch() # 配置信息 config_text = QLabel( f"当前配置:
" f"OCR引擎: {Config.OCR_ENGINE}
" f"灰度阈值: {Config.GRAY_THRESHOLD}
" f"输出目录: {Config.OUTPUT_DIR}" ) config_text.setFont(QFont("Microsoft YaHei", 10)) config_text.setStyleSheet("color: #666666;") config_text.setAlignment(Qt.AlignRight) info_layout.addWidget(config_text) main_layout.addWidget(info_card) # === 进度条 === self.progress_bar = QProgressBar() self.progress_bar.setMaximumHeight(8) self.progress_bar.setTextVisible(False) self.progress_bar.setStyleSheet(""" QProgressBar { background-color: #e0e0e0; border-radius: 4px; } QProgressBar::chunk { background-color: #4CAF50; border-radius: 4px; } """) self.progress_bar.setValue(0) main_layout.addWidget(self.progress_bar) # === 日志区域 === log_label = QLabel("运行日志") log_label.setFont(QFont("Microsoft YaHei", 12, QFont.Bold)) log_label.setStyleSheet("color: #2c3e50; margin-top: 10px;") main_layout.addWidget(log_label) self.log_text = LogTextEdit() main_layout.addWidget(self.log_text) # === 按钮区域 === button_layout = QHBoxLayout() button_layout.setSpacing(15) button_layout.addStretch() # 操作按钮(开始/结束 二合一) self.action_btn = ModernButton("开始", primary=True) self.action_btn.clicked.connect(self.on_action_button_clicked) button_layout.addWidget(self.action_btn) # 清空日志按钮 self.clear_btn = ModernButton("清空日志", primary=False) self.clear_btn.clicked.connect(self.clear_logs) button_layout.addWidget(self.clear_btn) button_layout.addStretch() main_layout.addLayout(button_layout) # === 底部信息 === footer = QLabel("点击「开始」按钮启动 | 输出目录: ./output/") footer.setFont(QFont("Microsoft YaHei", 9)) footer.setStyleSheet("color: #999999; margin-top: 10px;") footer.setAlignment(Qt.AlignCenter) main_layout.addWidget(footer) def setup_tray(self): """设置系统托盘""" self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) # 托盘菜单 tray_menu = QMenu() show_action = tray_menu.addAction("显示窗口") show_action.triggered.connect(self.show) tray_menu.addSeparator() start_action = tray_menu.addAction("开始") start_action.triggered.connect(self.start_capture) stop_action = tray_menu.addAction("结束") stop_action.triggered.connect(self.stop_capture) tray_menu.addSeparator() quit_action = tray_menu.addAction("退出") quit_action.triggered.connect(self.quit_app) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() def setup_logging(self): """设置日志重定向""" # 创建自定义日志处理器 import logging class QtHandler(logging.Handler): def __init__(self, callback): super().__init__() self.callback = callback def emit(self, record): msg = self.format(record) self.callback(msg, record.levelname) # 配置loguru输出到GUI logger.add(self.log_to_gui, format="{message}") def log_to_gui(self, message): """将日志输出到GUI""" # 在主线程中更新UI QTimer.singleShot(0, lambda: self._append_log_safe(message)) def _append_log_safe(self, message: str): """安全地添加日志(在主线程中调用)""" level = "INFO" if "错误" in message or "失败" in message or "✗" in message: level = "ERROR" elif "警告" in message: level = "WARNING" elif "完成" in message or "✓" in message: level = "INFO" self.log_text.append_log(message, level) def on_action_button_clicked(self): """操作按钮点击事件""" if self.action_btn.text() == "开始": self.start_capture() else: self.stop_capture() def start_capture(self): """开始截屏""" # 检查OCR服务 if not self.scroll_capture.ocr_engine.check_service(): self.log_text.append_log("✗ OCR服务未运行", "ERROR") if Config.OCR_ENGINE == "umi": self.log_text.append_log("请先启动Umi-OCR并开启HTTP服务", "WARNING") self.log_text.append_log("设置 → HTTP接口 → 启用HTTP服务", "INFO") return # 选择区域 self.log_text.append_log("请在屏幕上拖动选择截图区域...", "INFO") self.status_label.setText("选择区域") self.status_label.setStyleSheet(""" QLabel { color: #FF9800; background-color: #fff3e0; padding: 6px 16px; border-radius: 16px; } """) try: self.scroll_capture.capture_region = self.scroll_capture.region_selector.select_region() except Exception as e: self.log_text.append_log(f"区域选择失败: {e}", "ERROR") self.status_label.setText("就绪") self.status_label.setStyleSheet(""" QLabel { color: #4CAF50; background-color: #e8f5e9; padding: 6px 16px; border-radius: 16px; } """) return # 启动工作线程 self.worker = CaptureWorker(self.scroll_capture) self.worker.log_signal.connect(lambda msg: self.log_text.append_log(msg, "INFO")) self.worker.progress_signal.connect(self.update_progress) self.worker.status_signal.connect(self.update_status) self.worker.finished_signal.connect(self.on_finished) self.worker.error_signal.connect(self.on_error) self.worker.start() # 更新UI状态 - 按钮变为"结束" self.action_btn.setText("结束") self.action_btn.update_style() self.progress_bar.setValue(0) def stop_capture(self): """停止截屏""" if self.worker and self.worker.isRunning(): self.worker.stop() self.worker.wait(1000) self.log_text.append_log("用户手动结束", "WARNING") # 更新UI状态 - 按钮恢复为"开始" self.action_btn.setText("开始") self.action_btn.update_style() self.status_label.setText("就绪") self.status_label.setStyleSheet(""" QLabel { color: #4CAF50; background-color: #e8f5e9; padding: 6px 16px; border-radius: 16px; } """) def update_progress(self, value: int): """更新进度条""" self.progress_bar.setValue(value) def update_status(self, status: str): """更新状态标签""" self.status_label.setText(status) if status == "运行中": self.status_label.setStyleSheet(""" QLabel { color: #2196F3; background-color: #e3f2fd; padding: 6px 16px; border-radius: 16px; } """) elif status == "完成": self.status_label.setStyleSheet(""" QLabel { color: #4CAF50; background-color: #e8f5e9; padding: 6px 16px; border-radius: 16px; } """) elif status == "错误": self.status_label.setStyleSheet(""" QLabel { color: #F44336; background-color: #ffebee; padding: 6px 16px; border-radius: 16px; } """) def on_finished(self): """任务完成回调""" # 按钮恢复为"开始" self.action_btn.setText("开始") self.action_btn.update_style() self.log_text.append_log("✓ 截屏OCR任务已完成", "INFO") def on_error(self, error_msg: str): """错误回调""" self.log_text.append_log(f"✗ 错误: {error_msg}", "ERROR") # 按钮恢复为"开始" self.action_btn.setText("开始") self.action_btn.update_style() def clear_logs(self): """清空日志""" self.log_text.clear() self.log_text.append_log("日志已清空", "INFO") def quit_app(self): """退出应用""" if self.worker and self.worker.isRunning(): self.worker.stop() self.worker.wait(2000) self.tray_icon.hide() QApplication.quit() def closeEvent(self, event): """关闭事件""" # 最小化到托盘而不是关闭 if self.tray_icon.isVisible(): self.hide() self.tray_icon.showMessage( "滚动截屏OCR工具", "程序已最小化到系统托盘", QSystemTrayIcon.Information, 2000 ) event.ignore() else: event.accept() def main(): """入口函数""" app = QApplication(sys.argv) # 设置应用信息 app.setApplicationName("滚动截屏OCR工具") app.setApplicationVersion("1.0.0") # 设置全局字体 font = QFont("Microsoft YaHei", 10) app.setFont(font) # 创建并显示主窗口 window = MainWindow() window.show() # 显示启动提示 window.log_text.append_log("程序已启动,点击「开始」按钮开始", "INFO") window.log_text.append_log(f"OCR引擎: {Config.OCR_ENGINE}", "INFO") sys.exit(app.exec()) if __name__ == "__main__": main()