refactor: 重构为极简截图上传工具
- 简化项目定位:从智能工具转为极简截图上传工具 - 移除重型依赖:torch、transformers、paddleocr、SQLAlchemy - 新增轻量级核心模块: - config.py: 简化 YAML 配置管理 - database.py: 原生 SQLite 存储 - screenshot.py: 截图功能(全屏/区域) - uploader.py: 云端上传(支持 custom/telegraph/imgur) - plugins/ocr.py: 可选 RapidOCR 插件 - 重写主窗口:专注核心功能,移除复杂 UI - 更新依赖:核心 ~50MB,OCR 可选 - 更新文档:新的 README 和需求分析 v2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
"""
|
||||
GUI样式和主题模块
|
||||
|
||||
提供颜色方案和主题样式表
|
||||
"""
|
||||
|
||||
from src.gui.styles.colors import ColorScheme, COLORS, get_color
|
||||
from src.gui.styles.theme import ThemeStyles
|
||||
|
||||
# 浏览视图样式(如果存在)
|
||||
try:
|
||||
from src.gui.styles.browse_style import (
|
||||
get_style, get_category_color, get_category_name,
|
||||
CATEGORY_COLORS, CATEGORY_NAMES
|
||||
)
|
||||
_has_browse_style = True
|
||||
except ImportError:
|
||||
_has_browse_style = False
|
||||
|
||||
__all__ = [
|
||||
# 颜色和主题
|
||||
'ColorScheme',
|
||||
'COLORS',
|
||||
'get_color',
|
||||
'ThemeStyles',
|
||||
]
|
||||
|
||||
# 如果浏览样式存在,添加到导出
|
||||
if _has_browse_style:
|
||||
__all__.extend([
|
||||
'get_style',
|
||||
'get_category_color',
|
||||
'get_category_name',
|
||||
'CATEGORY_COLORS',
|
||||
'CATEGORY_NAMES',
|
||||
])
|
||||
@@ -1,341 +0,0 @@
|
||||
"""
|
||||
浏览视图样式定义
|
||||
|
||||
包含卡片、按钮、对话框等组件的样式
|
||||
"""
|
||||
|
||||
# 通用样式
|
||||
COMMON_STYLES = """
|
||||
QWidget {
|
||||
font-family: "Microsoft YaHei", "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
"""
|
||||
|
||||
# 卡片样式
|
||||
CARD_STYLES = """
|
||||
RecordCard {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #E8E8E8;
|
||||
}
|
||||
RecordCard:hover {
|
||||
background-color: #FAFAFA;
|
||||
border: 1px solid #4A90E2;
|
||||
}
|
||||
"""
|
||||
|
||||
# 按钮样式
|
||||
BUTTON_STYLES = {
|
||||
'primary': """
|
||||
QPushButton {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #357ABD;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #2E6FA8;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #BDC3C7;
|
||||
color: #ECF0F1;
|
||||
}
|
||||
""",
|
||||
'success': """
|
||||
QPushButton {
|
||||
background-color: #58D68D;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #48C9B0;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #45B39D;
|
||||
}
|
||||
""",
|
||||
'danger': """
|
||||
QPushButton {
|
||||
background-color: #EC7063;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #E74C3C;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #C0392B;
|
||||
}
|
||||
""",
|
||||
'secondary': """
|
||||
QPushButton {
|
||||
background-color: #95A5A6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #7F8C8D;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #6C7A7D;
|
||||
}
|
||||
""",
|
||||
}
|
||||
|
||||
# 分类按钮颜色
|
||||
CATEGORY_COLORS = {
|
||||
"TODO": "#5DADE2",
|
||||
"NOTE": "#58D68D",
|
||||
"IDEA": "#F5B041",
|
||||
"REF": "#AF7AC5",
|
||||
"FUNNY": "#EC7063",
|
||||
"TEXT": "#95A5A6",
|
||||
}
|
||||
|
||||
# 分类名称
|
||||
CATEGORY_NAMES = {
|
||||
"TODO": "待办",
|
||||
"NOTE": "笔记",
|
||||
"IDEA": "灵感",
|
||||
"REF": "参考",
|
||||
"FUNNY": "趣味",
|
||||
"TEXT": "文本",
|
||||
}
|
||||
|
||||
# 输入框样式
|
||||
INPUT_STYLES = """
|
||||
QLineEdit {
|
||||
padding: 10px 15px;
|
||||
border: 2px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background-color: #FAFAFA;
|
||||
color: #333;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border: 2px solid #4A90E2;
|
||||
background-color: white;
|
||||
}
|
||||
QLineEdit:disabled {
|
||||
background-color: #ECF0F1;
|
||||
color: #95A5A6;
|
||||
}
|
||||
"""
|
||||
|
||||
# 文本编辑框样式
|
||||
TEXTEDIT_STYLES = """
|
||||
QTextEdit {
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background-color: #FAFAFA;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
QTextEdit:focus {
|
||||
border: 1px solid #4A90E2;
|
||||
background-color: white;
|
||||
}
|
||||
"""
|
||||
|
||||
# 下拉框样式
|
||||
COMBOBOX_STYLES = """
|
||||
QComboBox {
|
||||
padding: 8px 15px;
|
||||
border: 2px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background-color: white;
|
||||
color: #333;
|
||||
}
|
||||
QComboBox:hover {
|
||||
border: 2px solid #4A90E2;
|
||||
}
|
||||
QComboBox::drop-down {
|
||||
border: none;
|
||||
width: 30px;
|
||||
}
|
||||
QComboBox::down-arrow {
|
||||
image: none;
|
||||
border: 5px solid transparent;
|
||||
border-top-color: #333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
selection-background-color: #4A90E2;
|
||||
selection-color: white;
|
||||
padding: 5px;
|
||||
}
|
||||
"""
|
||||
|
||||
# 滚动区域样式
|
||||
SCROLLAREA_STYLES = """
|
||||
QScrollArea {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
QScrollBar:vertical {
|
||||
border: none;
|
||||
background-color: #F5F5F5;
|
||||
width: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background-color: #BDC3C7;
|
||||
border-radius: 6px;
|
||||
min-height: 30px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background-color: #95A5A6;
|
||||
}
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
border: none;
|
||||
background-color: #F5F5F5;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background-color: #BDC3C7;
|
||||
border-radius: 6px;
|
||||
min-width: 30px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background-color: #95A5A6;
|
||||
}
|
||||
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
"""
|
||||
|
||||
# 框架样式
|
||||
FRAME_STYLES = """
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #E0E0E0;
|
||||
}
|
||||
"""
|
||||
|
||||
# 标签样式
|
||||
LABEL_STYLES = {
|
||||
'title': """
|
||||
QLabel {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #2C3E50;
|
||||
}
|
||||
""",
|
||||
'subtitle': """
|
||||
QLabel {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #34495E;
|
||||
}
|
||||
""",
|
||||
'body': """
|
||||
QLabel {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
""",
|
||||
'caption': """
|
||||
QLabel {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
""",
|
||||
}
|
||||
|
||||
# 对话框样式
|
||||
DIALOG_STYLES = """
|
||||
QDialog {
|
||||
background-color: #F5F7FA;
|
||||
}
|
||||
"""
|
||||
|
||||
# 工具栏样式
|
||||
TOOLBAR_STYLES = """
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def get_style(style_type: str, *args) -> str:
|
||||
"""
|
||||
获取样式字符串
|
||||
|
||||
Args:
|
||||
style_type: 样式类型 (button, input, label等)
|
||||
*args: 额外参数 (如button类型: primary, secondary等)
|
||||
|
||||
Returns:
|
||||
样式字符串
|
||||
"""
|
||||
styles = {
|
||||
'button': BUTTON_STYLES.get(args[0] if args else 'primary', ''),
|
||||
'input': INPUT_STYLES,
|
||||
'textedit': TEXTEDIT_STYLES,
|
||||
'combobox': COMBOBOX_STYLES,
|
||||
'scrollarea': SCROLLAREA_STYLES,
|
||||
'frame': FRAME_STYLES,
|
||||
'label': LABEL_STYLES.get(args[0] if args else 'body', ''),
|
||||
'dialog': DIALOG_STYLES,
|
||||
'toolbar': TOOLBAR_STYLES,
|
||||
}
|
||||
|
||||
return styles.get(style_type, '')
|
||||
|
||||
|
||||
def get_category_color(category: str) -> str:
|
||||
"""
|
||||
获取分类颜色
|
||||
|
||||
Args:
|
||||
category: 分类代码
|
||||
|
||||
Returns:
|
||||
颜色代码
|
||||
"""
|
||||
return CATEGORY_COLORS.get(category, "#95A5A6")
|
||||
|
||||
|
||||
def get_category_name(category: str) -> str:
|
||||
"""
|
||||
获取分类中文名
|
||||
|
||||
Args:
|
||||
category: 分类代码
|
||||
|
||||
Returns:
|
||||
中文名
|
||||
"""
|
||||
return CATEGORY_NAMES.get(category, category)
|
||||
@@ -1,122 +0,0 @@
|
||||
"""
|
||||
颜色定义模块
|
||||
|
||||
定义应用程序使用的颜色方案,采用米白色系
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class ColorScheme:
|
||||
"""颜色方案类 - 米白色主题"""
|
||||
|
||||
# 主色调 - 米白色系
|
||||
background_primary: str = "#FAF8F5" # 主背景色 - 米白色
|
||||
background_secondary: str = "#F0ECE8" # 次要背景色 - 浅米色
|
||||
background_card: str = "#FFFFFF" # 卡片背景色 - 纯白
|
||||
|
||||
# 文字颜色
|
||||
text_primary: str = "#2C2C2C" # 主要文字 - 深灰
|
||||
text_secondary: str = "#666666" # 次要文字 - 中灰
|
||||
text_disabled: str = "#999999" # 禁用文字 - 浅灰
|
||||
text_hint: str = "#B8B8B8" # 提示文字 - 更浅灰
|
||||
|
||||
# 强调色 - 温暖的棕色系
|
||||
accent_primary: str = "#8B6914" # 主要强调色 - 金棕
|
||||
accent_secondary: str = "#A67C52" # 次要强调色 - 驼色
|
||||
accent_hover: str = "#D4A574" # 悬停色 - 浅驼
|
||||
|
||||
# 边框和分割线
|
||||
border_light: str = "#E8E4E0" # 浅边框
|
||||
border_medium: str = "#D0CCC6" # 中边框
|
||||
border_dark: str = "#B0ABA5" # 深边框
|
||||
|
||||
# 功能色
|
||||
success: str = "#6B9B3A" # 成功 - 橄榄绿
|
||||
warning: str = "#D9A518" # 警告 - 金黄
|
||||
error: str = "#C94B38" # 错误 - 铁锈红
|
||||
info: str = "#5B8FB9" # 信息 - 钢蓝
|
||||
|
||||
# 阴影
|
||||
shadow_light: str = "rgba(0, 0, 0, 0.05)" # 浅阴影
|
||||
shadow_medium: str = "rgba(0, 0, 0, 0.1)" # 中阴影
|
||||
shadow_dark: str = "rgba(0, 0, 0, 0.15)" # 深阴影
|
||||
|
||||
# 侧边栏
|
||||
sidebar_background: str = "#F5F1EC" # 侧边栏背景
|
||||
sidebar_item_hover: str = "#EBE7E2" # 侧边栏项悬停
|
||||
sidebar_item_active: str = "#E0DCD6" # 侧边栏项激活
|
||||
sidebar_text: str = "#4A4642" # 侧边栏文字
|
||||
|
||||
# 按钮
|
||||
button_primary_bg: str = "#8B6914" # 主按钮背景
|
||||
button_primary_hover: str = "#A67C52" # 主按钮悬停
|
||||
button_primary_text: str = "#FFFFFF" # 主按钮文字
|
||||
|
||||
button_secondary_bg: str = "#E8E4E0" # 次要按钮背景
|
||||
button_secondary_hover: str = "#D0CCC6" # 次要按钮悬停
|
||||
button_secondary_text: str = "#2C2C2C" # 次要按钮文字
|
||||
|
||||
# 输入框
|
||||
input_background: str = "#FFFFFF" # 输入框背景
|
||||
input_border: str = "#D0CCC6" # 输入框边框
|
||||
input_focus_border: str = "#8B6914" # 输入框聚焦边框
|
||||
input_placeholder: str = "#B8B8B8" # 输入框占位符
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'background_primary': self.background_primary,
|
||||
'background_secondary': self.background_secondary,
|
||||
'background_card': self.background_card,
|
||||
'text_primary': self.text_primary,
|
||||
'text_secondary': self.text_secondary,
|
||||
'text_disabled': self.text_disabled,
|
||||
'text_hint': self.text_hint,
|
||||
'accent_primary': self.accent_primary,
|
||||
'accent_secondary': self.accent_secondary,
|
||||
'accent_hover': self.accent_hover,
|
||||
'border_light': self.border_light,
|
||||
'border_medium': self.border_medium,
|
||||
'border_dark': self.border_dark,
|
||||
'success': self.success,
|
||||
'warning': self.warning,
|
||||
'error': self.error,
|
||||
'info': self.info,
|
||||
'shadow_light': self.shadow_light,
|
||||
'shadow_medium': self.shadow_medium,
|
||||
'shadow_dark': self.shadow_dark,
|
||||
'sidebar_background': self.sidebar_background,
|
||||
'sidebar_item_hover': self.sidebar_item_hover,
|
||||
'sidebar_item_active': self.sidebar_item_active,
|
||||
'sidebar_text': self.sidebar_text,
|
||||
'button_primary_bg': self.button_primary_bg,
|
||||
'button_primary_hover': self.button_primary_hover,
|
||||
'button_primary_text': self.button_primary_text,
|
||||
'button_secondary_bg': self.button_secondary_bg,
|
||||
'button_secondary_hover': self.button_secondary_hover,
|
||||
'button_secondary_text': self.button_secondary_text,
|
||||
'input_background': self.input_background,
|
||||
'input_border': self.input_border,
|
||||
'input_focus_border': self.input_focus_border,
|
||||
'input_placeholder': self.input_placeholder,
|
||||
}
|
||||
|
||||
|
||||
# 全局颜色方案实例
|
||||
COLORS = ColorScheme()
|
||||
|
||||
|
||||
def get_color(name: str) -> str:
|
||||
"""
|
||||
获取颜色值
|
||||
|
||||
Args:
|
||||
name: 颜色名称
|
||||
|
||||
Returns:
|
||||
颜色值(十六进制字符串)
|
||||
"""
|
||||
return getattr(COLORS, name, "#000000")
|
||||
@@ -1,437 +0,0 @@
|
||||
"""
|
||||
主题样式表模块
|
||||
|
||||
定义 Qt 样式表(QSS),实现米白色主题
|
||||
"""
|
||||
|
||||
from .colors import COLORS
|
||||
|
||||
|
||||
class ThemeStyles:
|
||||
"""主题样式表类"""
|
||||
|
||||
@staticmethod
|
||||
def get_main_window_stylesheet() -> str:
|
||||
"""
|
||||
获取主窗口样式表
|
||||
|
||||
Returns:
|
||||
QSS 样式表字符串
|
||||
"""
|
||||
return f"""
|
||||
/* ========== 主窗口 ========== */
|
||||
QMainWindow {{
|
||||
background-color: {COLORS.background_primary};
|
||||
}}
|
||||
|
||||
/* ========== 侧边栏 ========== */
|
||||
QWidget#sidebar {{
|
||||
background-color: {COLORS.sidebar_background};
|
||||
border-right: 1px solid {COLORS.border_light};
|
||||
}}
|
||||
|
||||
QPushButton#navButton {{
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
color: {COLORS.sidebar_text};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
|
||||
QPushButton#navButton:hover {{
|
||||
background-color: {COLORS.sidebar_item_hover};
|
||||
}}
|
||||
|
||||
QPushButton#navButton:checked {{
|
||||
background-color: {COLORS.sidebar_item_active};
|
||||
color: {COLORS.accent_primary};
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QWidget#navSeparator {{
|
||||
background-color: {COLORS.border_light};
|
||||
max-height: 1px;
|
||||
min-height: 1px;
|
||||
margin: 8px 16px;
|
||||
}}
|
||||
|
||||
/* ========== 主内容区域 ========== */
|
||||
QWidget#contentArea {{
|
||||
background-color: {COLORS.background_primary};
|
||||
}}
|
||||
|
||||
QStackedWidget#contentStack {{
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}}
|
||||
|
||||
/* ========== 标题 ========== */
|
||||
QLabel#pageTitle {{
|
||||
color: {COLORS.text_primary};
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
padding: 8px 0;
|
||||
}}
|
||||
|
||||
QLabel#sectionTitle {{
|
||||
color: {COLORS.text_primary};
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
padding: 4px 0;
|
||||
}}
|
||||
|
||||
/* ========== 卡片 ========== */
|
||||
QWidget#card {{
|
||||
background-color: {COLORS.background_card};
|
||||
border-radius: 12px;
|
||||
border: 1px solid {COLORS.border_light};
|
||||
padding: 16px;
|
||||
}}
|
||||
|
||||
/* ========== 按钮 ========== */
|
||||
QPushButton {{
|
||||
background-color: {COLORS.button_secondary_bg};
|
||||
color: {COLORS.button_secondary_text};
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: {COLORS.button_secondary_hover};
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: {COLORS.border_medium};
|
||||
}}
|
||||
|
||||
QPushButton:disabled {{
|
||||
background-color: {COLORS.background_secondary};
|
||||
color: {COLORS.text_disabled};
|
||||
}}
|
||||
|
||||
QPushButton#primaryButton {{
|
||||
background-color: {COLORS.button_primary_bg};
|
||||
color: {COLORS.button_primary_text};
|
||||
}}
|
||||
|
||||
QPushButton#primaryButton:hover {{
|
||||
background-color: {COLORS.button_primary_hover};
|
||||
}}
|
||||
|
||||
/* ========== 输入框 ========== */
|
||||
QLineEdit, QTextEdit, QPlainTextEdit {{
|
||||
background-color: {COLORS.input_background};
|
||||
border: 1px solid {COLORS.input_border};
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
color: {COLORS.text_primary};
|
||||
font-size: 14px;
|
||||
selection-background-color: {COLORS.accent_secondary};
|
||||
}}
|
||||
|
||||
QLineEdit:hover, QTextEdit:hover, QPlainTextEdit:hover {{
|
||||
border-color: {COLORS.border_dark};
|
||||
}}
|
||||
|
||||
QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {{
|
||||
border: 2px solid {COLORS.input_focus_border};
|
||||
padding: 7px 11px;
|
||||
}}
|
||||
|
||||
QLineEdit:disabled, QTextEdit:disabled, QPlainTextEdit:disabled {{
|
||||
background-color: {COLORS.background_secondary};
|
||||
color: {COLORS.text_disabled};
|
||||
}}
|
||||
|
||||
/* ========== 下拉框 ========== */
|
||||
QComboBox {{
|
||||
background-color: {COLORS.input_background};
|
||||
border: 1px solid {COLORS.input_border};
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
color: {COLORS.text_primary};
|
||||
font-size: 14px;
|
||||
}}
|
||||
|
||||
QComboBox:hover {{
|
||||
border-color: {COLORS.border_dark};
|
||||
}}
|
||||
|
||||
QComboBox:focus {{
|
||||
border: 2px solid {COLORS.input_focus_border};
|
||||
}}
|
||||
|
||||
QComboBox::drop-down {{
|
||||
border: none;
|
||||
width: 20px;
|
||||
}}
|
||||
|
||||
QComboBox::down-arrow {{
|
||||
image: none;
|
||||
border: 5px solid transparent;
|
||||
border-top-color: {COLORS.text_secondary};
|
||||
margin-right: 5px;
|
||||
}}
|
||||
|
||||
QComboBox QAbstractItemView {{
|
||||
background-color: {COLORS.background_card};
|
||||
border: 1px solid {COLORS.border_light};
|
||||
selection-background-color: {COLORS.sidebar_item_active};
|
||||
selection-color: {COLORS.text_primary};
|
||||
padding: 4px;
|
||||
}}
|
||||
|
||||
/* ========== 滚动条 ========== */
|
||||
QScrollBar:vertical {{
|
||||
background-color: transparent;
|
||||
width: 10px;
|
||||
margin: 0px;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:vertical {{
|
||||
background-color: {COLORS.border_medium};
|
||||
border-radius: 5px;
|
||||
min-height: 30px;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:vertical:hover {{
|
||||
background-color: {COLORS.border_dark};
|
||||
}}
|
||||
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
|
||||
height: 0px;
|
||||
}}
|
||||
|
||||
QScrollBar:horizontal {{
|
||||
background-color: transparent;
|
||||
height: 10px;
|
||||
margin: 0px;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:horizontal {{
|
||||
background-color: {COLORS.border_medium};
|
||||
border-radius: 5px;
|
||||
min-width: 30px;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:horizontal:hover {{
|
||||
background-color: {COLORS.border_dark};
|
||||
}}
|
||||
|
||||
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
|
||||
width: 0px;
|
||||
}}
|
||||
|
||||
/* ========== 分组框 ========== */
|
||||
QGroupBox {{
|
||||
background-color: {COLORS.background_card};
|
||||
border: 1px solid {COLORS.border_light};
|
||||
border-radius: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: {COLORS.text_primary};
|
||||
}}
|
||||
|
||||
QGroupBox::title {{
|
||||
subcontrol-origin: margin;
|
||||
left: 16px;
|
||||
padding: 0 8px;
|
||||
}}
|
||||
|
||||
/* ========== 标签页 ========== */
|
||||
QTabWidget::pane {{
|
||||
background-color: {COLORS.background_card};
|
||||
border: 1px solid {COLORS.border_light};
|
||||
border-radius: 8px;
|
||||
top: -1px;
|
||||
}}
|
||||
|
||||
QTabBar::tab {{
|
||||
background-color: {COLORS.background_secondary};
|
||||
color: {COLORS.text_secondary};
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
margin-right: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
|
||||
QTabBar::tab:selected {{
|
||||
background-color: {COLORS.background_card};
|
||||
color: {COLORS.text_primary};
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QTabBar::tab:hover:!selected {{
|
||||
background-color: {COLORS.sidebar_item_hover};
|
||||
}}
|
||||
|
||||
/* ========== 复选框 ========== */
|
||||
QCheckBox {{
|
||||
color: {COLORS.text_primary};
|
||||
font-size: 14px;
|
||||
spacing: 8px;
|
||||
}}
|
||||
|
||||
QCheckBox::indicator {{
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid {COLORS.input_border};
|
||||
border-radius: 4px;
|
||||
background-color: {COLORS.input_background};
|
||||
}}
|
||||
|
||||
QCheckBox::indicator:hover {{
|
||||
border-color: {COLORS.border_dark};
|
||||
}}
|
||||
|
||||
QCheckBox::indicator:checked {{
|
||||
background-color: {COLORS.accent_primary};
|
||||
border-color: {COLORS.accent_primary};
|
||||
image: none;
|
||||
}}
|
||||
|
||||
QCheckBox::indicator:checked::after {{
|
||||
content: "✓";
|
||||
color: {COLORS.button_primary_text};
|
||||
}}
|
||||
|
||||
/* ========== 单选框 ========== */
|
||||
QRadioButton {{
|
||||
color: {COLORS.text_primary};
|
||||
font-size: 14px;
|
||||
spacing: 8px;
|
||||
}}
|
||||
|
||||
QRadioButton::indicator {{
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid {COLORS.input_border};
|
||||
border-radius: 9px;
|
||||
background-color: {COLORS.input_background};
|
||||
}}
|
||||
|
||||
QRadioButton::indicator:hover {{
|
||||
border-color: {COLORS.border_dark};
|
||||
}}
|
||||
|
||||
QRadioButton::indicator:checked {{
|
||||
background-color: {COLORS.input_background};
|
||||
border-color: {COLORS.accent_primary};
|
||||
}}
|
||||
|
||||
QRadioButton::indicator:checked::after {{
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: {COLORS.accent_primary};
|
||||
}}
|
||||
|
||||
/* ========== 进度条 ========== */
|
||||
QProgressBar {{
|
||||
background-color: {COLORS.background_secondary};
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
height: 8px;
|
||||
text-align: center;
|
||||
color: {COLORS.text_primary};
|
||||
font-size: 12px;
|
||||
}}
|
||||
|
||||
QProgressBar::chunk {{
|
||||
background-color: {COLORS.accent_primary};
|
||||
border-radius: 6px;
|
||||
}}
|
||||
|
||||
/* ========== 分隔线 ========== */
|
||||
QFrame[frameShape="4"], QFrame[frameShape="5"] {{
|
||||
color: {COLORS.border_light};
|
||||
}}
|
||||
|
||||
/* ========== 工具提示 ========== */
|
||||
QToolTip {{
|
||||
background-color: {COLORS.text_primary};
|
||||
color: {COLORS.background_primary};
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}}
|
||||
|
||||
/* ========== 菜单 ========== */
|
||||
QMenu {{
|
||||
background-color: {COLORS.background_card};
|
||||
border: 1px solid {COLORS.border_light};
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
}}
|
||||
|
||||
QMenu::item {{
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
color: {COLORS.text_primary};
|
||||
font-size: 14px;
|
||||
}}
|
||||
|
||||
QMenu::item:selected {{
|
||||
background-color: {COLORS.sidebar_item_active};
|
||||
color: {COLORS.accent_primary};
|
||||
}}
|
||||
|
||||
QMenu::separator {{
|
||||
height: 1px;
|
||||
background-color: {COLORS.border_light};
|
||||
margin: 4px 8px;
|
||||
}}
|
||||
|
||||
/* ========== 列表 ========== */
|
||||
QListWidget {{
|
||||
background-color: {COLORS.background_card};
|
||||
border: 1px solid {COLORS.border_light};
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
}}
|
||||
|
||||
QListWidget::item {{
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
color: {COLORS.text_primary};
|
||||
font-size: 14px;
|
||||
}}
|
||||
|
||||
QListWidget::item:hover {{
|
||||
background-color: {COLORS.sidebar_item_hover};
|
||||
}}
|
||||
|
||||
QListWidget::item:selected {{
|
||||
background-color: {COLORS.sidebar_item_active};
|
||||
color: {COLORS.accent_primary};
|
||||
}}
|
||||
|
||||
/* ========== 状态栏 ========== */
|
||||
QStatusBar {{
|
||||
background-color: {COLORS.background_secondary};
|
||||
color: {COLORS.text_secondary};
|
||||
border-top: 1px solid {COLORS.border_light};
|
||||
font-size: 12px;
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def apply_style(widget) -> None:
|
||||
"""
|
||||
应用样式到部件
|
||||
|
||||
Args:
|
||||
widget: Qt 部件
|
||||
"""
|
||||
widget.setStyleSheet(ThemeStyles.get_main_window_stylesheet())
|
||||
@@ -1,86 +0,0 @@
|
||||
"""
|
||||
自定义GUI组件
|
||||
"""
|
||||
|
||||
# 浏览相关组件
|
||||
from src.gui.widgets.record_card import RecordCard
|
||||
from src.gui.widgets.record_detail_dialog import RecordDetailDialog
|
||||
from src.gui.widgets.browse_view import BrowseView
|
||||
|
||||
# 图片处理组件
|
||||
from src.gui.widgets.screenshot_widget import (
|
||||
ScreenshotWidget,
|
||||
ScreenshotOverlay,
|
||||
QuickScreenshotHelper,
|
||||
take_screenshot
|
||||
)
|
||||
from src.gui.widgets.clipboard_monitor import (
|
||||
ClipboardMonitor,
|
||||
ClipboardImagePicker
|
||||
)
|
||||
from src.gui.widgets.image_picker import (
|
||||
ImagePicker,
|
||||
DropArea,
|
||||
QuickImagePicker
|
||||
)
|
||||
from src.gui.widgets.image_preview_widget import (
|
||||
ImagePreviewWidget,
|
||||
ZoomMode,
|
||||
ImageLabel
|
||||
)
|
||||
|
||||
# 结果展示和消息处理组件
|
||||
from src.gui.widgets.result_widget import ResultWidget, QuickResultDialog
|
||||
from src.gui.widgets.message_handler import (
|
||||
MessageHandler,
|
||||
ErrorLogViewer,
|
||||
ProgressDialog,
|
||||
LogLevel,
|
||||
show_info,
|
||||
show_warning,
|
||||
show_error,
|
||||
ask_yes_no,
|
||||
ask_ok_cancel
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# 浏览相关
|
||||
'RecordCard',
|
||||
'RecordDetailDialog',
|
||||
'BrowseView',
|
||||
|
||||
# 截图相关
|
||||
'ScreenshotWidget',
|
||||
'ScreenshotOverlay',
|
||||
'QuickScreenshotHelper',
|
||||
'take_screenshot',
|
||||
|
||||
# 剪贴板相关
|
||||
'ClipboardMonitor',
|
||||
'ClipboardImagePicker',
|
||||
|
||||
# 图片选择相关
|
||||
'ImagePicker',
|
||||
'DropArea',
|
||||
'QuickImagePicker',
|
||||
|
||||
# 图片预览相关
|
||||
'ImagePreviewWidget',
|
||||
'ZoomMode',
|
||||
'ImageLabel',
|
||||
|
||||
# 结果展示相关
|
||||
'ResultWidget',
|
||||
'QuickResultDialog',
|
||||
|
||||
# 消息处理相关
|
||||
'MessageHandler',
|
||||
'ErrorLogViewer',
|
||||
'ProgressDialog',
|
||||
'LogLevel',
|
||||
'show_info',
|
||||
'show_warning',
|
||||
'show_error',
|
||||
'ask_yes_no',
|
||||
'ask_ok_cancel',
|
||||
]
|
||||
@@ -1,478 +0,0 @@
|
||||
"""
|
||||
浏览视图组件
|
||||
|
||||
实现分类浏览功能,包括:
|
||||
- 全部记录列表视图
|
||||
- 按分类筛选
|
||||
- 卡片样式展示
|
||||
- 记录详情查看
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QScrollArea, QLabel, QLineEdit, QFrame, QSizePolicy,
|
||||
QMessageBox, QInputDialog
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
|
||||
from PyQt6.QtGui import QFont
|
||||
|
||||
from src.models.database import Record, RecordCategory, get_db
|
||||
from src.gui.widgets.record_card import RecordCard
|
||||
from src.gui.widgets.record_detail_dialog import RecordDetailDialog
|
||||
|
||||
|
||||
class BrowseView(QWidget):
|
||||
"""
|
||||
浏览视图组件
|
||||
|
||||
显示所有记录的卡片列表,支持分类筛选
|
||||
"""
|
||||
|
||||
# 定义信号:记录被修改时发出
|
||||
record_modified = pyqtSignal(int) # 记录ID
|
||||
record_deleted = pyqtSignal(int) # 记录ID
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
"""
|
||||
初始化浏览视图
|
||||
|
||||
Args:
|
||||
parent: 父组件
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.current_category = "ALL" # 当前筛选分类,ALL表示全部
|
||||
self.search_text = "" # 搜索文本
|
||||
self.records: List[Record] = [] # 当前显示的记录列表
|
||||
self.card_widgets: List[RecordCard] = [] # 卡片组件列表
|
||||
|
||||
self.setup_ui()
|
||||
self.load_records()
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI布局"""
|
||||
# 主布局
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(20, 20, 20, 20)
|
||||
main_layout.setSpacing(15)
|
||||
|
||||
# 1. 顶部工具栏
|
||||
toolbar = self.create_toolbar()
|
||||
main_layout.addWidget(toolbar)
|
||||
|
||||
# 2. 分类筛选栏
|
||||
category_bar = self.create_category_bar()
|
||||
main_layout.addWidget(category_bar)
|
||||
|
||||
# 3. 记录列表(卡片网格)
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.scroll_area.setStyleSheet("""
|
||||
QScrollArea {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
|
||||
# 卡片容器
|
||||
self.cards_container = QWidget()
|
||||
self.cards_layout = QVBoxLayout(self.cards_container)
|
||||
self.cards_layout.setContentsMargins(10, 10, 10, 10)
|
||||
self.cards_layout.setSpacing(15)
|
||||
self.cards_layout.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.scroll_area.setWidget(self.cards_container)
|
||||
main_layout.addWidget(self.scroll_area)
|
||||
|
||||
# 设置样式
|
||||
self.setStyleSheet("""
|
||||
BrowseView {
|
||||
background-color: #F5F7FA;
|
||||
}
|
||||
""")
|
||||
|
||||
def create_toolbar(self) -> QFrame:
|
||||
"""创建工具栏"""
|
||||
frame = QFrame()
|
||||
frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QHBoxLayout(frame)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# 标题
|
||||
title_label = QLabel("浏览记录")
|
||||
title_label.setStyleSheet("""
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #2C3E50;
|
||||
""")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
# 搜索框
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.setPlaceholderText("搜索记录...")
|
||||
self.search_input.setMinimumWidth(250)
|
||||
self.search_input.setMaximumWidth(400)
|
||||
self.search_input.setStyleSheet("""
|
||||
QLineEdit {
|
||||
padding: 10px 15px;
|
||||
border: 2px solid #E0E0E0;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border: 2px solid #4A90E2;
|
||||
background-color: white;
|
||||
}
|
||||
""")
|
||||
self.search_input.textChanged.connect(self.on_search_text_changed)
|
||||
layout.addWidget(self.search_input)
|
||||
|
||||
# 刷新按钮
|
||||
refresh_btn = QPushButton("刷新")
|
||||
refresh_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #357ABD;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #2E6FA8;
|
||||
}
|
||||
""")
|
||||
refresh_btn.clicked.connect(self.load_records)
|
||||
layout.addWidget(refresh_btn)
|
||||
|
||||
return frame
|
||||
|
||||
def create_category_bar(self) -> QFrame:
|
||||
"""创建分类筛选栏"""
|
||||
frame = QFrame()
|
||||
frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QHBoxLayout(frame)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# 全部
|
||||
self.all_btn = self.create_category_button("全部", "ALL", checked=True)
|
||||
self.all_btn.clicked.connect(lambda: self.filter_by_category("ALL"))
|
||||
layout.addWidget(self.all_btn)
|
||||
|
||||
# 各个分类
|
||||
categories = [
|
||||
("待办", "TODO", "#5DADE2"),
|
||||
("笔记", "NOTE", "#58D68D"),
|
||||
("灵感", "IDEA", "#F5B041"),
|
||||
("参考", "REF", "#AF7AC5"),
|
||||
("趣味", "FUNNY", "#EC7063"),
|
||||
("文本", "TEXT", "#95A5A6"),
|
||||
]
|
||||
|
||||
self.category_buttons = {}
|
||||
for name, code, color in categories:
|
||||
btn = self.create_category_button(name, code, color)
|
||||
btn.clicked.connect(lambda checked, c=code: self.filter_by_category(c))
|
||||
layout.addWidget(btn)
|
||||
self.category_buttons[code] = btn
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
# 统计标签
|
||||
self.stats_label = QLabel()
|
||||
self.stats_label.setStyleSheet("""
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
""")
|
||||
layout.addWidget(self.stats_label)
|
||||
|
||||
return frame
|
||||
|
||||
def create_category_button(self, text: str, category_code: str,
|
||||
color: str = None, checked: bool = False) -> QPushButton:
|
||||
"""
|
||||
创建分类按钮
|
||||
|
||||
Args:
|
||||
text: 按钮文本
|
||||
category_code: 分类代码
|
||||
color: 分类颜色
|
||||
checked: 是否选中
|
||||
|
||||
Returns:
|
||||
QPushButton对象
|
||||
"""
|
||||
btn = QPushButton(text)
|
||||
btn.setCheckable(True)
|
||||
btn.setChecked(checked)
|
||||
|
||||
# 设置按钮样式
|
||||
if color:
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: {color};
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 25px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
opacity: 0.8;
|
||||
}}
|
||||
QPushButton:checked {{
|
||||
border: 3px solid #2C3E50;
|
||||
}}
|
||||
""")
|
||||
else:
|
||||
btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #34495E;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 25px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
QPushButton:checked {
|
||||
border: 3px solid #4A90E2;
|
||||
}
|
||||
""")
|
||||
|
||||
return btn
|
||||
|
||||
def load_records(self):
|
||||
"""从数据库加载记录"""
|
||||
try:
|
||||
session = get_db()
|
||||
|
||||
# 查询记录
|
||||
query = session.query(Record).order_by(Record.created_at.desc())
|
||||
|
||||
# 应用分类筛选
|
||||
if self.current_category != "ALL":
|
||||
query = query.filter(Record.category == self.current_category)
|
||||
|
||||
# 应用搜索筛选
|
||||
if self.search_text:
|
||||
search_pattern = f"%{self.search_text}%"
|
||||
query = query.filter(
|
||||
(Record.ocr_text.like(search_pattern)) |
|
||||
(Record.ai_result.like(search_pattern)) |
|
||||
(Record.notes.like(search_pattern))
|
||||
)
|
||||
|
||||
self.records = query.all()
|
||||
|
||||
# 更新统计
|
||||
self.update_stats()
|
||||
|
||||
# 渲染卡片
|
||||
self.render_cards()
|
||||
|
||||
session.close()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"加载记录失败: {str(e)}")
|
||||
|
||||
def render_cards(self):
|
||||
"""渲染记录卡片"""
|
||||
# 清空现有卡片
|
||||
for card in self.card_widgets:
|
||||
card.deleteLater()
|
||||
self.card_widgets.clear()
|
||||
|
||||
# 如果没有记录
|
||||
if not self.records:
|
||||
empty_label = QLabel("没有找到记录")
|
||||
empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
empty_label.setStyleSheet("""
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
padding: 50px;
|
||||
""")
|
||||
self.cards_layout.addWidget(empty_label)
|
||||
return
|
||||
|
||||
# 创建卡片网格布局
|
||||
from PyQt6.QtWidgets import QGridLayout
|
||||
grid_widget = QWidget()
|
||||
grid_layout = QGridLayout(grid_widget)
|
||||
grid_layout.setSpacing(15)
|
||||
|
||||
# 计算列数(每行最多4个)
|
||||
columns = 4
|
||||
row, col = 0, 0
|
||||
|
||||
for record in self.records:
|
||||
card = RecordCard(
|
||||
record_id=record.id,
|
||||
image_path=record.image_path,
|
||||
ocr_text=record.ocr_text or "",
|
||||
category=record.category,
|
||||
created_at=record.created_at
|
||||
)
|
||||
card.clicked.connect(self.open_record_detail)
|
||||
|
||||
self.card_widgets.append(card)
|
||||
grid_layout.addWidget(card, row, col)
|
||||
|
||||
col += 1
|
||||
if col >= columns:
|
||||
col = 0
|
||||
row += 1
|
||||
|
||||
# 清空原有布局并添加新的网格
|
||||
while self.cards_layout.count():
|
||||
item = self.cards_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
self.cards_layout.addWidget(grid_widget)
|
||||
|
||||
def update_stats(self):
|
||||
"""更新统计信息"""
|
||||
total_count = len(self.records)
|
||||
category_name = self.current_category if self.current_category != "ALL" else "全部"
|
||||
|
||||
if self.current_category == "ALL":
|
||||
self.stats_label.setText(f"共 {total_count} 条记录")
|
||||
else:
|
||||
category_names = {
|
||||
"TODO": "待办",
|
||||
"NOTE": "笔记",
|
||||
"IDEA": "灵感",
|
||||
"REF": "参考",
|
||||
"FUNNY": "趣味",
|
||||
"TEXT": "文本",
|
||||
}
|
||||
cn_name = category_names.get(self.current_category, self.current_category)
|
||||
self.stats_label.setText(f"{cn_name}: {total_count} 条")
|
||||
|
||||
def filter_by_category(self, category: str):
|
||||
"""
|
||||
按分类筛选
|
||||
|
||||
Args:
|
||||
category: 分类代码,"ALL"表示全部
|
||||
"""
|
||||
self.current_category = category
|
||||
|
||||
# 更新按钮状态
|
||||
if category == "ALL":
|
||||
self.all_btn.setChecked(True)
|
||||
for btn in self.category_buttons.values():
|
||||
btn.setChecked(False)
|
||||
else:
|
||||
self.all_btn.setChecked(False)
|
||||
for code, btn in self.category_buttons.items():
|
||||
btn.setChecked(code == category)
|
||||
|
||||
# 重新加载记录
|
||||
self.load_records()
|
||||
|
||||
def on_search_text_changed(self, text: str):
|
||||
"""
|
||||
搜索文本改变
|
||||
|
||||
Args:
|
||||
text: 搜索文本
|
||||
"""
|
||||
self.search_text = text
|
||||
|
||||
# 使用定时器延迟搜索(避免频繁查询)
|
||||
if hasattr(self, '_search_timer'):
|
||||
self._search_timer.stop()
|
||||
|
||||
self._search_timer = QTimer()
|
||||
self._search_timer.setSingleShot(True)
|
||||
self._search_timer.timeout.connect(self.load_records)
|
||||
self._search_timer.start(300) # 300ms延迟
|
||||
|
||||
def open_record_detail(self, record_id: int):
|
||||
"""
|
||||
打开记录详情
|
||||
|
||||
Args:
|
||||
record_id: 记录ID
|
||||
"""
|
||||
try:
|
||||
session = get_db()
|
||||
record = session.query(Record).filter(Record.id == record_id).first()
|
||||
|
||||
if not record:
|
||||
QMessageBox.warning(self, "警告", "记录不存在")
|
||||
session.close()
|
||||
return
|
||||
|
||||
# 创建详情对话框
|
||||
dialog = RecordDetailDialog(
|
||||
record_id=record.id,
|
||||
image_path=record.image_path,
|
||||
ocr_text=record.ocr_text or "",
|
||||
category=record.category,
|
||||
ai_result=record.ai_result,
|
||||
tags=record.tags,
|
||||
notes=record.notes,
|
||||
created_at=record.created_at,
|
||||
updated_at=record.updated_at,
|
||||
parent=self
|
||||
)
|
||||
|
||||
# 显示对话框
|
||||
result = dialog.exec()
|
||||
|
||||
if result == QDialog.DialogCode.Accepted:
|
||||
# 获取修改后的数据
|
||||
data = dialog.get_data()
|
||||
|
||||
if data.get('modified'):
|
||||
# 保存修改到数据库
|
||||
record.category = data['category']
|
||||
record.notes = data['notes']
|
||||
session.commit()
|
||||
|
||||
# 发出信号
|
||||
self.record_modified.emit(record_id)
|
||||
|
||||
# 刷新列表
|
||||
self.load_records()
|
||||
|
||||
session.close()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"打开详情失败: {str(e)}")
|
||||
|
||||
def refresh(self):
|
||||
"""刷新记录列表"""
|
||||
self.load_records()
|
||||
@@ -1,381 +0,0 @@
|
||||
"""
|
||||
剪贴板监听组件
|
||||
|
||||
实现剪贴板变化监听,自动检测图片内容:
|
||||
- 监听剪贴板变化
|
||||
- 自动检测图片内容
|
||||
- 发出图片检测信号
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import QApplication, QWidget
|
||||
from PyQt6.QtCore import QObject, pyqtSignal, QTimer
|
||||
from PyQt6.QtGui import QClipboard, QPixmap, QImage
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import tempfile
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ClipboardMonitor(QObject):
|
||||
"""
|
||||
剪贴板监听器
|
||||
|
||||
监听系统剪贴板的变化,自动检测图片内容
|
||||
"""
|
||||
|
||||
# 信号:检测到图片时发出,传递图片路径
|
||||
image_detected = pyqtSignal(str)
|
||||
# 信号:剪贴板内容变化时发出,传递是否有图片
|
||||
clipboard_changed = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, parent: Optional[QObject] = None):
|
||||
"""
|
||||
初始化剪贴板监听器
|
||||
|
||||
Args:
|
||||
parent: 父对象
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
# 获取剪贴板
|
||||
self.clipboard = QApplication.clipboard()
|
||||
|
||||
# 监听剪贴板变化
|
||||
self.clipboard.dataChanged.connect(self._on_clipboard_changed)
|
||||
|
||||
# 记录上次的图片数据,避免重复触发
|
||||
self.last_image_data = None
|
||||
|
||||
# 临时保存目录
|
||||
self.temp_dir = Path(tempfile.gettempdir()) / "cutthenthink" / "clipboard"
|
||||
self.temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 监听开关
|
||||
self._enabled = True
|
||||
|
||||
def _on_clipboard_changed(self):
|
||||
"""剪贴板内容变化处理"""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
# 检查剪贴板是否有图片
|
||||
pixmap = self.clipboard.pixmap()
|
||||
|
||||
if not pixmap.isNull():
|
||||
# 有图片
|
||||
# 检查是否是新的图片(避免重复触发)
|
||||
image_data = self._get_image_data(pixmap)
|
||||
|
||||
if image_data != self.last_image_data:
|
||||
self.last_image_data = image_data
|
||||
|
||||
# 保存图片
|
||||
filepath = self._save_clipboard_image(pixmap)
|
||||
|
||||
if filepath:
|
||||
self.image_detected.emit(filepath)
|
||||
|
||||
self.clipboard_changed.emit(True)
|
||||
else:
|
||||
# 无图片
|
||||
self.last_image_data = None
|
||||
self.clipboard_changed.emit(False)
|
||||
|
||||
def _get_image_data(self, pixmap: QPixmap) -> bytes:
|
||||
"""
|
||||
获取图片数据(用于比较)
|
||||
|
||||
Args:
|
||||
pixmap: 图片对象
|
||||
|
||||
Returns:
|
||||
图片的字节数据
|
||||
"""
|
||||
from io import BytesIO
|
||||
|
||||
buffer = BytesIO()
|
||||
# 保存为 PNG 格式到内存
|
||||
pixmap.save(buffer, "PNG")
|
||||
return buffer.getvalue()
|
||||
|
||||
def _save_clipboard_image(self, pixmap: QPixmap) -> Optional[str]:
|
||||
"""
|
||||
保存剪贴板图片
|
||||
|
||||
Args:
|
||||
pixmap: 图片对象
|
||||
|
||||
Returns:
|
||||
保存的文件路径,失败返回 None
|
||||
"""
|
||||
try:
|
||||
# 生成文件名
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"clipboard_{timestamp}.png"
|
||||
filepath = self.temp_dir / filename
|
||||
|
||||
# 保存图片
|
||||
if pixmap.save(str(filepath)):
|
||||
return str(filepath)
|
||||
|
||||
except Exception as e:
|
||||
print(f"保存剪贴板图片失败: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
"""
|
||||
检查监听是否启用
|
||||
|
||||
Returns:
|
||||
True 表示启用,False 表示禁用
|
||||
"""
|
||||
return self._enabled
|
||||
|
||||
def set_enabled(self, enabled: bool):
|
||||
"""
|
||||
设置监听状态
|
||||
|
||||
Args:
|
||||
enabled: True 启用,False 禁用
|
||||
"""
|
||||
self._enabled = enabled
|
||||
|
||||
def enable(self):
|
||||
"""启用监听"""
|
||||
self.set_enabled(True)
|
||||
|
||||
def disable(self):
|
||||
"""禁用监听"""
|
||||
self.set_enabled(False)
|
||||
|
||||
def has_image(self) -> bool:
|
||||
"""
|
||||
检查剪贴板当前是否有图片
|
||||
|
||||
Returns:
|
||||
True 表示有图片,False 表示无图片
|
||||
"""
|
||||
pixmap = self.clipboard.pixmap()
|
||||
return not pixmap.isNull()
|
||||
|
||||
def get_image(self) -> Optional[QPixmap]:
|
||||
"""
|
||||
获取剪贴板中的图片
|
||||
|
||||
Returns:
|
||||
图片对象,无图片时返回 None
|
||||
"""
|
||||
pixmap = self.clipboard.pixmap()
|
||||
if pixmap.isNull():
|
||||
return None
|
||||
return pixmap
|
||||
|
||||
def save_current_image(self, filepath: str) -> bool:
|
||||
"""
|
||||
保存当前剪贴板图片
|
||||
|
||||
Args:
|
||||
filepath: 保存路径
|
||||
|
||||
Returns:
|
||||
成功返回 True,失败返回 False
|
||||
"""
|
||||
pixmap = self.get_image()
|
||||
if pixmap is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
return pixmap.save(filepath)
|
||||
except Exception as e:
|
||||
print(f"保存图片失败: {e}")
|
||||
return False
|
||||
|
||||
def clear_history(self):
|
||||
"""清空临时保存的剪贴板图片历史"""
|
||||
try:
|
||||
import shutil
|
||||
if self.temp_dir.exists():
|
||||
shutil.rmtree(self.temp_dir)
|
||||
self.temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
print(f"清空剪贴板历史失败: {e}")
|
||||
|
||||
|
||||
class ClipboardImagePicker(QWidget):
|
||||
"""
|
||||
剪贴板图片选择器
|
||||
|
||||
提供图形界面,显示剪贴板中的图片并允许用户操作
|
||||
"""
|
||||
|
||||
# 信号:用户选择使用图片时发出,传递图片路径
|
||||
image_selected = pyqtSignal(str)
|
||||
# 信号:用户取消选择时发出
|
||||
selection_cancelled = pyqtSignal()
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
"""
|
||||
初始化剪贴板图片选择器
|
||||
|
||||
Args:
|
||||
parent: 父组件
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.clipboard = QApplication.clipboard()
|
||||
self.current_image_path = None
|
||||
|
||||
self._init_ui()
|
||||
self._check_clipboard()
|
||||
|
||||
def _init_ui(self):
|
||||
"""初始化UI"""
|
||||
from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QPushButton
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# 标题
|
||||
title = QLabel("剪贴板图片")
|
||||
title.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 图片预览
|
||||
self.preview_label = QLabel()
|
||||
self.preview_label.setMinimumSize(400, 300)
|
||||
self.preview_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.preview_label.setStyleSheet("""
|
||||
QLabel {
|
||||
background-color: #F5F5F5;
|
||||
border: 2px dashed #CCCCCC;
|
||||
border-radius: 8px;
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
""")
|
||||
self.preview_label.setText("剪贴板中没有图片")
|
||||
layout.addWidget(self.preview_label)
|
||||
|
||||
# 按钮区域
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(12)
|
||||
|
||||
# 刷新按钮
|
||||
self.refresh_btn = QPushButton("🔄 刷新")
|
||||
self.refresh_btn.setMinimumHeight(36)
|
||||
self.refresh_btn.clicked.connect(self._check_clipboard)
|
||||
button_layout.addWidget(self.refresh_btn)
|
||||
|
||||
button_layout.addStretch()
|
||||
|
||||
# 使用按钮
|
||||
self.use_btn = QPushButton("✓ 使用图片")
|
||||
self.use_btn.setMinimumHeight(36)
|
||||
self.use_btn.setEnabled(False)
|
||||
self.use_btn.clicked.connect(self._on_use_image)
|
||||
button_layout.addWidget(self.use_btn)
|
||||
|
||||
# 取消按钮
|
||||
self.cancel_btn = QPushButton("✕ 取消")
|
||||
self.cancel_btn.setMinimumHeight(36)
|
||||
self.cancel_btn.clicked.connect(self._on_cancel)
|
||||
button_layout.addWidget(self.cancel_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# 应用样式
|
||||
self.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #357ABD;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #2A639D;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #CCCCCC;
|
||||
color: #666666;
|
||||
}
|
||||
""")
|
||||
|
||||
def _check_clipboard(self):
|
||||
"""检查剪贴板中的图片"""
|
||||
pixmap = self.clipboard.pixmap()
|
||||
|
||||
if pixmap.isNull():
|
||||
# 无图片
|
||||
self.preview_label.setText("剪贴板中没有图片")
|
||||
self.preview_label.setPixmap(QPixmap())
|
||||
self.use_btn.setEnabled(False)
|
||||
self.current_image_path = None
|
||||
else:
|
||||
# 有图片
|
||||
# 缩放预览
|
||||
scaled_pixmap = pixmap.scaled(
|
||||
380, 280,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
|
||||
self.preview_label.setPixmap(scaled_pixmap)
|
||||
self.preview_label.setText("")
|
||||
|
||||
# 保存到临时文件
|
||||
self.current_image_path = self._save_temp_image(pixmap)
|
||||
self.use_btn.setEnabled(True)
|
||||
|
||||
def _save_temp_image(self, pixmap: QPixmap) -> Optional[str]:
|
||||
"""
|
||||
保存图片到临时文件
|
||||
|
||||
Args:
|
||||
pixmap: 图片对象
|
||||
|
||||
Returns:
|
||||
保存的文件路径
|
||||
"""
|
||||
try:
|
||||
temp_dir = Path(tempfile.gettempdir()) / "cutthenthink" / "clipboard"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"clipboard_{timestamp}.png"
|
||||
filepath = temp_dir / filename
|
||||
|
||||
if pixmap.save(str(filepath)):
|
||||
return str(filepath)
|
||||
|
||||
except Exception as e:
|
||||
print(f"保存临时图片失败: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _on_use_image(self):
|
||||
"""使用图片按钮点击处理"""
|
||||
if self.current_image_path:
|
||||
self.image_selected.emit(self.current_image_path)
|
||||
|
||||
def _on_cancel(self):
|
||||
"""取消按钮点击处理"""
|
||||
self.selection_cancelled.emit()
|
||||
|
||||
def refresh(self):
|
||||
"""刷新剪贴板检查"""
|
||||
self._check_clipboard()
|
||||
@@ -1,472 +0,0 @@
|
||||
"""
|
||||
图片文件选择组件
|
||||
|
||||
实现图片文件选择功能,包括:
|
||||
- 文件对话框选择
|
||||
- 拖放支持
|
||||
- 支持的图片格式过滤
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QLabel, QFileDialog, QFrame, QMessageBox
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QMimeData
|
||||
from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QPixmap, QCursor
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class ImagePicker(QWidget):
|
||||
"""
|
||||
图片选择器组件
|
||||
|
||||
提供多种方式选择图片:
|
||||
- 点击按钮打开文件对话框
|
||||
- 拖放文件到组件
|
||||
"""
|
||||
|
||||
# 信号:图片选择完成时发出,传递图片路径列表
|
||||
images_selected = pyqtSignal(list)
|
||||
# 信号:单个图片选择完成时发出,传递图片路径
|
||||
image_selected = pyqtSignal(str)
|
||||
# 信号:取消选择
|
||||
selection_cancelled = pyqtSignal()
|
||||
|
||||
# 支持的图片格式
|
||||
SUPPORTED_FORMATS = [
|
||||
"图片文件 (*.png *.jpg *.jpeg *.bmp *.gif *.webp *.tiff)",
|
||||
"PNG 文件 (*.png)",
|
||||
"JPEG 文件 (*.jpg *.jpeg)",
|
||||
"位图文件 (*.bmp)",
|
||||
"GIF 文件 (*.gif)",
|
||||
"WebP 文件 (*.webp)",
|
||||
"所有文件 (*.*)"
|
||||
]
|
||||
|
||||
def __init__(self, multiple: bool = False, parent: Optional[QWidget] = None):
|
||||
"""
|
||||
初始化图片选择器
|
||||
|
||||
Args:
|
||||
multiple: 是否允许多选
|
||||
parent: 父组件
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.multiple = multiple
|
||||
self.selected_paths = []
|
||||
|
||||
# 启用拖放
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui(self):
|
||||
"""初始化UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# 创建拖放区域
|
||||
self.drop_area = DropArea(self.multiple, self)
|
||||
self.drop_area.images_dropped.connect(self._on_images_dropped)
|
||||
layout.addWidget(self.drop_area)
|
||||
|
||||
# 创建按钮区域
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(12)
|
||||
|
||||
# 选择文件按钮
|
||||
self.select_btn = QPushButton("📂 选择图片")
|
||||
self.select_btn.setMinimumHeight(40)
|
||||
self.select_btn.clicked.connect(self._on_select_clicked)
|
||||
button_layout.addWidget(self.select_btn)
|
||||
|
||||
# 清除按钮
|
||||
self.clear_btn = QPushButton("🗑️ 清除")
|
||||
self.clear_btn.setMinimumHeight(40)
|
||||
self.clear_btn.setEnabled(False)
|
||||
self.clear_btn.clicked.connect(self._on_clear_clicked)
|
||||
button_layout.addWidget(self.clear_btn)
|
||||
|
||||
button_layout.addStretch()
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# 应用样式
|
||||
self._apply_styles()
|
||||
|
||||
def _apply_styles(self):
|
||||
"""应用样式"""
|
||||
self.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #357ABD;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #2A639D;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #CCCCCC;
|
||||
color: #666666;
|
||||
}
|
||||
""")
|
||||
|
||||
def _on_select_clicked(self):
|
||||
"""选择按钮点击处理"""
|
||||
if self.multiple:
|
||||
# 多选
|
||||
filepaths, _ = QFileDialog.getOpenFileNames(
|
||||
self,
|
||||
"选择图片",
|
||||
str(Path.home()),
|
||||
";;".join(self.SUPPORTED_FORMATS)
|
||||
)
|
||||
|
||||
if filepaths:
|
||||
self._on_images_dropped(filepaths)
|
||||
else:
|
||||
# 单选
|
||||
filepath, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择图片",
|
||||
str(Path.home()),
|
||||
";;".join(self.SUPPORTED_FORMATS)
|
||||
)
|
||||
|
||||
if filepath:
|
||||
self._on_images_dropped([filepath])
|
||||
|
||||
def _on_clear_clicked(self):
|
||||
"""清除按钮点击处理"""
|
||||
self.selected_paths.clear()
|
||||
self.drop_area.clear_previews()
|
||||
self.clear_btn.setEnabled(False)
|
||||
|
||||
def _on_images_dropped(self, paths: List[str]):
|
||||
"""
|
||||
图片拖放或选择完成处理
|
||||
|
||||
Args:
|
||||
paths: 图片路径列表
|
||||
"""
|
||||
# 过滤有效图片
|
||||
valid_paths = self._filter_valid_images(paths)
|
||||
|
||||
if not valid_paths:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"无效文件",
|
||||
"所选文件不是有效的图片格式"
|
||||
)
|
||||
return
|
||||
|
||||
if self.multiple:
|
||||
# 多选模式:添加到列表
|
||||
self.selected_paths.extend(valid_paths)
|
||||
self.images_selected.emit(self.selected_paths)
|
||||
else:
|
||||
# 单选模式:只保留最后一个
|
||||
self.selected_paths = valid_paths[-1:]
|
||||
if valid_paths:
|
||||
self.image_selected.emit(valid_paths[0])
|
||||
|
||||
# 更新预览
|
||||
self.drop_area.show_previews(valid_paths)
|
||||
self.clear_btn.setEnabled(True)
|
||||
|
||||
def _filter_valid_images(self, paths: List[str]) -> List[str]:
|
||||
"""
|
||||
过滤有效的图片文件
|
||||
|
||||
Args:
|
||||
paths: 文件路径列表
|
||||
|
||||
Returns:
|
||||
有效的图片路径列表
|
||||
"""
|
||||
valid_extensions = {'.png', '.jpg', '.jpeg', '.bmp',
|
||||
'.gif', '.webp', '.tiff', '.tif'}
|
||||
|
||||
valid_paths = []
|
||||
for path in paths:
|
||||
filepath = Path(path)
|
||||
if filepath.suffix.lower() in valid_extensions and filepath.exists():
|
||||
valid_paths.append(str(filepath))
|
||||
|
||||
return valid_paths
|
||||
|
||||
def get_selected_images(self) -> List[str]:
|
||||
"""
|
||||
获取已选择的图片路径
|
||||
|
||||
Returns:
|
||||
图片路径列表
|
||||
"""
|
||||
return self.selected_paths.copy()
|
||||
|
||||
def clear_selection(self):
|
||||
"""清除选择"""
|
||||
self._on_clear_clicked()
|
||||
|
||||
|
||||
class DropArea(QFrame):
|
||||
"""
|
||||
拖放区域组件
|
||||
|
||||
显示拖放提示和图片预览
|
||||
"""
|
||||
|
||||
# 信号:图片拖放完成,传递路径列表
|
||||
images_dropped = pyqtSignal(list)
|
||||
|
||||
def __init__(self, multiple: bool = False, parent: Optional[QWidget] = None):
|
||||
"""
|
||||
初始化拖放区域
|
||||
|
||||
Args:
|
||||
multiple: 是否支持多张图片
|
||||
parent: 父组件
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.multiple = multiple
|
||||
self.preview_labels = []
|
||||
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui(self):
|
||||
"""初始化UI"""
|
||||
self.setAcceptDrops(True)
|
||||
self.setMinimumHeight(200)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
||||
# 提示标签
|
||||
self.hint_label = QLabel()
|
||||
self.update_hint()
|
||||
self.hint_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
layout.addWidget(self.hint_label)
|
||||
|
||||
# 预览容器
|
||||
self.preview_container = QWidget()
|
||||
self.preview_layout = QVBoxLayout(self.preview_container)
|
||||
self.preview_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.preview_layout.setSpacing(10)
|
||||
layout.addWidget(self.preview_container)
|
||||
|
||||
# 应用样式
|
||||
self._apply_styles()
|
||||
|
||||
def _apply_styles(self):
|
||||
"""应用样式"""
|
||||
self.setStyleSheet("""
|
||||
DropArea {
|
||||
background-color: #F9F9F9;
|
||||
border: 2px dashed #CCCCCC;
|
||||
border-radius: 12px;
|
||||
}
|
||||
DropArea:hover {
|
||||
background-color: #F0F8FF;
|
||||
border: 2px dashed #4A90E2;
|
||||
}
|
||||
""")
|
||||
self.hint_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #666666;
|
||||
font-size: 16px;
|
||||
}
|
||||
""")
|
||||
|
||||
def update_hint(self):
|
||||
"""更新提示文本"""
|
||||
if self.multiple:
|
||||
hint = "🖼️ 拖放图片到此处\n或点击下方按钮选择"
|
||||
else:
|
||||
hint = "🖼️ 拖放图片到此处\n或点击下方按钮选择"
|
||||
|
||||
self.hint_label.setText(hint)
|
||||
|
||||
def dragEnterEvent(self, event: QDragEnterEvent):
|
||||
"""拖拽进入事件"""
|
||||
if event.mimeData().hasUrls():
|
||||
event.acceptProposedAction()
|
||||
self.setStyleSheet("""
|
||||
DropArea {
|
||||
background-color: #E6F2FF;
|
||||
border: 2px dashed #4A90E2;
|
||||
border-radius: 12px;
|
||||
}
|
||||
""")
|
||||
|
||||
def dragLeaveEvent(self, event):
|
||||
"""拖拽离开事件"""
|
||||
self._apply_styles()
|
||||
|
||||
def dropEvent(self, event: QDropEvent):
|
||||
"""拖放事件"""
|
||||
self._apply_styles()
|
||||
|
||||
mime_data = event.mimeData()
|
||||
if mime_data.hasUrls():
|
||||
# 获取文件路径
|
||||
paths = []
|
||||
for url in mime_data.urls():
|
||||
if url.isLocalFile():
|
||||
paths.append(url.toLocalFile())
|
||||
|
||||
if paths:
|
||||
self.images_dropped.emit(paths)
|
||||
|
||||
def show_previews(self, paths: List[str]):
|
||||
"""
|
||||
显示图片预览
|
||||
|
||||
Args:
|
||||
paths: 图片路径列表
|
||||
"""
|
||||
# 清除旧预览
|
||||
self.clear_previews()
|
||||
|
||||
if not self.multiple and len(paths) > 0:
|
||||
# 单选模式只显示第一个
|
||||
paths = [paths[0]]
|
||||
|
||||
for path in paths:
|
||||
# 创建预览标签
|
||||
preview_label = ImagePreviewLabel(path, self)
|
||||
self.preview_layout.addWidget(preview_label)
|
||||
self.preview_labels.append(preview_label)
|
||||
|
||||
# 隐藏提示
|
||||
self.hint_label.hide()
|
||||
|
||||
def clear_previews(self):
|
||||
"""清除所有预览"""
|
||||
for label in self.preview_labels:
|
||||
label.deleteLater()
|
||||
self.preview_labels.clear()
|
||||
self.hint_label.show()
|
||||
|
||||
|
||||
class ImagePreviewLabel(QLabel):
|
||||
"""
|
||||
图片预览标签
|
||||
|
||||
显示单个图片的预览
|
||||
"""
|
||||
|
||||
def __init__(self, image_path: str, parent: Optional[QWidget] = None):
|
||||
"""
|
||||
初始化预览标签
|
||||
|
||||
Args:
|
||||
image_path: 图片路径
|
||||
parent: 父组件
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.image_path = image_path
|
||||
self._load_preview()
|
||||
|
||||
def _load_preview(self):
|
||||
"""加载图片预览"""
|
||||
try:
|
||||
pixmap = QPixmap(self.image_path)
|
||||
|
||||
if not pixmap.isNull():
|
||||
# 缩放到合适大小
|
||||
scaled_pixmap = pixmap.scaled(
|
||||
560, 315,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
|
||||
self.setPixmap(scaled_pixmap)
|
||||
self.setAlignment(
|
||||
Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
|
||||
# 显示文件名
|
||||
filename = Path(self.image_path).name
|
||||
self.setToolTip(filename)
|
||||
|
||||
# 设置样式
|
||||
self.setStyleSheet("""
|
||||
QLabel {
|
||||
background-color: white;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
""")
|
||||
self.setMinimumHeight(100)
|
||||
|
||||
except Exception as e:
|
||||
self.setText(f"加载失败: {Path(self.image_path).name}")
|
||||
self.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #FF0000;
|
||||
font-size: 14px;
|
||||
}
|
||||
""")
|
||||
|
||||
|
||||
class QuickImagePicker:
|
||||
"""
|
||||
快速图片选择器助手
|
||||
|
||||
提供静态方法快速选择图片
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def pick_single_image(parent: Optional[QWidget] = None) -> Optional[str]:
|
||||
"""
|
||||
选择单个图片(同步对话框)
|
||||
|
||||
Args:
|
||||
parent: 父组件
|
||||
|
||||
Returns:
|
||||
选择的图片路径,取消返回 None
|
||||
"""
|
||||
filepath, _ = QFileDialog.getOpenFileName(
|
||||
parent,
|
||||
"选择图片",
|
||||
str(Path.home()),
|
||||
";;".join(ImagePicker.SUPPORTED_FORMATS)
|
||||
)
|
||||
|
||||
return filepath if filepath else None
|
||||
|
||||
@staticmethod
|
||||
def pick_multiple_images(parent: Optional[QWidget] = None) -> List[str]:
|
||||
"""
|
||||
选择多个图片(同步对话框)
|
||||
|
||||
Args:
|
||||
parent: 父组件
|
||||
|
||||
Returns:
|
||||
选择的图片路径列表
|
||||
"""
|
||||
filepaths, _ = QFileDialog.getOpenFileNames(
|
||||
parent,
|
||||
"选择图片",
|
||||
str(Path.home()),
|
||||
";;".join(ImagePicker.SUPPORTED_FORMATS)
|
||||
)
|
||||
|
||||
return filepaths
|
||||
@@ -1,504 +0,0 @@
|
||||
"""
|
||||
图片预览组件
|
||||
|
||||
实现图片预览功能,包括:
|
||||
- 图片显示和缩放
|
||||
- 缩放控制
|
||||
- 旋转功能
|
||||
- 全屏查看
|
||||
- 信息显示
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QLabel, QScrollArea, QFrame, QSizePolicy, QSlider,
|
||||
QApplication, QMessageBox
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QPoint, pyqtSignal, QSize, QRect
|
||||
from PyQt6.QtGui import QPixmap, QPainter, QCursor, QAction, QImage, QKeySequence
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ZoomMode(str, Enum):
|
||||
"""缩放模式"""
|
||||
FIT = "fit" # 适应窗口
|
||||
FILL = "fill" # 填充窗口
|
||||
ACTUAL = "actual" # 实际大小
|
||||
CUSTOM = "custom" # 自定义缩放
|
||||
|
||||
|
||||
class ImagePreviewWidget(QWidget):
|
||||
"""
|
||||
图片预览组件
|
||||
|
||||
提供完整的图片预览功能,包括缩放、旋转、平移等
|
||||
"""
|
||||
|
||||
# 信号:图片加载完成时发出
|
||||
image_loaded = pyqtSignal(str)
|
||||
# 信号:图片加载失败时发出
|
||||
image_load_failed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
"""
|
||||
初始化图片预览组件
|
||||
|
||||
Args:
|
||||
parent: 父组件
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
# 图片相关
|
||||
self.original_pixmap: Optional[QPixmap] = None
|
||||
self.current_pixmap: Optional[QPixmap] = None
|
||||
self.image_path = ""
|
||||
|
||||
# 显示参数
|
||||
self.zoom_factor = 1.0
|
||||
self.rotation_angle = 0
|
||||
self.min_zoom = 0.1
|
||||
self.max_zoom = 10.0
|
||||
self.zoom_mode = ZoomMode.FIT
|
||||
|
||||
# 拖动平移
|
||||
self.drag_start_pos: Optional[QPoint] = None
|
||||
self.scroll_start_pos: Optional[QPoint] = None
|
||||
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui(self):
|
||||
"""初始化UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# 创建工具栏
|
||||
self._create_toolbar(layout)
|
||||
|
||||
# 创建滚动区域
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setAlignment(
|
||||
Qt.AlignmentFlag.AlignCenter |
|
||||
Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
|
||||
# 创建图片标签
|
||||
self.image_label = ImageLabel()
|
||||
self.image_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignCenter |
|
||||
Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.image_label.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding,
|
||||
QSizePolicy.Policy.Expanding
|
||||
)
|
||||
|
||||
# 连接信号
|
||||
self.image_label.drag_started.connect(self._on_drag_started)
|
||||
self.image_label.dragged.connect(self._on_dragged)
|
||||
self.image_label.drag_finished.connect(self._on_drag_finished)
|
||||
|
||||
self.scroll_area.setWidget(self.image_label)
|
||||
layout.addWidget(self.scroll_area)
|
||||
|
||||
# 创建缩放滑块
|
||||
self._create_zoom_slider(layout)
|
||||
|
||||
# 应用样式
|
||||
self._apply_styles()
|
||||
|
||||
# 显示占位符
|
||||
self._show_placeholder()
|
||||
|
||||
def _create_toolbar(self, parent_layout: QVBoxLayout):
|
||||
"""创建工具栏"""
|
||||
toolbar = QWidget()
|
||||
toolbar.setObjectName("previewToolbar")
|
||||
toolbar_layout = QHBoxLayout(toolbar)
|
||||
toolbar_layout.setContentsMargins(12, 8, 12, 8)
|
||||
toolbar_layout.setSpacing(8)
|
||||
|
||||
# 放大按钮
|
||||
self.zoom_in_btn = QPushButton("🔍+")
|
||||
self.zoom_in_btn.setToolTip("放大 (Ctrl++)")
|
||||
self.zoom_in_btn.setMinimumSize(36, 36)
|
||||
self.zoom_in_btn.clicked.connect(self.zoom_in)
|
||||
toolbar_layout.addWidget(self.zoom_in_btn)
|
||||
|
||||
# 缩小按钮
|
||||
self.zoom_out_btn = QPushButton("🔍-")
|
||||
self.zoom_out_btn.setToolTip("缩小 (Ctrl+-)")
|
||||
self.zoom_out_btn.setMinimumSize(36, 36)
|
||||
self.zoom_out_btn.clicked.connect(self.zoom_out)
|
||||
toolbar_layout.addWidget(self.zoom_out_btn)
|
||||
|
||||
# 适应按钮
|
||||
self.fit_btn = QPushButton("📐 适应")
|
||||
self.fit_btn.setToolTip("适应窗口 (Ctrl+F)")
|
||||
self.fit_btn.setMinimumSize(60, 36)
|
||||
self.fit_btn.clicked.connect(self.fit_to_window)
|
||||
toolbar_layout.addWidget(self.fit_btn)
|
||||
|
||||
# 实际大小按钮
|
||||
self.actual_btn = QPushButton("1:1")
|
||||
self.actual_btn.setToolTip("实际大小 (Ctrl+0)")
|
||||
self.actual_btn.setMinimumSize(60, 36)
|
||||
self.actual_btn.clicked.connect(self.actual_size)
|
||||
toolbar_layout.addWidget(self.actual_btn)
|
||||
|
||||
toolbar_layout.addStretch()
|
||||
|
||||
# 左旋转按钮
|
||||
self.rotate_left_btn = QPushButton("↺")
|
||||
self.rotate_left_btn.setToolTip("向左旋转 (Ctrl+L)")
|
||||
self.rotate_left_btn.setMinimumSize(36, 36)
|
||||
self.rotate_left_btn.clicked.connect(lambda: self.rotate(-90))
|
||||
toolbar_layout.addWidget(self.rotate_left_btn)
|
||||
|
||||
# 右旋转按钮
|
||||
self.rotate_right_btn = QPushButton("↻")
|
||||
self.rotate_right_btn.setToolTip("向右旋转 (Ctrl+R)")
|
||||
self.rotate_right_btn.setMinimumSize(36, 36)
|
||||
self.rotate_right_btn.clicked.connect(lambda: self.rotate(90))
|
||||
toolbar_layout.addWidget(self.rotate_right_btn)
|
||||
|
||||
# 全屏按钮
|
||||
self.fullscreen_btn = QPushButton("⛶")
|
||||
self.fullscreen_btn.setToolTip("全屏 (F11)")
|
||||
self.fullscreen_btn.setMinimumSize(36, 36)
|
||||
self.fullscreen_btn.clicked.connect(self.toggle_fullscreen)
|
||||
toolbar_layout.addWidget(self.fullscreen_btn)
|
||||
|
||||
# 应用工具栏样式
|
||||
toolbar.setStyleSheet("""
|
||||
QWidget#previewToolbar {
|
||||
background-color: #F5F5F5;
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
}
|
||||
""")
|
||||
|
||||
parent_layout.addWidget(toolbar)
|
||||
|
||||
def _create_zoom_slider(self, parent_layout: QVBoxLayout):
|
||||
"""创建缩放滑块"""
|
||||
slider_container = QWidget()
|
||||
slider_layout = QHBoxLayout(slider_container)
|
||||
slider_layout.setContentsMargins(12, 4, 12, 8)
|
||||
slider_layout.setSpacing(8)
|
||||
|
||||
# 缩放百分比标签
|
||||
self.zoom_label = QLabel("100%")
|
||||
self.zoom_label.setMinimumWidth(60)
|
||||
slider_layout.addWidget(self.zoom_label)
|
||||
|
||||
# 缩放滑块
|
||||
self.zoom_slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.zoom_slider.setMinimum(10) # 10%
|
||||
self.zoom_slider.setMaximum(1000) # 1000%
|
||||
self.zoom_slider.setValue(100)
|
||||
self.zoom_slider.valueChanged.connect(self._on_slider_changed)
|
||||
slider_layout.addWidget(self.zoom_slider)
|
||||
|
||||
slider_container.setMaximumHeight(50)
|
||||
parent_layout.addWidget(slider_container)
|
||||
|
||||
def _apply_styles(self):
|
||||
"""应用样式"""
|
||||
self.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #357ABD;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #2A639D;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #CCCCCC;
|
||||
color: #666666;
|
||||
}
|
||||
QScrollArea {
|
||||
background-color: #2C2C2C;
|
||||
border: none;
|
||||
}
|
||||
QLabel {
|
||||
color: #666666;
|
||||
font-size: 13px;
|
||||
}
|
||||
""")
|
||||
|
||||
def _show_placeholder(self):
|
||||
"""显示占位符"""
|
||||
self.image_label.setText("""
|
||||
<div style='color: #999999; font-size: 16px;'>
|
||||
<p style='text-align: center;'>🖼️</p>
|
||||
<p style='text-align: center;'>暂无图片</p>
|
||||
<p style='text-align: center; font-size: 13px;'>
|
||||
请选择或拖入图片
|
||||
</p>
|
||||
</div>
|
||||
""")
|
||||
|
||||
def load_image(self, image_path: str) -> bool:
|
||||
"""
|
||||
加载图片
|
||||
|
||||
Args:
|
||||
image_path: 图片路径
|
||||
|
||||
Returns:
|
||||
成功返回 True,失败返回 False
|
||||
"""
|
||||
try:
|
||||
path = Path(image_path)
|
||||
if not path.exists():
|
||||
self.image_load_failed.emit(f"文件不存在: {image_path}")
|
||||
return False
|
||||
|
||||
# 加载图片
|
||||
pixmap = QPixmap(str(path))
|
||||
if pixmap.isNull():
|
||||
self.image_load_failed.emit(f"无法加载图片: {image_path}")
|
||||
return False
|
||||
|
||||
self.original_pixmap = pixmap
|
||||
self.current_pixmap = QPixmap(pixmap)
|
||||
self.image_path = image_path
|
||||
|
||||
# 重置显示参数
|
||||
self.rotation_angle = 0
|
||||
self.zoom_mode = ZoomMode.FIT
|
||||
self.fit_to_window()
|
||||
|
||||
self.image_loaded.emit(image_path)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.image_load_failed.emit(f"加载失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def load_pixmap(self, pixmap: QPixmap) -> bool:
|
||||
"""
|
||||
加载 QPixmap 对象
|
||||
|
||||
Args:
|
||||
pixmap: 图片对象
|
||||
|
||||
Returns:
|
||||
成功返回 True,失败返回 False
|
||||
"""
|
||||
try:
|
||||
if pixmap.isNull():
|
||||
return False
|
||||
|
||||
self.original_pixmap = QPixmap(pixmap)
|
||||
self.current_pixmap = QPixmap(pixmap)
|
||||
self.image_path = ""
|
||||
|
||||
# 重置显示参数
|
||||
self.rotation_angle = 0
|
||||
self.zoom_mode = ZoomMode.FIT
|
||||
self.fit_to_window()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def update_display(self):
|
||||
"""更新显示"""
|
||||
if self.current_pixmap is None:
|
||||
return
|
||||
|
||||
# 应用旋转
|
||||
rotated_pixmap = self.current_pixmap.transformed(
|
||||
self.current_pixmap.transform().rotate(self.rotation_angle),
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
|
||||
# 应用缩放
|
||||
if self.zoom_mode == ZoomMode.FIT:
|
||||
scaled_pixmap = rotated_pixmap.scaled(
|
||||
self.scroll_area.viewport().size(),
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
self.zoom_factor = scaled_pixmap.width() / rotated_pixmap.width()
|
||||
elif self.zoom_mode == ZoomMode.CUSTOM:
|
||||
scaled_pixmap = rotated_pixmap.scaled(
|
||||
int(rotated_pixmap.width() * self.zoom_factor),
|
||||
int(rotated_pixmap.height() * self.zoom_factor),
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
else: # ACTUAL
|
||||
scaled_pixmap = rotated_pixmap
|
||||
self.zoom_factor = 1.0
|
||||
|
||||
self.image_label.setPixmap(scaled_pixmap)
|
||||
self._update_zoom_label()
|
||||
|
||||
def _update_zoom_label(self):
|
||||
"""更新缩放标签"""
|
||||
zoom_percent = int(self.zoom_factor * 100)
|
||||
self.zoom_label.setText(f"{zoom_percent}%")
|
||||
self.zoom_slider.blockSignals(True)
|
||||
self.zoom_slider.setValue(zoom_percent)
|
||||
self.zoom_slider.blockSignals(False)
|
||||
|
||||
def zoom_in(self):
|
||||
"""放大"""
|
||||
self.zoom_mode = ZoomMode.CUSTOM
|
||||
self.zoom_factor = min(self.zoom_factor * 1.2, self.max_zoom)
|
||||
self.update_display()
|
||||
|
||||
def zoom_out(self):
|
||||
"""缩小"""
|
||||
self.zoom_mode = ZoomMode.CUSTOM
|
||||
self.zoom_factor = max(self.zoom_factor / 1.2, self.min_zoom)
|
||||
self.update_display()
|
||||
|
||||
def fit_to_window(self):
|
||||
"""适应窗口"""
|
||||
self.zoom_mode = ZoomMode.FIT
|
||||
self.update_display()
|
||||
|
||||
def actual_size(self):
|
||||
"""实际大小"""
|
||||
self.zoom_mode = ZoomMode.ACTUAL
|
||||
self.update_display()
|
||||
|
||||
def rotate(self, angle: int):
|
||||
"""
|
||||
旋转图片
|
||||
|
||||
Args:
|
||||
angle: 旋转角度(90 或 -90)
|
||||
"""
|
||||
self.rotation_angle = (self.rotation_angle + angle) % 360
|
||||
self.update_display()
|
||||
|
||||
def toggle_fullscreen(self):
|
||||
"""切换全屏"""
|
||||
window = self.window()
|
||||
if window.isFullScreen():
|
||||
window.showNormal()
|
||||
else:
|
||||
window.showFullScreen()
|
||||
|
||||
def _on_slider_changed(self, value: int):
|
||||
"""缩放滑块值改变"""
|
||||
self.zoom_mode = ZoomMode.CUSTOM
|
||||
self.zoom_factor = value / 100.0
|
||||
self.update_display()
|
||||
|
||||
def _on_drag_started(self, pos: QPoint):
|
||||
"""拖动开始"""
|
||||
self.drag_start_pos = pos
|
||||
self.scroll_start_pos = QPoint(
|
||||
self.scroll_area.horizontalScrollBar().value(),
|
||||
self.scroll_area.verticalScrollBar().value()
|
||||
)
|
||||
self.image_label.setCursor(QCursor(Qt.CursorShape.ClosedHandCursor))
|
||||
|
||||
def _on_dragged(self, pos: QPoint):
|
||||
"""拖动中"""
|
||||
if self.drag_start_pos and self.scroll_start_pos:
|
||||
delta = pos - self.drag_start_pos
|
||||
self.scroll_area.horizontalScrollBar().setValue(
|
||||
self.scroll_start_pos.x() - delta.x()
|
||||
)
|
||||
self.scroll_area.verticalScrollBar().setValue(
|
||||
self.scroll_start_pos.y() - delta.y()
|
||||
)
|
||||
|
||||
def _on_drag_finished(self):
|
||||
"""拖动结束"""
|
||||
self.drag_start_pos = None
|
||||
self.scroll_start_pos = None
|
||||
self.image_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""键盘事件"""
|
||||
# Ctrl++ 放大
|
||||
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
||||
if event.key() == Qt.Key.Key_Plus or event.key() == Qt.Key.Key_Equal:
|
||||
self.zoom_in()
|
||||
elif event.key() == Qt.Key.Key_Minus:
|
||||
self.zoom_out()
|
||||
elif event.key() == Qt.Key.Key_F:
|
||||
self.fit_to_window()
|
||||
elif event.key() == Qt.Key.Key_0:
|
||||
self.actual_size()
|
||||
elif event.key() == Qt.Key.Key_L:
|
||||
self.rotate(-90)
|
||||
elif event.key() == Qt.Key.Key_R:
|
||||
self.rotate(90)
|
||||
elif event.key() == Qt.Key.Key_F11:
|
||||
self.toggle_fullscreen()
|
||||
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def get_current_pixmap(self) -> Optional[QPixmap]:
|
||||
"""获取当前显示的图片"""
|
||||
return self.current_pixmap
|
||||
|
||||
def clear(self):
|
||||
"""清除图片"""
|
||||
self.original_pixmap = None
|
||||
self.current_pixmap = None
|
||||
self.image_path = ""
|
||||
self.zoom_factor = 1.0
|
||||
self.rotation_angle = 0
|
||||
self._show_placeholder()
|
||||
|
||||
|
||||
class ImageLabel(QLabel):
|
||||
"""
|
||||
可拖动的图片标签
|
||||
|
||||
支持鼠标拖动平移图片
|
||||
"""
|
||||
|
||||
# 信号
|
||||
drag_started = pyqtSignal(QPoint)
|
||||
dragged = pyqtSignal(QPoint)
|
||||
drag_finished = pyqtSignal()
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
"""初始化图片标签"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.is_dragging = False
|
||||
self.last_pos = QPoint()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""鼠标按下事件"""
|
||||
if event.button() == Qt.MouseButton.LeftButton and self.pixmap():
|
||||
self.is_dragging = True
|
||||
self.last_pos = event.pos()
|
||||
self.drag_started.emit(event.pos())
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""鼠标移动事件"""
|
||||
if self.is_dragging:
|
||||
self.dragged.emit(event.pos())
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""鼠标释放事件"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.is_dragging = False
|
||||
self.drag_finished.emit()
|
||||
super().mouseReleaseEvent(event)
|
||||
@@ -1,835 +0,0 @@
|
||||
"""
|
||||
错误提示和日志系统的 GUI 集成
|
||||
|
||||
提供统一的消息处理和错误显示功能
|
||||
"""
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
class LogLevel:
|
||||
"""日志级别"""
|
||||
DEBUG = "DEBUG"
|
||||
INFO = "INFO"
|
||||
WARNING = "WARNING"
|
||||
ERROR = "ERROR"
|
||||
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:
|
||||
"""
|
||||
消息处理器
|
||||
|
||||
负责显示各种类型的消息和错误
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
初始化消息处理器
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
设置日志捕获器
|
||||
|
||||
Args:
|
||||
capture: 日志捕获器
|
||||
"""
|
||||
self.log_capture = capture
|
||||
|
||||
def show_info(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
details: Optional[str] = None,
|
||||
log: bool = True
|
||||
):
|
||||
"""
|
||||
显示信息对话框
|
||||
|
||||
Args:
|
||||
title: 标题
|
||||
message: 消息内容
|
||||
details: 详细信息(可选)
|
||||
log: 是否记录到日志
|
||||
"""
|
||||
if log:
|
||||
logger.info(message)
|
||||
|
||||
if details:
|
||||
full_message = f"{message}\n\n详细信息:\n{details}"
|
||||
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:
|
||||
messagebox.showinfo(title, full_message)
|
||||
|
||||
def show_warning(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
details: Optional[str] = None,
|
||||
log: bool = True
|
||||
):
|
||||
"""
|
||||
显示警告对话框
|
||||
|
||||
Args:
|
||||
title: 标题
|
||||
message: 消息内容
|
||||
details: 详细信息(可选)
|
||||
log: 是否记录到日志
|
||||
"""
|
||||
if log:
|
||||
logger.warning(message)
|
||||
|
||||
if details:
|
||||
full_message = f"{message}\n\n详细信息:\n{details}"
|
||||
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:
|
||||
messagebox.showwarning(title, full_message)
|
||||
|
||||
def show_error(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
details: Optional[str] = None,
|
||||
exception: Optional[Exception] = None,
|
||||
log: bool = True
|
||||
):
|
||||
"""
|
||||
显示错误对话框
|
||||
|
||||
Args:
|
||||
title: 标题
|
||||
message: 消息内容
|
||||
details: 详细信息(可选)
|
||||
exception: 异常对象(可选)
|
||||
log: 是否记录到日志
|
||||
"""
|
||||
if log:
|
||||
logger.error(message, exc_info=exception is not None)
|
||||
|
||||
# 构建完整消息
|
||||
full_message = message
|
||||
|
||||
if exception:
|
||||
full_message += f"\n\n错误类型: {type(exception).__name__}"
|
||||
|
||||
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:
|
||||
messagebox.showerror(title, full_message)
|
||||
|
||||
def ask_yes_no(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
default: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
询问是/否
|
||||
|
||||
Args:
|
||||
title: 标题
|
||||
message: 消息内容
|
||||
default: 默认值(True=是,False=否)
|
||||
|
||||
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
|
||||
|
||||
def ask_ok_cancel(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
default: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
询问确定/取消
|
||||
|
||||
Args:
|
||||
title: 标题
|
||||
message: 消息内容
|
||||
default: 默认值(True=确定,False=取消)
|
||||
|
||||
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
|
||||
|
||||
def ask_retry_cancel(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
default: str = "retry"
|
||||
) -> Optional[bool]:
|
||||
"""
|
||||
询问重试/取消
|
||||
|
||||
Args:
|
||||
title: 标题
|
||||
message: 消息内容
|
||||
default: 默认选项 ("retry" 或 "cancel")
|
||||
|
||||
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:
|
||||
result = messagebox.askretrycancel(title, message, default=default == "retry")
|
||||
|
||||
if result is True:
|
||||
logger.info(f"用户选择: 重试 ({message})")
|
||||
elif result is False:
|
||||
logger.info(f"用户选择: 取消 ({message})")
|
||||
else:
|
||||
logger.info(f"用户选择: 关闭 ({message})")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# 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 版本)
|
||||
|
||||
显示详细的错误和日志信息
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
title: str = "错误日志",
|
||||
errors: Optional[List[Dict[str, Any]]] = None
|
||||
):
|
||||
"""
|
||||
初始化错误日志查看器
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
title: 窗口标题
|
||||
errors: 错误列表
|
||||
"""
|
||||
self.parent = parent
|
||||
self.errors = errors or []
|
||||
|
||||
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_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,
|
||||
textvariable=self.level_var,
|
||||
values=["ALL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
width=10,
|
||||
state=tk.READONLY
|
||||
)
|
||||
level_combo.pack(side=tk.LEFT, padx=5)
|
||||
level_combo.bind("<<ComboboxSelected>>", self._on_filter_change)
|
||||
|
||||
ttk.Button(toolbar, text="清空", command=self._on_clear).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(toolbar, text="导出", command=self._on_export).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(toolbar, text="关闭", command=self.destroy).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
# 主内容区域
|
||||
content_frame = ttk.Frame(self)
|
||||
content_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
# 创建文本控件
|
||||
self.text_widget = tk.Text(
|
||||
content_frame,
|
||||
wrap=tk.WORD,
|
||||
font=("Consolas", 9)
|
||||
)
|
||||
self.text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# 滚动条
|
||||
scrollbar = ttk.Scrollbar(content_frame, orient=tk.VERTICAL, command=self.text_widget.yview)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self.text_widget.config(yscrollcommand=scrollbar.set)
|
||||
|
||||
def _on_filter_change(self, event=None):
|
||||
"""过滤器改变"""
|
||||
self._load_errors()
|
||||
|
||||
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_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
|
||||
for error in self.errors:
|
||||
level = error.get("level", "INFO")
|
||||
|
||||
# 过滤
|
||||
if level_filter != "ALL" and level != level_filter:
|
||||
continue
|
||||
|
||||
count += 1
|
||||
|
||||
timestamp = error.get("timestamp", datetime.now())
|
||||
message = error.get("message", "")
|
||||
|
||||
# 格式化时间
|
||||
if isinstance(timestamp, datetime):
|
||||
time_str = timestamp.strftime("%H:%M:%S")
|
||||
else:
|
||||
time_str = str(timestamp)
|
||||
|
||||
# 插入内容
|
||||
if HAS_TKINTER:
|
||||
import tkinter as tk
|
||||
self.text_widget.insert(tk.END, f"[{time_str}] ", "timestamp")
|
||||
self.text_widget.insert(tk.END, f"[{level}] ", level)
|
||||
self.text_widget.insert(tk.END, f"{message}\n")
|
||||
else:
|
||||
self.text_widget.append(f"[{time_str}] [{level}] {message}")
|
||||
|
||||
|
||||
class ProgressDialog:
|
||||
"""
|
||||
进度对话框(选择 Tkinter 或 PyQt6 实现)
|
||||
|
||||
显示处理进度和状态
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
title: str = "处理中",
|
||||
message: str = "请稍候...",
|
||||
cancelable: bool = False,
|
||||
on_cancel: Optional[Callable] = None
|
||||
):
|
||||
"""
|
||||
初始化进度对话框
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
title: 标题
|
||||
message: 消息
|
||||
cancelable: 是否可取消
|
||||
on_cancel: 取消回调
|
||||
"""
|
||||
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
|
||||
|
||||
# 启动进度条动画
|
||||
if HAS_TKINTER:
|
||||
self._impl.progress_bar.start(10)
|
||||
else:
|
||||
from PyQt6.QtCore import QPropertyAnimation
|
||||
# PyQt6 不需要手动启动动画
|
||||
|
||||
def set_message(self, message: str):
|
||||
"""
|
||||
设置消息
|
||||
|
||||
Args:
|
||||
message: 消息内容
|
||||
"""
|
||||
if HAS_TKINTER:
|
||||
self._impl.set_message(message)
|
||||
else:
|
||||
self.message_label.setText(message)
|
||||
|
||||
def set_detail(self, detail: str):
|
||||
"""
|
||||
设置详细信息
|
||||
|
||||
Args:
|
||||
detail: 详细信息
|
||||
"""
|
||||
if HAS_TKINTER:
|
||||
self._impl.set_detail(detail)
|
||||
else:
|
||||
self.message_label.setText(f"{self.message_label.text()}\n{detail}")
|
||||
|
||||
def set_progress(self, value: float, maximum: float = 100):
|
||||
"""
|
||||
设置进度值
|
||||
|
||||
Args:
|
||||
value: 当前进度值
|
||||
maximum: 最大值
|
||||
"""
|
||||
if HAS_TKINTER:
|
||||
self._impl.set_progress(value, maximum)
|
||||
else:
|
||||
if self.progress_bar:
|
||||
self.progress_bar.setRange(0, int(maximum))
|
||||
self.progress_bar.setValue(int(value))
|
||||
else:
|
||||
from PyQt6.QtWidgets import QProgressBar
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setRange(0, int(maximum))
|
||||
self.progress_bar.setValue(int(value))
|
||||
|
||||
def _on_cancel(self):
|
||||
"""取消按钮点击"""
|
||||
self.cancelled = True
|
||||
if self.on_cancel_callback:
|
||||
self.on_cancel_callback()
|
||||
self.close()
|
||||
|
||||
def is_cancelled(self) -> bool:
|
||||
"""
|
||||
检查是否已取消
|
||||
|
||||
Returns:
|
||||
是否已取消
|
||||
"""
|
||||
return self.cancelled
|
||||
|
||||
def close(self):
|
||||
"""关闭对话框"""
|
||||
self.accept()
|
||||
|
||||
|
||||
class _TkProgressDialog:
|
||||
"""Tkinter 进度对话框实现"""
|
||||
|
||||
def __init__(self, on_cancel):
|
||||
self.on_cancel_callback = on_cancel
|
||||
self.cancelled = False
|
||||
self.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)
|
||||
|
||||
# 消息标签
|
||||
self.message_label = ttk.Label(main_frame, text=message, font=("Arial", 10))
|
||||
self.message_label.pack(side=tk.TOP, pady=(0, 20))
|
||||
|
||||
# 进度条
|
||||
self.progress_bar = ttk.Progressbar(
|
||||
main_frame,
|
||||
mode='indeterminate',
|
||||
length=350
|
||||
)
|
||||
self.progress_bar.pack(side=tk.TOP, pady=(0, 10))
|
||||
|
||||
# 启动进度条动画
|
||||
self.progress_bar.start(10)
|
||||
|
||||
# 详细信息标签
|
||||
self.detail_label = ttk.Label(main_frame, text="", font=("Arial", 9))
|
||||
self.detail_label.pack(side=tk.TOP, pady=(0, 20))
|
||||
|
||||
# 取消按钮
|
||||
if cancelable:
|
||||
cancel_btn = ttk.Button(main_frame, text="取消", command=self._on_cancel)
|
||||
cancel_btn.pack(side=tk.TOP)
|
||||
|
||||
def set_message(self, message: str):
|
||||
self.message_label.config(text=message)
|
||||
|
||||
def set_detail(self, detail: str):
|
||||
self.detail_label.config(text=detail)
|
||||
|
||||
def set_progress(self, value: float, maximum: float = 100):
|
||||
self.progress_bar.config(mode='determinate')
|
||||
self.progress_bar.config(maximum=maximum)
|
||||
self.progress_bar.config(value=value)
|
||||
|
||||
def _on_cancel(self):
|
||||
"""取消按钮点击"""
|
||||
self.cancelled = True
|
||||
if self.on_cancel_callback:
|
||||
self.on_cancel_callback()
|
||||
# 关闭对话框
|
||||
import tkinter as tk
|
||||
self.destroy() # tkinter 的 Toplevel 有 destroy 方法
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def show_info(title: str, message: str, details: Optional[str] = None, parent=None):
|
||||
"""显示信息对话框"""
|
||||
handler = MessageHandler(parent)
|
||||
handler.show_info(title, message, details)
|
||||
|
||||
|
||||
def show_warning(title: str, message: str, details: Optional[str] = None, parent=None):
|
||||
"""显示警告对话框"""
|
||||
handler = MessageHandler(parent)
|
||||
handler.show_warning(title, message, details)
|
||||
|
||||
|
||||
def show_error(title: str, message: str, details: Optional[str] = None, exception: Optional[Exception] = None, parent=None):
|
||||
"""显示错误对话框"""
|
||||
handler = MessageHandler(parent)
|
||||
handler.show_error(title, message, details, exception)
|
||||
|
||||
|
||||
def ask_yes_no(title: str, message: str, parent=None, default: bool = True) -> bool:
|
||||
"""询问是/否"""
|
||||
handler = MessageHandler(parent)
|
||||
return handler.ask_yes_no(title, message, default)
|
||||
|
||||
|
||||
def ask_ok_cancel(title: str, message: str, parent=None, default: bool = True) -> bool:
|
||||
"""询问确定/取消"""
|
||||
handler = MessageHandler(parent)
|
||||
return handler.ask_ok_cancel(title, message, default)
|
||||
@@ -1,290 +0,0 @@
|
||||
"""
|
||||
记录卡片组件
|
||||
|
||||
用于在浏览视图中展示单条记录的卡片,包含:
|
||||
- 缩略图预览
|
||||
- 分类标签
|
||||
- OCR 文本预览
|
||||
- 时间戳
|
||||
- 点击打开详情
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame,
|
||||
QPushButton, QGraphicsDropShadowEffect
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QSize
|
||||
from PyQt6.QtGui import QPixmap, QImage, QPainter, QPalette, QColor, QFont
|
||||
|
||||
|
||||
class RecordCard(QFrame):
|
||||
"""
|
||||
记录卡片组件
|
||||
|
||||
显示单条记录的摘要信息,点击可查看详情
|
||||
"""
|
||||
|
||||
# 定义信号:点击卡片时发出,传递记录ID
|
||||
clicked = pyqtSignal(int)
|
||||
|
||||
# 分类颜色映射
|
||||
CATEGORY_COLORS = {
|
||||
"TODO": "#5DADE2", # 蓝色
|
||||
"NOTE": "#58D68D", # 绿色
|
||||
"IDEA": "#F5B041", # 橙色
|
||||
"REF": "#AF7AC5", # 紫色
|
||||
"FUNNY": "#EC7063", # 红色
|
||||
"TEXT": "#95A5A6", # 灰色
|
||||
}
|
||||
|
||||
# 分类名称映射
|
||||
CATEGORY_NAMES = {
|
||||
"TODO": "待办",
|
||||
"NOTE": "笔记",
|
||||
"IDEA": "灵感",
|
||||
"REF": "参考",
|
||||
"FUNNY": "趣味",
|
||||
"TEXT": "文本",
|
||||
}
|
||||
|
||||
def __init__(self, record_id: int, image_path: str, ocr_text: str,
|
||||
category: str, created_at: Optional[datetime] = None,
|
||||
parent: Optional[QWidget] = None):
|
||||
"""
|
||||
初始化记录卡片
|
||||
|
||||
Args:
|
||||
record_id: 记录ID
|
||||
image_path: 图片路径
|
||||
ocr_text: OCR识别的文本
|
||||
category: 分类
|
||||
created_at: 创建时间
|
||||
parent: 父组件
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.record_id = record_id
|
||||
self.image_path = image_path
|
||||
self.ocr_text = ocr_text or ""
|
||||
self.category = category
|
||||
self.created_at = created_at
|
||||
|
||||
# 设置卡片样式
|
||||
self.setup_ui()
|
||||
self.set_style()
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI布局"""
|
||||
self.setFrameStyle(QFrame.Shape.Box)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
# 主布局
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(12, 12, 12, 12)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# 1. 缩略图
|
||||
self.thumbnail_label = QLabel()
|
||||
self.thumbnail_label.setMinimumHeight(150)
|
||||
self.thumbnail_label.setMaximumHeight(150)
|
||||
self.thumbnail_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.thumbnail_label.setStyleSheet("""
|
||||
QLabel {
|
||||
background-color: #F5F5F5;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #E0E0E0;
|
||||
}
|
||||
""")
|
||||
self.load_thumbnail()
|
||||
layout.addWidget(self.thumbnail_label)
|
||||
|
||||
# 2. 分类标签和时间
|
||||
header_layout = QHBoxLayout()
|
||||
header_layout.setSpacing(8)
|
||||
|
||||
self.category_label = QLabel()
|
||||
self.category_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.category_label.setStyleSheet("""
|
||||
QLabel {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
""")
|
||||
self.update_category_label()
|
||||
header_layout.addWidget(self.category_label)
|
||||
|
||||
header_layout.addStretch()
|
||||
|
||||
self.time_label = QLabel()
|
||||
self.time_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #999999;
|
||||
font-size: 11px;
|
||||
}
|
||||
""")
|
||||
self.update_time_label()
|
||||
header_layout.addWidget(self.time_label)
|
||||
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
# 3. OCR文本预览
|
||||
self.preview_label = QLabel()
|
||||
self.preview_label.setWordWrap(True)
|
||||
self.preview_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
||||
self.preview_label.setMinimumHeight(60)
|
||||
self.preview_label.setMaximumHeight(80)
|
||||
self.preview_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #333333;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
""")
|
||||
self.update_preview_text()
|
||||
layout.addWidget(self.preview_label)
|
||||
|
||||
# 设置卡片固定宽度
|
||||
self.setFixedWidth(280)
|
||||
self.setMinimumHeight(320)
|
||||
|
||||
def set_style(self):
|
||||
"""设置卡片整体样式"""
|
||||
self.setStyleSheet("""
|
||||
RecordCard {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #E8E8E8;
|
||||
}
|
||||
RecordCard:hover {
|
||||
background-color: #FAFAFA;
|
||||
border: 1px solid #4A90E2;
|
||||
}
|
||||
""")
|
||||
|
||||
# 添加阴影效果
|
||||
shadow = QGraphicsDropShadowEffect()
|
||||
shadow.setBlurRadius(10)
|
||||
shadow.setOffset(0, 2)
|
||||
shadow.setColor(QColor(0, 0, 0, 30))
|
||||
self.setGraphicsEffect(shadow)
|
||||
|
||||
def load_thumbnail(self):
|
||||
"""加载缩略图"""
|
||||
try:
|
||||
image_path = Path(self.image_path)
|
||||
if image_path.exists():
|
||||
# 加载图片
|
||||
pixmap = QPixmap(str(image_path))
|
||||
|
||||
# 缩放到合适大小(保持比例)
|
||||
scaled_pixmap = pixmap.scaled(
|
||||
260, 140,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
|
||||
self.thumbnail_label.setPixmap(scaled_pixmap)
|
||||
else:
|
||||
# 图片不存在,显示占位符
|
||||
self.thumbnail_label.setText("图片未找到")
|
||||
except Exception as e:
|
||||
# 加载失败,显示占位符
|
||||
self.thumbnail_label.setText("加载失败")
|
||||
|
||||
def update_category_label(self):
|
||||
"""更新分类标签"""
|
||||
category_name = self.CATEGORY_NAMES.get(self.category, self.category)
|
||||
self.category_label.setText(category_name)
|
||||
|
||||
# 设置分类颜色
|
||||
color = self.CATEGORY_COLORS.get(self.category, "#95A5A6")
|
||||
self.category_label.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background-color: {color};
|
||||
}}
|
||||
""")
|
||||
|
||||
def update_time_label(self):
|
||||
"""更新时间标签"""
|
||||
if self.created_at:
|
||||
# 格式化时间
|
||||
now = datetime.now()
|
||||
diff = now - self.created_at
|
||||
|
||||
if diff.days > 7:
|
||||
# 超过一周显示完整日期
|
||||
time_str = self.created_at.strftime("%Y-%m-%d")
|
||||
elif diff.days > 0:
|
||||
# 几天前
|
||||
time_str = f"{diff.days}天前"
|
||||
elif diff.seconds >= 3600:
|
||||
# 几小时前
|
||||
hours = diff.seconds // 3600
|
||||
time_str = f"{hours}小时前"
|
||||
elif diff.seconds >= 60:
|
||||
# 几分钟前
|
||||
minutes = diff.seconds // 60
|
||||
time_str = f"{minutes}分钟前"
|
||||
else:
|
||||
time_str = "刚刚"
|
||||
else:
|
||||
time_str = ""
|
||||
|
||||
self.time_label.setText(time_str)
|
||||
|
||||
def update_preview_text(self):
|
||||
"""更新预览文本"""
|
||||
if self.ocr_text:
|
||||
# 截取前100个字符作为预览
|
||||
preview = self.ocr_text[:100]
|
||||
if len(self.ocr_text) > 100:
|
||||
preview += "..."
|
||||
self.preview_label.setText(preview)
|
||||
else:
|
||||
self.preview_label.setText("无文本内容")
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""鼠标点击事件"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit(self.record_id)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def update_data(self, image_path: Optional[str] = None,
|
||||
ocr_text: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
created_at: Optional[datetime] = None):
|
||||
"""
|
||||
更新卡片数据
|
||||
|
||||
Args:
|
||||
image_path: 新的图片路径
|
||||
ocr_text: 新的OCR文本
|
||||
category: 新的分类
|
||||
created_at: 新的创建时间
|
||||
"""
|
||||
if image_path is not None:
|
||||
self.image_path = image_path
|
||||
self.load_thumbnail()
|
||||
|
||||
if ocr_text is not None:
|
||||
self.ocr_text = ocr_text
|
||||
self.update_preview_text()
|
||||
|
||||
if category is not None:
|
||||
self.category = category
|
||||
self.update_category_label()
|
||||
|
||||
if created_at is not None:
|
||||
self.created_at = created_at
|
||||
self.update_time_label()
|
||||
@@ -1,442 +0,0 @@
|
||||
"""
|
||||
记录详情对话框
|
||||
|
||||
显示单条记录的完整信息:
|
||||
- 完整图片预览
|
||||
- 完整OCR文本
|
||||
- AI分析结果
|
||||
- 分类和标签
|
||||
- 支持编辑和删除操作
|
||||
"""
|
||||
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QTextEdit, QScrollArea, QComboBox, QFrame, QSizePolicy,
|
||||
QMessageBox, QWidget
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QSize
|
||||
from PyQt6.QtGui import QPixmap, QFont, QTextDocument
|
||||
|
||||
from src.models.database import RecordCategory
|
||||
|
||||
|
||||
class RecordDetailDialog(QDialog):
|
||||
"""
|
||||
记录详情对话框
|
||||
|
||||
显示记录的完整信息,支持查看图片、OCR文本和AI结果
|
||||
"""
|
||||
|
||||
def __init__(self, record_id: int, image_path: str, ocr_text: str,
|
||||
category: str, ai_result: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None, notes: Optional[str] = None,
|
||||
created_at: Optional[datetime] = None,
|
||||
updated_at: Optional[datetime] = None,
|
||||
parent: Optional['QWidget'] = None):
|
||||
"""
|
||||
初始化记录详情对话框
|
||||
|
||||
Args:
|
||||
record_id: 记录ID
|
||||
image_path: 图片路径
|
||||
ocr_text: OCR文本
|
||||
category: 分类
|
||||
ai_result: AI分析结果
|
||||
tags: 标签列表
|
||||
notes: 备注
|
||||
created_at: 创建时间
|
||||
updated_at: 更新时间
|
||||
parent: 父组件
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.record_id = record_id
|
||||
self.image_path = image_path
|
||||
self.ocr_text = ocr_text or ""
|
||||
self.category = category
|
||||
self.ai_result = ai_result or ""
|
||||
self.tags = tags or []
|
||||
self.notes = notes or ""
|
||||
self.created_at = created_at
|
||||
self.updated_at = updated_at
|
||||
|
||||
self.modified = False
|
||||
|
||||
self.setup_ui()
|
||||
self.load_data()
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI布局"""
|
||||
self.setWindowTitle("记录详情")
|
||||
self.setMinimumSize(900, 700)
|
||||
self.resize(1000, 800)
|
||||
|
||||
# 主布局
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(20, 20, 20, 20)
|
||||
main_layout.setSpacing(15)
|
||||
|
||||
# 顶部工具栏
|
||||
toolbar_layout = QHBoxLayout()
|
||||
toolbar_layout.setSpacing(10)
|
||||
|
||||
# 分类选择
|
||||
category_label = QLabel("分类:")
|
||||
category_label.setStyleSheet("font-size: 14px; font-weight: bold;")
|
||||
toolbar_layout.addWidget(category_label)
|
||||
|
||||
self.category_combo = QComboBox()
|
||||
self.category_combo.addItems(RecordCategory.all())
|
||||
self.category_combo.currentTextChanged.connect(self.on_category_changed)
|
||||
toolbar_layout.addWidget(self.category_combo)
|
||||
|
||||
toolbar_layout.addStretch()
|
||||
|
||||
# 删除按钮
|
||||
self.delete_btn = QPushButton("删除记录")
|
||||
self.delete_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #EC7063;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #E74C3C;
|
||||
}
|
||||
""")
|
||||
self.delete_btn.clicked.connect(self.delete_record)
|
||||
toolbar_layout.addWidget(self.delete_btn)
|
||||
|
||||
# 关闭按钮
|
||||
self.close_btn = QPushButton("关闭")
|
||||
self.close_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #95A5A6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #7F8C8D;
|
||||
}
|
||||
""")
|
||||
self.close_btn.clicked.connect(self.close)
|
||||
toolbar_layout.addWidget(self.close_btn)
|
||||
|
||||
main_layout.addLayout(toolbar_layout)
|
||||
|
||||
# 创建滚动区域
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
|
||||
# 滚动内容
|
||||
scroll_content = QWidget()
|
||||
scroll_layout = QVBoxLayout(scroll_content)
|
||||
scroll_layout.setSpacing(20)
|
||||
scroll_layout.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
# 1. 图片预览
|
||||
image_section = self.create_image_section()
|
||||
scroll_layout.addWidget(image_section)
|
||||
|
||||
# 2. OCR文本
|
||||
ocr_section = self.create_ocr_section()
|
||||
scroll_layout.addWidget(ocr_section)
|
||||
|
||||
# 3. AI分析结果
|
||||
ai_section = self.create_ai_section()
|
||||
scroll_layout.addWidget(ai_section)
|
||||
|
||||
# 4. 备注
|
||||
notes_section = self.create_notes_section()
|
||||
scroll_layout.addWidget(notes_section)
|
||||
|
||||
# 5. 时间信息
|
||||
time_section = self.create_time_section()
|
||||
scroll_layout.addWidget(time_section)
|
||||
|
||||
scroll_layout.addStretch()
|
||||
|
||||
scroll_area.setWidget(scroll_content)
|
||||
main_layout.addWidget(scroll_area)
|
||||
|
||||
def create_image_section(self) -> QFrame:
|
||||
"""创建图片预览区域"""
|
||||
frame = QFrame()
|
||||
frame.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #E0E0E0;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(frame)
|
||||
|
||||
# 标题
|
||||
title = QLabel("原始图片")
|
||||
title.setStyleSheet("font-size: 16px; font-weight: bold; color: #333;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 图片预览
|
||||
self.image_label = QLabel()
|
||||
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.image_label.setMinimumHeight(300)
|
||||
self.image_label.setStyleSheet("background-color: #F5F5F5; border-radius: 6px;")
|
||||
layout.addWidget(self.image_label)
|
||||
|
||||
return frame
|
||||
|
||||
def create_ocr_section(self) -> QFrame:
|
||||
"""创建OCR文本区域"""
|
||||
frame = QFrame()
|
||||
frame.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #E0E0E0;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(frame)
|
||||
|
||||
# 标题
|
||||
title = QLabel("OCR识别结果")
|
||||
title.setStyleSheet("font-size: 16px; font-weight: bold; color: #333;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 文本内容
|
||||
self.ocr_text_edit = QTextEdit()
|
||||
self.ocr_text_edit.setReadOnly(True)
|
||||
self.ocr_text_edit.setMinimumHeight(150)
|
||||
self.ocr_text_edit.setStyleSheet("""
|
||||
QTextEdit {
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background-color: #FAFAFA;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.ocr_text_edit)
|
||||
|
||||
return frame
|
||||
|
||||
def create_ai_section(self) -> QFrame:
|
||||
"""创建AI分析结果区域"""
|
||||
frame = QFrame()
|
||||
frame.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #E0E0E0;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(frame)
|
||||
|
||||
# 标题
|
||||
title = QLabel("AI分析结果")
|
||||
title.setStyleSheet("font-size: 16px; font-weight: bold; color: #333;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 使用 QTextEdit 显示 Markdown
|
||||
self.ai_text_edit = QTextEdit()
|
||||
self.ai_text_edit.setReadOnly(True)
|
||||
self.ai_text_edit.setMinimumHeight(200)
|
||||
self.ai_text_edit.setStyleSheet("""
|
||||
QTextEdit {
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
background-color: #FAFAFA;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.ai_text_edit)
|
||||
|
||||
return frame
|
||||
|
||||
def create_notes_section(self) -> QFrame:
|
||||
"""创建备注区域"""
|
||||
frame = QFrame()
|
||||
frame.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #E0E0E0;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(frame)
|
||||
|
||||
# 标题
|
||||
title = QLabel("备注")
|
||||
title.setStyleSheet("font-size: 16px; font-weight: bold; color: #333;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 备注输入
|
||||
self.notes_edit = QTextEdit()
|
||||
self.notes_edit.setPlaceholderText("添加备注...")
|
||||
self.notes_edit.setMinimumHeight(100)
|
||||
self.notes_edit.setStyleSheet("""
|
||||
QTextEdit {
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
""")
|
||||
self.notes_edit.textChanged.connect(self.on_content_changed)
|
||||
layout.addWidget(self.notes_edit)
|
||||
|
||||
return frame
|
||||
|
||||
def create_time_section(self) -> QFrame:
|
||||
"""创建时间信息区域"""
|
||||
frame = QFrame()
|
||||
frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #F8F9FA;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QHBoxLayout(frame)
|
||||
|
||||
self.created_at_label = QLabel()
|
||||
self.created_at_label.setStyleSheet("color: #666; font-size: 12px;")
|
||||
layout.addWidget(self.created_at_label)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
self.updated_at_label = QLabel()
|
||||
self.updated_at_label.setStyleSheet("color: #666; font-size: 12px;")
|
||||
layout.addWidget(self.updated_at_label)
|
||||
|
||||
return frame
|
||||
|
||||
def load_data(self):
|
||||
"""加载数据到界面"""
|
||||
# 设置分类
|
||||
index = self.category_combo.findText(self.category)
|
||||
if index >= 0:
|
||||
self.category_combo.setCurrentIndex(index)
|
||||
|
||||
# 加载图片
|
||||
try:
|
||||
image_path = Path(self.image_path)
|
||||
if image_path.exists():
|
||||
pixmap = QPixmap(str(image_path))
|
||||
# 缩放图片以适应区域
|
||||
scaled_pixmap = pixmap.scaled(
|
||||
800, 600,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
self.image_label.setPixmap(scaled_pixmap)
|
||||
else:
|
||||
self.image_label.setText("图片文件不存在")
|
||||
except Exception as e:
|
||||
self.image_label.setText(f"加载图片失败: {str(e)}")
|
||||
|
||||
# 设置OCR文本
|
||||
self.ocr_text_edit.setPlainText(self.ocr_text)
|
||||
|
||||
# 设置AI结果(显示纯文本)
|
||||
if self.ai_result:
|
||||
self.ai_text_edit.setPlainText(self.ai_result)
|
||||
else:
|
||||
self.ai_text_edit.setPlainText("无AI分析结果")
|
||||
|
||||
# 设置备注
|
||||
self.notes_edit.setPlainText(self.notes)
|
||||
|
||||
# 设置时间信息
|
||||
if self.created_at:
|
||||
self.created_at_label.setText(f"创建时间: {self.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
if self.updated_at:
|
||||
self.updated_at_label.setText(f"更新时间: {self.updated_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
def on_category_changed(self, category: str):
|
||||
"""分类改变时"""
|
||||
self.category = category
|
||||
self.modified = True
|
||||
|
||||
def on_content_changed(self):
|
||||
"""内容改变时"""
|
||||
self.modified = True
|
||||
|
||||
def delete_record(self):
|
||||
"""删除记录"""
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"确认删除",
|
||||
"确定要删除这条记录吗?\n\n此操作不可撤销!",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
# 发出删除信号(由父窗口处理)
|
||||
self.accept()
|
||||
# 这里可以添加自定义信号通知父窗口删除记录
|
||||
|
||||
def get_data(self) -> dict:
|
||||
"""
|
||||
获取修改后的数据
|
||||
|
||||
Returns:
|
||||
包含修改后数据的字典
|
||||
"""
|
||||
return {
|
||||
'category': self.category_combo.currentText(),
|
||||
'notes': self.notes_edit.toPlainText(),
|
||||
'modified': self.modified
|
||||
}
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""关闭事件"""
|
||||
if self.modified:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"保存更改",
|
||||
"记录已被修改,是否保存?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel,
|
||||
QMessageBox.StandardButton.Yes
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
# 保存更改
|
||||
self.accept()
|
||||
elif reply == QMessageBox.StandardButton.Cancel:
|
||||
event.ignore()
|
||||
return
|
||||
else:
|
||||
# 不保存直接关闭
|
||||
event.accept()
|
||||
else:
|
||||
event.accept()
|
||||
@@ -1,282 +0,0 @@
|
||||
"""
|
||||
结果展示组件
|
||||
|
||||
用于展示处理结果,包括:
|
||||
- OCR 文本展示
|
||||
- AI 处理结果展示(纯文本格式)
|
||||
- 一键复制功能
|
||||
- 日志查看
|
||||
"""
|
||||
|
||||
from typing import Optional, Callable
|
||||
import logging
|
||||
|
||||
# 尝试导入 tkinter,失败时使用 PyQt6
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox
|
||||
HAS_TKINTER = True
|
||||
except ImportError:
|
||||
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(QWidget):
|
||||
"""
|
||||
结果展示组件 (PyQt6 版本)
|
||||
|
||||
显示处理结果,支持 Markdown 渲染和一键复制
|
||||
"""
|
||||
|
||||
# 信号:内容改变
|
||||
content_changed = pyqtSignal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
copy_callback: Optional[Callable] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
初始化结果展示组件
|
||||
|
||||
Args:
|
||||
parent: 父容器
|
||||
copy_callback: 复制按钮回调函数
|
||||
**kwargs: 其他参数
|
||||
"""
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self.copy_callback = copy_callback
|
||||
self.current_result: Optional[ProcessResult] = None
|
||||
self.display_mode = "raw" # raw 或 markdown
|
||||
|
||||
self._create_ui()
|
||||
|
||||
def _create_ui(self):
|
||||
"""创建 UI"""
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# 顶部工具栏
|
||||
toolbar_layout = QHBoxLayout()
|
||||
|
||||
# 结果类型选择
|
||||
toolbar_layout.addWidget(QLabel("显示:"))
|
||||
|
||||
from PyQt6.QtWidgets import QRadioButton, QButtonGroup
|
||||
self.mode_group = QButtonGroup()
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
# 右侧按钮
|
||||
self.copy_button = QPushButton("复制")
|
||||
self.copy_button.clicked.connect(self._on_copy)
|
||||
toolbar_layout.addWidget(self.copy_button)
|
||||
|
||||
self.clear_button = QPushButton("清空")
|
||||
self.clear_button.clicked.connect(self._on_clear)
|
||||
toolbar_layout.addWidget(self.clear_button)
|
||||
|
||||
layout.addLayout(toolbar_layout)
|
||||
|
||||
# 主内容区域
|
||||
self.text_widget = QTextEdit()
|
||||
self.text_widget.setReadOnly(True)
|
||||
self.text_widget.setFont(QFont("Consolas", 10))
|
||||
layout.addWidget(self.text_widget)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def _set_mode(self, mode: str):
|
||||
"""设置显示模式"""
|
||||
self.display_mode = mode
|
||||
self._update_result_content()
|
||||
|
||||
def _on_copy(self):
|
||||
"""复制按钮点击"""
|
||||
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:
|
||||
self._update_status("复制失败,请检查是否安装了 pyperclip")
|
||||
|
||||
def _on_clear(self):
|
||||
"""清空按钮点击"""
|
||||
self.text_widget.clear()
|
||||
self.current_result = None
|
||||
self._update_status("已清空")
|
||||
|
||||
def _update_result_content(self):
|
||||
"""更新结果内容"""
|
||||
if not self.current_result:
|
||||
self.text_widget.clear()
|
||||
return
|
||||
|
||||
mode = self.display_mode
|
||||
if mode == "markdown":
|
||||
content = self._get_markdown_content()
|
||||
else:
|
||||
content = self._get_raw_content()
|
||||
|
||||
self.text_widget.setPlainText(content)
|
||||
|
||||
def _get_markdown_content(self) -> str:
|
||||
"""获取 Markdown 格式内容"""
|
||||
if not self.current_result:
|
||||
return ""
|
||||
|
||||
ai_result = self.current_result.ai_result
|
||||
ocr_text = self.current_result.ocr_result.full_text if self.current_result.ocr_result else ""
|
||||
|
||||
return create_markdown_result(ai_result, ocr_text)
|
||||
|
||||
def _get_raw_content(self) -> str:
|
||||
"""获取原始文本内容"""
|
||||
if not self.current_result:
|
||||
return ""
|
||||
|
||||
parts = []
|
||||
|
||||
# OCR 文本
|
||||
if self.current_result.ocr_result:
|
||||
parts.append("## OCR 识别结果\n")
|
||||
parts.append(self.current_result.ocr_result.full_text)
|
||||
parts.append(f"\n\n置信度: {self.current_result.ocr_result.total_confidence:.2%}\n")
|
||||
|
||||
# AI 结果
|
||||
if self.current_result.ai_result:
|
||||
parts.append("\n## AI 处理结果\n")
|
||||
parts.append(f"分类: {self.current_result.ai_result.category.value}\n")
|
||||
parts.append(f"置信度: {self.current_result.ai_result.confidence:.2%}\n")
|
||||
parts.append(f"标题: {self.current_result.ai_result.title}\n")
|
||||
parts.append(f"标签: {', '.join(self.current_result.ai_result.tags)}\n")
|
||||
parts.append(f"\n内容:\n{self.current_result.ai_result.content}\n")
|
||||
|
||||
# 处理信息
|
||||
parts.append("\n## 处理信息\n")
|
||||
parts.append(f"成功: {'是' if self.current_result.success else '否'}\n")
|
||||
parts.append(f"耗时: {self.current_result.process_time:.2f}秒\n")
|
||||
parts.append(f"已完成的步骤: {', '.join(self.current_result.steps_completed)}\n")
|
||||
|
||||
if self.current_result.warnings:
|
||||
parts.append(f"\n警告:\n")
|
||||
for warning in self.current_result.warnings:
|
||||
parts.append(f" - {warning}\n")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
def _update_status(self, message: str):
|
||||
"""更新状态"""
|
||||
# 这里可以发出信号让父窗口更新状态
|
||||
pass
|
||||
|
||||
def set_result(self, result: ProcessResult):
|
||||
"""
|
||||
设置处理结果并显示
|
||||
|
||||
Args:
|
||||
result: 处理结果
|
||||
"""
|
||||
self.current_result = result
|
||||
self._update_result_content()
|
||||
|
||||
# 更新状态
|
||||
if result.success:
|
||||
status = f"处理成功 | 耗时 {result.process_time:.2f}秒"
|
||||
else:
|
||||
status = f"处理失败: {result.error_message or '未知错误'}"
|
||||
|
||||
self._update_status(status)
|
||||
|
||||
def append_log(self, level: str, message: str):
|
||||
"""添加日志"""
|
||||
# 简化版本:直接输出到控制台
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
print(f"[{timestamp}] [{level}] {message}")
|
||||
|
||||
|
||||
class QuickResultDialog:
|
||||
"""
|
||||
快速结果显示对话框 (PyQt6 版本)
|
||||
|
||||
用于快速显示处理结果,不集成到主界面
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
result: ProcessResult,
|
||||
on_close: Optional[Callable] = None
|
||||
):
|
||||
"""
|
||||
初始化对话框
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
result: 处理结果
|
||||
on_close: 关闭回调
|
||||
"""
|
||||
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QHBoxLayout, QLabel
|
||||
|
||||
super(QDialog, self).__init__(parent)
|
||||
self.result = result
|
||||
self.on_close = on_close
|
||||
|
||||
self.setWindowTitle("处理结果")
|
||||
self.resize(600, 400)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# 显示结果
|
||||
result_widget = ResultWidget(self)
|
||||
result_widget.set_result(result)
|
||||
layout.addWidget(result_widget)
|
||||
|
||||
# 底部按钮
|
||||
button_layout = QHBoxLayout()
|
||||
close_btn = QPushButton("关闭")
|
||||
close_btn.clicked.connect(self._on_close)
|
||||
button_layout.addWidget(close_btn)
|
||||
button_layout.addStretch()
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
self.setLayout(layout)
|
||||
|
||||
def _on_close(self):
|
||||
"""关闭对话框"""
|
||||
if self.on_close:
|
||||
self.on_close()
|
||||
self.accept()
|
||||
@@ -1,368 +0,0 @@
|
||||
"""
|
||||
截图窗口组件
|
||||
|
||||
实现全屏截图功能,包括:
|
||||
- 全屏透明覆盖窗口
|
||||
- 区域选择
|
||||
- 截图预览
|
||||
- 保存和取消操作
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QLabel, QApplication, QMessageBox
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QRect, QPoint, QSize, pyqtSignal
|
||||
from PyQt6.QtGui import (
|
||||
QPixmap, QPainter, QPen, QColor, QScreen,
|
||||
QCursor, QGuiApplication
|
||||
)
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
|
||||
class ScreenshotOverlay(QWidget):
|
||||
"""
|
||||
全屏截图覆盖窗口
|
||||
|
||||
提供全屏透明的截图区域选择界面
|
||||
"""
|
||||
|
||||
# 信号:截图完成时发出,传递图片和截图区域
|
||||
screenshot_taken = pyqtSignal(QPixmap, QRect)
|
||||
# 信号:取消截图
|
||||
screenshot_cancelled = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""初始化截图覆盖窗口"""
|
||||
super().__init__(parent)
|
||||
|
||||
# 设置窗口标志:无边框、置顶、全屏
|
||||
self.setWindowFlags(
|
||||
Qt.WindowType.FramelessWindowHint |
|
||||
Qt.WindowType.WindowStaysOnTopHint |
|
||||
Qt.WindowType.Tool
|
||||
)
|
||||
|
||||
# 设置半透明背景
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
||||
|
||||
# 状态变量
|
||||
self.is_capturing = False
|
||||
self.is_dragging = False
|
||||
self.start_pos = QPoint()
|
||||
self.end_pos = QPoint()
|
||||
self.current_rect = QRect()
|
||||
|
||||
# 获取屏幕截图
|
||||
self.screen_pixmap = self._capture_screen()
|
||||
|
||||
# 初始化UI
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui(self):
|
||||
"""初始化UI"""
|
||||
# 设置全屏
|
||||
screen = QApplication.primaryScreen()
|
||||
if screen:
|
||||
screen_geometry = screen.availableGeometry()
|
||||
self.setGeometry(screen_geometry)
|
||||
|
||||
# 创建工具栏
|
||||
self._create_toolbar()
|
||||
|
||||
def _create_toolbar(self):
|
||||
"""创建底部工具栏"""
|
||||
self.toolbar = QWidget(self)
|
||||
self.toolbar.setObjectName("screenshotToolbar")
|
||||
|
||||
toolbar_layout = QHBoxLayout(self.toolbar)
|
||||
toolbar_layout.setContentsMargins(16, 8, 16, 8)
|
||||
toolbar_layout.setSpacing(12)
|
||||
|
||||
# 完成按钮
|
||||
self.finish_btn = QPushButton("✓ 完成")
|
||||
self.finish_btn.setObjectName("screenshotButton")
|
||||
self.finish_btn.setMinimumSize(80, 36)
|
||||
self.finish_btn.clicked.connect(self._on_finish)
|
||||
self.finish_btn.setEnabled(False)
|
||||
toolbar_layout.addWidget(self.finish_btn)
|
||||
|
||||
# 取消按钮
|
||||
self.cancel_btn = QPushButton("✕ 取消")
|
||||
self.cancel_btn.setObjectName("screenshotButton")
|
||||
self.cancel_btn.setMinimumSize(80, 36)
|
||||
self.cancel_btn.clicked.connect(self._on_cancel)
|
||||
toolbar_layout.addWidget(self.cancel_btn)
|
||||
|
||||
# 设置工具栏样式
|
||||
self.toolbar.setStyleSheet("""
|
||||
QWidget#screenshotToolbar {
|
||||
background-color: rgba(40, 40, 40, 230);
|
||||
border-radius: 8px;
|
||||
}
|
||||
QPushButton#screenshotButton {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton#screenshotButton:hover {
|
||||
background-color: #357ABD;
|
||||
}
|
||||
QPushButton#screenshotButton:pressed {
|
||||
background-color: #2A639D;
|
||||
}
|
||||
QPushButton#screenshotButton:disabled {
|
||||
background-color: #CCCCCC;
|
||||
color: #666666;
|
||||
}
|
||||
""")
|
||||
|
||||
# 初始隐藏工具栏
|
||||
self.toolbar.hide()
|
||||
|
||||
def _capture_screen(self) -> QPixmap:
|
||||
"""
|
||||
捕获屏幕截图
|
||||
|
||||
Returns:
|
||||
屏幕截图的 QPixmap
|
||||
"""
|
||||
screen = QApplication.primaryScreen()
|
||||
if screen:
|
||||
return screen.grabWindow(0) # 0 = 整个屏幕
|
||||
return QPixmap()
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""绘制事件"""
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# 1. 绘制半透明黑色背景
|
||||
painter.fillRect(self.rect(), QColor(0, 0, 0, 100))
|
||||
|
||||
# 2. 如果有选择区域,绘制选区
|
||||
if self.is_capturing and not self.current_rect.isEmpty():
|
||||
# 清除选区背景(显示原始屏幕内容)
|
||||
painter.drawPixmap(
|
||||
self.current_rect.topLeft(),
|
||||
self.screen_pixmap,
|
||||
self.current_rect
|
||||
)
|
||||
|
||||
# 绘制选区边框
|
||||
pen = QPen(QColor(74, 144, 226), 2)
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(self.current_rect)
|
||||
|
||||
# 绘制尺寸信息
|
||||
size_text = f"{self.current_rect.width()} x {self.current_rect.height()}"
|
||||
painter.setPen(QColor(255, 255, 255))
|
||||
painter.drawText(
|
||||
self.current_rect.x(),
|
||||
self.current_rect.y() - 10,
|
||||
size_text
|
||||
)
|
||||
|
||||
# 更新工具栏位置(在选区下方中央)
|
||||
toolbar_width = 200
|
||||
toolbar_height = 52
|
||||
x = self.current_rect.center().x() - toolbar_width // 2
|
||||
y = self.current_rect.bottom() + 10
|
||||
|
||||
# 确保工具栏在屏幕内
|
||||
if y + toolbar_height > self.height():
|
||||
y = self.current_rect.top() - toolbar_height - 10
|
||||
|
||||
self.toolbar.setGeometry(x, y, toolbar_width, toolbar_height)
|
||||
self.toolbar.show()
|
||||
else:
|
||||
self.toolbar.hide()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""鼠标按下事件"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.is_dragging = True
|
||||
self.start_pos = event.pos()
|
||||
self.end_pos = event.pos()
|
||||
self.is_capturing = True
|
||||
self.finish_btn.setEnabled(False)
|
||||
self.update()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""鼠标移动事件"""
|
||||
if self.is_dragging:
|
||||
self.end_pos = event.pos()
|
||||
|
||||
# 计算选择区域
|
||||
x = min(self.start_pos.x(), self.end_pos.x())
|
||||
y = min(self.start_pos.y(), self.end_pos.y())
|
||||
width = abs(self.end_pos.x() - self.start_pos.x())
|
||||
height = abs(self.end_pos.y() - self.start_pos.y())
|
||||
|
||||
self.current_rect = QRect(x, y, width, height)
|
||||
|
||||
# 只有当区域足够大时才启用完成按钮
|
||||
self.finish_btn.setEnabled(width > 10 and height > 10)
|
||||
|
||||
self.update()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""鼠标释放事件"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.is_dragging = False
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""键盘事件"""
|
||||
# ESC 键取消截图
|
||||
if event.key() == Qt.Key.Key_Escape:
|
||||
self._on_cancel()
|
||||
# Enter 键完成截图
|
||||
elif event.key() == Qt.Key.Key_Return:
|
||||
if self.finish_btn.isEnabled():
|
||||
self._on_finish()
|
||||
|
||||
def _on_finish(self):
|
||||
"""完成截图"""
|
||||
if not self.current_rect.isEmpty():
|
||||
# 从屏幕截图中裁剪选区
|
||||
screenshot = self.screen_pixmap.copy(self.current_rect)
|
||||
self.screenshot_taken.emit(screenshot, self.current_rect)
|
||||
self.close()
|
||||
|
||||
def _on_cancel(self):
|
||||
"""取消截图"""
|
||||
self.screenshot_cancelled.emit()
|
||||
self.close()
|
||||
|
||||
def show_screenshot(self):
|
||||
"""显示截图窗口"""
|
||||
self.showFullScreen()
|
||||
# 设置鼠标为十字准星
|
||||
self.setCursor(Qt.CursorShape.CrossCursor)
|
||||
|
||||
|
||||
class ScreenshotWidget(QWidget):
|
||||
"""
|
||||
截图管理组件
|
||||
|
||||
提供完整的截图功能,包括触发、处理和保存
|
||||
"""
|
||||
|
||||
# 信号:截图完成,传递图片路径
|
||||
screenshot_saved = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""初始化截图组件"""
|
||||
super().__init__(parent)
|
||||
|
||||
# 截图覆盖窗口
|
||||
self.overlay = None
|
||||
|
||||
# 临时保存目录
|
||||
self.temp_dir = Path(tempfile.gettempdir()) / "cutthenthink" / "screenshots"
|
||||
self.temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def take_screenshot(self):
|
||||
"""触发截图"""
|
||||
# 创建并显示截图覆盖窗口
|
||||
self.overlay = ScreenshotOverlay()
|
||||
self.overlay.screenshot_taken.connect(self._on_screenshot_taken)
|
||||
self.overlay.screenshot_cancelled.connect(self._on_screenshot_cancelled)
|
||||
self.overlay.show_screenshot()
|
||||
|
||||
def _on_screenshot_taken(self, pixmap: QPixmap, rect: QRect):
|
||||
"""
|
||||
截图完成的回调
|
||||
|
||||
Args:
|
||||
pixmap: 截图图片
|
||||
rect: 截图区域
|
||||
"""
|
||||
# 保存截图到临时目录
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"screenshot_{timestamp}.png"
|
||||
filepath = self.temp_dir / filename
|
||||
|
||||
if pixmap.save(str(filepath)):
|
||||
self.screenshot_saved.emit(str(filepath))
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"保存失败",
|
||||
f"无法保存截图到:{filepath}"
|
||||
)
|
||||
|
||||
def _on_screenshot_cancelled(self):
|
||||
"""截图取消的回调"""
|
||||
# 可选:显示提示或执行其他操作
|
||||
pass
|
||||
|
||||
def take_and_save_screenshot(self, save_path: str = None) -> str:
|
||||
"""
|
||||
截图并保存到指定路径(同步版本,阻塞等待)
|
||||
|
||||
Args:
|
||||
save_path: 保存路径,为 None 时使用默认路径
|
||||
|
||||
Returns:
|
||||
保存的文件路径,失败返回 None
|
||||
"""
|
||||
# 这个版本需要使用事件循环同步等待
|
||||
# 由于 PyQt 的事件机制,建议使用信号方式
|
||||
# 这里提供一个简单的实现供测试使用
|
||||
import asyncio
|
||||
|
||||
future = asyncio.Future()
|
||||
|
||||
def on_saved(path):
|
||||
future.set_result(path)
|
||||
|
||||
self.screenshot_saved.connect(on_saved)
|
||||
self.take_screenshot()
|
||||
|
||||
# 注意:实际使用时建议在异步上下文中调用
|
||||
return None
|
||||
|
||||
|
||||
class QuickScreenshotHelper:
|
||||
"""
|
||||
快速截图助手类
|
||||
|
||||
用于全局快捷键触发截图
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_screenshot_widget = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""获取单例实例"""
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def trigger_screenshot(cls):
|
||||
"""触发截图(可被全局快捷键调用)"""
|
||||
instance = cls.get_instance()
|
||||
if instance._screenshot_widget is None:
|
||||
instance._screenshot_widget = ScreenshotWidget()
|
||||
instance._screenshot_widget.take_screenshot()
|
||||
|
||||
@classmethod
|
||||
def set_screenshot_widget(cls, widget: ScreenshotWidget):
|
||||
"""设置截图组件实例"""
|
||||
instance = cls.get_instance()
|
||||
instance._screenshot_widget = widget
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def take_screenshot():
|
||||
"""触发截图的便捷函数"""
|
||||
QuickScreenshotHelper.trigger_screenshot()
|
||||
Reference in New Issue
Block a user