Files
long-screen-cut/gui.py
xiaji 41ff658e31 feat(gui): 添加基于PySide6的图形界面
- 实现美观现代的GUI界面,包含开始/停止按钮、日志显示和进度条
- 添加系统托盘支持,关闭窗口时最小化到托盘
- 重定向日志输出到GUI界面,支持彩色日志显示
- 保留原有命令行功能,同时提供更友好的图形操作方式
2026-03-06 16:26:07 +08:00

583 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
滚动截屏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)
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 = []
# 循环处理
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} 次截屏完成")
# 保存最终结果
if self.scroll_capture.all_results:
self.scroll_capture.save_final_result()
self.log_signal.emit(f"✓ 共处理 {len(self.scroll_capture.all_results)} 次截屏")
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'<span style="color: #666666;">[{timestamp}]</span> <span style="color: {color};">{message}</span>'
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(
"<b>使用步骤:</b><br>"
"1. 点击「开始截屏」按钮<br>"
"2. 按住鼠标左键拖动选择区域<br>"
"3. 程序自动滚动截屏并OCR识别<br>"
"4. 检测到重复内容时自动停止"
)
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"<b>当前配置:</b><br>"
f"OCR引擎: {Config.OCR_ENGINE}<br>"
f"灰度阈值: {Config.GRAY_THRESHOLD}<br>"
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.stop_btn = ModernButton("停止", primary=False)
self.stop_btn.setEnabled(False)
self.stop_btn.clicked.connect(self.stop_capture)
button_layout.addWidget(self.stop_btn)
# 开始按钮
self.start_btn = ModernButton("开始截屏", primary=True)
self.start_btn.clicked.connect(self.start_capture)
button_layout.addWidget(self.start_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("按 Ctrl+F9 也可以快速启动 | 输出目录: ./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 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.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
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")
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.status_label.setText("已停止")
self.status_label.setStyleSheet("""
QLabel {
color: #F44336;
background-color: #ffebee;
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.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.log_text.append_log("✓ 截屏OCR任务已完成", "INFO")
def on_error(self, error_msg: str):
"""错误回调"""
self.log_text.append_log(f"✗ 错误: {error_msg}", "ERROR")
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
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()