diff --git a/build_macos_app.py b/build_macos_app.py new file mode 100644 index 0000000..c0ff9f8 --- /dev/null +++ b/build_macos_app.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +MagicWord macOS M系列芯片打包脚本 +用于构建macOS Apple Silicon原生应用程序 +""" + +import os +import sys +import subprocess +import platform +import shutil +import plistlib +from datetime import datetime + +def run_command(command, shell=False, cwd=None): + """运行命令并返回结果""" + try: + result = subprocess.run(command, shell=shell, capture_output=True, text=True, encoding='utf-8', cwd=cwd) + return result.returncode, result.stdout, result.stderr + except Exception as e: + return -1, "", str(e) + +def check_system(): + """检查系统是否为macOS Apple Silicon""" + if platform.system() != "Darwin": + print("错误: 此脚本仅支持macOS系统") + return False + + # 检查是否为Apple Silicon + machine = platform.machine() + if machine not in ['arm64', 'aarch64']: + print(f"警告: 当前为 {machine} 架构,建议Apple Silicon (arm64) 以获得最佳性能") + + print(f"系统信息: macOS {platform.mac_ver()[0]}, {machine}") + return True + +def clean_build_dirs(): + """清理构建目录""" + print("清理构建目录...") + dirs_to_clean = ['build', 'dist', '__pycache__', '*.egg-info'] + + for dir_name in dirs_to_clean: + if '*' in dir_name: + import glob + for path in glob.glob(dir_name): + if os.path.isdir(path): + shutil.rmtree(path, ignore_errors=True) + elif os.path.exists(dir_name): + if os.path.isdir(dir_name): + shutil.rmtree(dir_name, ignore_errors=True) + else: + os.remove(dir_name) + + # 清理src目录下的__pycache__ + for root, dirs, files in os.walk('src'): + for dir_name in dirs: + if dir_name == '__pycache__': + cache_path = os.path.join(root, dir_name) + shutil.rmtree(cache_path, ignore_errors=True) + print(f"清理缓存: {cache_path}") + +def install_dependencies(): + """安装依赖""" + print("安装项目依赖...") + + # 首先确保pip是最新的 + run_command([sys.executable, "-m", "pip", "install", "--upgrade", "pip"]) + + # 安装requirements.txt中的依赖 + code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]) + if code != 0: + print(f"依赖安装失败: {stderr}") + return False + + # 安装PyInstaller + print("安装PyInstaller...") + code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "pyinstaller"]) + if code != 0: + print(f"PyInstaller安装失败: {stderr}") + return False + + print("依赖安装成功") + return True + +def build_macos_app(): + """构建macOS应用包""" + print("构建macOS应用包...") + + # PyInstaller命令 - 针对macOS优化 + pyinstaller_cmd = [ + "pyinstaller", + "--name", "MagicWord", + "--version", "0.3.0", + "--distpath", "dist", + "--workpath", "build", + "--specpath", ".", + # macOS特定的数据文件路径格式 + "--add-data", "resources:resources", + "--add-data", "src:src", + # 隐藏导入模块 + "--hidden-import", "PyQt5", + "--hidden-import", "PyQt5.QtCore", + "--hidden-import", "PyQt5.QtGui", + "--hidden-import", "PyQt5.QtWidgets", + "--hidden-import", "requests", + "--hidden-import", "beautifulsoup4", + "--hidden-import", "python-docx", + "--hidden-import", "PyPDF2", + "--hidden-import", "ebooklib", + "--hidden-import", "chardet", + "--hidden-import", "PIL", + # macOS应用包选项 + "--windowed", # 无控制台窗口 + "--osx-bundle-identifier", "com.magicword.app", + "--target-architecture", "arm64", # Apple Silicon + "--noconfirm", + "src/main.py" + ] + + print("运行PyInstaller...") + code, stdout, stderr = run_command(pyinstaller_cmd) + + if code != 0: + print(f"构建失败: {stderr}") + print("尝试通用架构...") + # 尝试通用架构 + pyinstaller_cmd[-2] = "--target-architecture" + pyinstaller_cmd[-1] = "universal2" + code, stdout, stderr = run_command(pyinstaller_cmd) + if code != 0: + print(f"通用架构构建也失败: {stderr}") + return False + + print("macOS应用包构建成功") + return True + +def create_app_bundle(): + """创建macOS应用束""" + print("创建macOS应用束...") + + app_path = "dist/MagicWord.app" + if not os.path.exists(app_path): + print(f"错误: 找不到应用包 {app_path}") + return False + + # 创建Info.plist文件 + info_plist = { + 'CFBundleName': 'MagicWord', + 'CFBundleDisplayName': 'MagicWord - 隐私学习软件', + 'CFBundleIdentifier': 'com.magicword.app', + 'CFBundleVersion': '0.3.0', + 'CFBundleShortVersionString': '0.3.0', + 'CFBundleExecutable': 'MagicWord', + 'CFBundlePackageType': 'APPL', + 'CFBundleSignature': '????', + 'LSMinimumSystemVersion': '11.0', # macOS Big Sur及更高版本 + 'NSHighResolutionCapable': True, + 'NSHumanReadableCopyright': 'Copyright © 2024 MagicWord Team. All rights reserved.', + 'CFBundleDocumentTypes': [ + { + 'CFBundleTypeName': 'Text Document', + 'CFBundleTypeExtensions': ['txt', 'docx', 'pdf'], + 'CFBundleTypeRole': 'Editor' + } + ] + } + + plist_path = os.path.join(app_path, "Contents", "Info.plist") + with open(plist_path, 'wb') as f: + plistlib.dump(info_plist, f) + + # 复制图标文件 + icon_files = [ + 'resources/icons/app_icon_128X128.png', + 'resources/icons/app_icon_256X256.png', + 'resources/icons/app_icon_32X32.png', + 'resources/icons/app_icon_64X64.png' + ] + + resources_dir = os.path.join(app_path, "Contents", "Resources") + os.makedirs(resources_dir, exist_ok=True) + + for icon_file in icon_files: + if os.path.exists(icon_file): + shutil.copy2(icon_file, resources_dir) + print(f"复制图标: {icon_file}") + + print("macOS应用束创建完成") + return True + +def create_dmg(): + """创建DMG安装包""" + print("创建DMG安装包...") + + app_path = "dist/MagicWord.app" + if not os.path.exists(app_path): + print(f"错误: 找不到应用包 {app_path}") + return False + + # 创建发布目录 + release_dir = "macos_release" + if os.path.exists(release_dir): + shutil.rmtree(release_dir) + os.makedirs(release_dir) + + # 复制应用到发布目录 + release_app_path = os.path.join(release_dir, "MagicWord.app") + shutil.copytree(app_path, release_app_path) + + # 创建应用程序链接 + applications_link = os.path.join(release_dir, "Applications") + os.symlink("/Applications", applications_link) + + # 创建README文件 + readme_content = f"""# MagicWord 0.3.0 for macOS + +## 安装说明 + +1. 将 MagicWord.app 拖拽到 Applications 文件夹 +2. 首次运行时,如果出现安全提示,请前往 系统设置 > 隐私与安全性 允许应用运行 +3. 或者右键点击应用选择"打开" + +## 系统要求 + +- macOS Big Sur (11.0) 或更高版本 +- Apple Silicon (M1/M2/M3) 或 Intel 处理器 + +## 功能特性 + +- 隐私学习:通过打字练习来学习文档内容 +- 支持多种文档格式:TXT, DOCX, PDF +- 智能打字模式 +- 美观的Word风格界面 + +## 版本信息 + +构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +平台: {platform.system()} {platform.machine()} +Python版本: {platform.python_version()} + +## 技术支持 + +如有问题,请查看项目文档或联系开发团队。 +""" + + with open(os.path.join(release_dir, "README.txt"), "w") as f: + f.write(readme_content) + + # 创建DMG文件(如果系统支持) + dmg_name = f"MagicWord-0.3.0-macOS-{platform.machine()}.dmg" + dmg_path = os.path.join("dist", dmg_name) + + # 使用hdiutil创建DMG + create_dmg_cmd = [ + "hdiutil", "create", + "-volname", "MagicWord", + "-srcfolder", release_dir, + "-ov", + "-format", "UDZO", + dmg_path + ] + + code, stdout, stderr = run_command(create_dmg_cmd) + if code == 0: + print(f"DMG创建成功: {dmg_path}") + return True + else: + print(f"DMG创建失败: {stderr}") + print("已创建应用包,可手动打包DMG") + return False + +def main(): + """主函数""" + print("=== MagicWord macOS打包脚本 ===") + print(f"构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + # 检查系统 + if not check_system(): + return False + + # 清理构建目录 + clean_build_dirs() + + # 安装依赖 + if not install_dependencies(): + print("依赖安装失败") + return False + + # 构建应用 + if not build_macos_app(): + print("应用构建失败") + return False + + # 创建应用束 + if not create_app_bundle(): + print("应用束创建失败") + return False + + # 创建DMG + create_dmg() + + print("\n=== 构建完成 ===") + print("应用位置: dist/MagicWord.app") + print("如需安装,请将应用拖拽到Applications文件夹") + print("首次运行时可能需要允许未知来源的应用") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/build_simple.py b/build_simple.py new file mode 100644 index 0000000..4c7bd88 --- /dev/null +++ b/build_simple.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +MagicWord macOS简化打包脚本 +""" + +import os +import sys +import subprocess +import shutil + +def run_command(cmd): + """运行命令""" + print(f"运行: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"错误: {result.stderr}") + return False + return True + +def main(): + print("=== MagicWord macOS打包 ===") + + # 清理旧的构建文件 + print("清理构建文件...") + for folder in ['build', 'dist', '__pycache__']: + if os.path.exists(folder): + shutil.rmtree(folder) + + # 安装依赖 + print("安装依赖...") + if not run_command([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]): + return + + if not run_command([sys.executable, "-m", "pip", "install", "pyinstaller"]): + return + + # 构建命令 - 包含所有必要的图片和图标文件 + cmd = [ + sys.executable, "-m", "PyInstaller", + "--name", "MagicWord", + "--distpath", "dist", + "--workpath", "build", + # 资源文件 + "--add-data", "resources:resources", + # UI图片文件 + "--add-data", "src/ui/UI.png:ui", + "--add-data", "src/ui/114514.png:ui", + # 图标文件 + "--add-data", "resources/icons/app_icon_32X32.png:resources/icons", + "--add-data", "resources/icons/app_icon_64X64.png:resources/icons", + "--add-data", "resources/icons/app_icon_128X128.png:resources/icons", + "--add-data", "resources/icons/app_icon_256X256.png:resources/icons", + # 隐藏导入模块 + "--hidden-import", "PyQt5", + "--hidden-import", "PyQt5.QtCore", + "--hidden-import", "PyQt5.QtGui", + "--hidden-import", "PyQt5.QtWidgets", + "--hidden-import", "requests", + "--hidden-import", "beautifulsoup4", + "--hidden-import", "python-docx", + "--hidden-import", "PyPDF2", + "--hidden-import", "ebooklib", + "--hidden-import", "chardet", + "--hidden-import", "PIL", + # macOS应用选项 + "--windowed", + "--osx-bundle-identifier", "com.magicword.app", + "--icon", "resources/icons/app_icon_128X128.png", + "src/main.py" + ] + + print("构建应用...") + if run_command(cmd): + print("✅ 构建成功!") + print(f"应用位置: {os.path.abspath('dist/MagicWord.app')}") + print("\n安装步骤:") + print("1. 将 MagicWord.app 拖拽到 Applications 文件夹") + print("2. 首次运行时,右键点击应用选择'打开'") + else: + print("❌ 构建失败!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/quick_build_macos.sh b/quick_build_macos.sh new file mode 100755 index 0000000..bf8b1db --- /dev/null +++ b/quick_build_macos.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# MagicWord macOS快速打包脚本 + +echo "=== MagicWord macOS打包脚本 ===" +echo "开始打包进程..." + +# 检查Python版本 +python_version=$(python3 --version 2>/dev/null || python --version) +echo "Python版本: $python_version" + +# 安装依赖 +echo "安装依赖..." +pip3 install -r requirements.txt +pip3 install pyinstaller + +# 清理旧的构建文件 +echo "清理旧的构建文件..." +rm -rf build dist __pycache__ *.spec +find src -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true + +# 构建应用 +echo "构建macOS应用..." +python3 -m pyinstaller \\ + --name "MagicWord" \\ + --distpath "dist" \\ + --workpath "build" \\ + --add-data "resources:resources" \\ + --hidden-import "PyQt5" \\ + --hidden-import "PyQt5.QtCore" \\ + --hidden-import "PyQt5.QtGui" \\ + --hidden-import "PyQt5.QtWidgets" \\ + --hidden-import "requests" \\ + --hidden-import "beautifulsoup4" \\ + --hidden-import "python-docx" \\ + --hidden-import "PyPDF2" \\ + --hidden-import "ebooklib" \\ + --hidden-import "chardet" \\ + --hidden-import "PIL" \\ + --windowed \\ + --osx-bundle-identifier "com.magicword.app" \\ + --target-architecture arm64 \\ + --noconfirm \\ + src/main.py + +# 检查构建结果 +if [ -d "dist/MagicWord.app" ]; then + echo "✅ 构建成功!" + echo "应用位置: dist/MagicWord.app" + echo "" + echo "安装步骤:" + echo "1. 将 MagicWord.app 拖拽到 Applications 文件夹" + echo "2. 首次运行时,右键点击应用选择'打开'" + echo "3. 或者在 系统设置 > 隐私与安全性 中允许应用运行" +else + echo "❌ 构建失败!" + exit 1 +fi + +# 可选:创建DMG +read -p "是否创建DMG安装包?(y/n): " create_dmg +if [[ $create_dmg =~ ^[Yy]$ ]]; then + echo "创建DMG安装包..." + + # 创建发布目录 + release_dir="macos_release" + rm -rf "$release_dir" + mkdir -p "$release_dir" + + # 复制应用 + cp -r "dist/MagicWord.app" "$release_dir/" + + # 创建Applications链接 + ln -s "/Applications" "$release_dir/Applications" + + # 创建README + cat > "$release_dir/README.txt" << 'EOF' +MagicWord for macOS + +安装说明: +1. 将 MagicWord.app 拖拽到 Applications 文件夹 +2. 首次运行时,右键点击应用选择"打开" +3. 或者在 系统设置 > 隐私与安全性 中允许应用运行 + +系统要求: +- macOS Big Sur (11.0) 或更高版本 +- Apple Silicon (M1/M2/M3) 推荐 + +功能特性: +- 隐私学习:通过打字练习来学习文档内容 +- 支持多种文档格式:TXT, DOCX, PDF +- 智能打字模式 +- 美观的Word风格界面 +EOF + + # 创建DMG + dmg_name="MagicWord-0.3.0-macOS-arm64.dmg" + hdiutil create -volname "MagicWord" -srcfolder "$release_dir" -ov -format UDZO "$dmg_name" + + if [ -f "$dmg_name" ]; then + echo "✅ DMG创建成功: $dmg_name" + else + echo "⚠️ DMG创建失败,但应用包已准备就绪" + fi +fi + +echo "=== 打包完成 ===" \ No newline at end of file diff --git a/src/learning_mode_window.py b/src/learning_mode_window.py new file mode 100644 index 0000000..1ec350b --- /dev/null +++ b/src/learning_mode_window.py @@ -0,0 +1,434 @@ +# learning_mode_window.py +import sys +import os +from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QTextEdit, QLabel, QFrame, QMenuBar, + QAction, QFileDialog, QMessageBox, QApplication, + QSplitter, QScrollArea, QStatusBar, QProgressBar) +from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect +from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor + +from src.ui.components import CustomTitleBar, TextDisplayWidget +from src.typing_logic import TypingLogic +from src.file_parser import FileParser +from src.ui.theme_manager import theme_manager + +class LearningModeWindow(QMainWindow): + def __init__(self, parent=None, imported_content="", current_position=0): + """ + 学习模式窗口 + - 顶部显示UI.png图片 + - 下方显示输入字符页面 + - 输入字符显示导入的文件内容 + + 参数: + parent: 父窗口 + imported_content: 从主窗口传递的导入内容 + current_position: 当前学习进度位置 + """ + super().__init__(parent) + self.parent_window = parent + self.imported_content = imported_content + self.current_position = current_position + self.typing_logic = None + self.is_loading_file = False + + # 初始化UI + self.initUI() + + # 初始化打字逻辑 + self.init_typing_logic() + + # 如果有导入内容,初始化显示 + if self.imported_content: + self.initialize_with_imported_content() + + def initialize_with_imported_content(self): + """ + 使用从主窗口传递的导入内容初始化学习模式窗口 + """ + if not self.imported_content: + return + + # 重置打字逻辑 + if self.typing_logic: + self.typing_logic.reset(self.imported_content) + + # 显示已学习的内容 + display_text = self.imported_content[:self.current_position] + self.text_display_widget.text_display.setPlainText(display_text) + + # 更新状态 + progress = (self.current_position / len(self.imported_content)) * 100 + self.status_label.setText(f"继续学习 - 进度: {self.current_position}/{len(self.imported_content)} 字符") + self.progress_label.setText(f"进度: {progress:.1f}%") + + # 设置光标位置到文本末尾 + cursor = self.text_display_widget.text_display.textCursor() + cursor.movePosition(cursor.End) + self.text_display_widget.text_display.setTextCursor(cursor) + + def initUI(self): + """ + 初始化学习模式窗口UI + - 设置窗口标题和大小 + - 创建顶部图片区域 + - 创建下方输入区域 + """ + # 设置窗口属性 + self.setWindowTitle("学习模式 - MagicWord") + self.setGeometry(200, 200, 900, 700) + self.setWindowFlags(Qt.Window) # 独立窗口 + + # 创建中央widget + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # 创建主布局 + main_layout = QVBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + central_widget.setLayout(main_layout) + + # 创建顶部图片区域 + self.create_top_image_area(main_layout) + + # 创建分隔线 + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Sunken) + separator.setStyleSheet("background-color: #d0d0d0;") + main_layout.addWidget(separator) + + # 创建输入区域 + self.create_input_area(main_layout) + + # 创建菜单栏 + self.create_menu_bar() + + # 创建状态栏 + self.create_status_bar() + + # 如果有导入内容,更新状态栏显示 + if self.imported_content: + progress = (self.current_position / len(self.imported_content)) * 100 + self.status_label.setText(f"继续学习 - 进度: {self.current_position}/{len(self.imported_content)} 字符") + self.progress_label.setText(f"进度: {progress:.1f}%") + + def create_top_image_area(self, main_layout): + """ + 创建顶部图片区域 + - 加载并显示UI.png图片,完全铺满窗口上方 + - 窗口大小自动适配图片大小 + """ + # 创建图片标签 + self.image_label = QLabel() + self.image_label.setAlignment(Qt.AlignCenter) + self.image_label.setStyleSheet(""" + QLabel { + background-color: #f8f9fa; + border: none; + margin: 0px; + padding: 0px; + } + """) + + # 加载UI.png图片 + ui_image_path = os.path.join(os.path.dirname(__file__), 'ui', 'UI.png') + if os.path.exists(ui_image_path): + pixmap = QPixmap(ui_image_path) + + # 保存原始图片尺寸 + self.original_pixmap = pixmap + + # 设置图片完全铺满标签 + self.image_label.setPixmap(pixmap) + self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签 + + # 设置图片标签的尺寸策略,使其可以扩展 + from PyQt5.QtWidgets import QSizePolicy + self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # 设置图片区域的最小高度为图片高度的1/3,确保图片可见 + min_height = max(200, pixmap.height() // 3) + self.image_label.setMinimumHeight(min_height) + + # 重新设置窗口大小以适配图片 + self.resize(pixmap.width(), self.height()) + else: + self.image_label.setText("UI图片未找到") + self.image_label.setStyleSheet(""" + QLabel { + background-color: #f8f9fa; + border: none; + color: #666666; + font-size: 14px; + padding: 20px; + } + """) + self.image_label.setMinimumHeight(200) + + # 直接添加图片标签到主布局,不使用滚动区域 + main_layout.addWidget(self.image_label) + + def resizeEvent(self, event): + """ + 窗口大小改变事件 + - 动态调整图片显示区域 + """ + super().resizeEvent(event) + + # 获取窗口宽度 + window_width = self.width() + + # 如果图片标签存在,调整其大小以适配窗口 + if hasattr(self, 'image_label') and self.image_label: + # 设置图片标签的固定宽度为窗口宽度 + self.image_label.setFixedWidth(window_width) + + # 根据窗口宽度计算合适的高度(保持图片比例) + if hasattr(self, 'original_pixmap') and self.original_pixmap: + original_width = self.original_pixmap.width() + original_height = self.original_pixmap.height() + + # 计算保持比例的高度 + new_height = int(window_width * original_height / original_width) + self.image_label.setFixedHeight(new_height) + else: + # 如果没有原始图片,使用默认高度 + self.image_label.setFixedHeight(300) + + def create_input_area(self, main_layout): + """ + 创建输入区域 + - 创建文本显示组件 + - 设置与主系统相同的样式 + """ + # 创建文本显示组件(复用主系统的组件) + self.text_display_widget = TextDisplayWidget(self) + + # 设置样式,使其与主系统保持一致 + self.text_display_widget.setStyleSheet(""" + TextDisplayWidget { + background-color: white; + border: 1px solid #d0d0d0; + border-radius: 0px; + } + """) + + # 连接文本变化信号 + self.text_display_widget.text_display.textChanged.connect(self.on_text_changed) + + main_layout.addWidget(self.text_display_widget, 1) # 占据剩余空间 + + def create_menu_bar(self): + """ + 创建菜单栏 + - 文件菜单:导入、退出 + - 帮助菜单:关于 + """ + menu_bar = self.menuBar() + + # 文件菜单 + file_menu = menu_bar.addMenu('文件') + + # 导入动作 + import_action = QAction('导入文件', self) + import_action.setShortcut('Ctrl+O') + import_action.triggered.connect(self.import_file) + file_menu.addAction(import_action) + + file_menu.addSeparator() + + # 退出动作 + exit_action = QAction('退出学习模式', self) + exit_action.setShortcut('Ctrl+Q') + exit_action.triggered.connect(self.close) + file_menu.addAction(exit_action) + + # 帮助菜单 + help_menu = menu_bar.addMenu('帮助') + + about_action = QAction('关于', self) + about_action.triggered.connect(self.show_about) + help_menu.addAction(about_action) + + def create_status_bar(self): + """ + 创建状态栏 + - 显示当前状态和学习进度 + """ + self.status_bar = self.statusBar() + + # 创建状态标签 + self.status_label = QLabel("就绪 - 请先导入文件开始学习") + self.progress_label = QLabel("进度: 0%") + + # 添加到状态栏 + self.status_bar.addWidget(self.status_label) + self.status_bar.addPermanentWidget(self.progress_label) + + def init_typing_logic(self): + """ + 初始化打字逻辑 + - 创建打字逻辑实例 + - 设置默认内容 + """ + # 创建打字逻辑实例 + self.typing_logic = TypingLogic("欢迎使用学习模式!\n\n请先导入文件开始打字学习。") + + # 设置文本显示组件的打字逻辑 + if hasattr(self.text_display_widget, 'set_typing_logic'): + self.text_display_widget.set_typing_logic(self.typing_logic) + + def import_file(self): + """ + 导入文件 + - 打开文件对话框选择文件 + - 解析文件内容 + - 重置打字逻辑 + """ + file_path, _ = QFileDialog.getOpenFileName( + self, "导入学习文件", "", + "文档文件 (*.docx *.txt *.pdf);;所有文件 (*.*)" + ) + + if file_path: + try: + self.is_loading_file = True + + # 使用文件解析器 + parser = FileParser() + content = parser.parse_file(file_path) + + if content: + # 存储导入的内容 + self.imported_content = content + self.current_position = 0 + + # 重置打字逻辑 + if self.typing_logic: + self.typing_logic.reset(content) + + # 清空文本显示 + self.text_display_widget.text_display.clear() + + # 更新状态 + self.status_label.setText(f"已导入: {os.path.basename(file_path)}") + self.progress_label.setText(f"进度: 0% (0/{len(content)} 字符)") + + # 显示成功消息 + QMessageBox.information(self, "导入成功", + f"文件导入成功!\n文件: {os.path.basename(file_path)}\n字符数: {len(content)}\n\n开始打字以显示学习内容。") + + else: + QMessageBox.warning(self, "导入失败", "无法解析文件内容,请检查文件格式。") + + except Exception as e: + QMessageBox.critical(self, "导入错误", f"导入文件时出错:\n{str(e)}") + + finally: + self.is_loading_file = False + + def on_text_changed(self): + """ + 文本变化处理 + - 根据导入的内容逐步显示 + - 更新学习进度 + """ + # 如果正在加载文件,跳过处理 + if self.is_loading_file: + return + + # 如果没有导入内容,清空文本 + if not self.imported_content: + current_text = self.text_display_widget.text_display.toPlainText() + if current_text: + self.text_display_widget.text_display.clear() + self.status_label.setText("请先导入文件开始学习") + return + + # 获取当前文本 + current_text = self.text_display_widget.text_display.toPlainText() + + # 始终确保显示的是导入的文件内容,而不是用户输入的文字 + expected_text = self.imported_content[:len(current_text)] + + # 如果当前文本与期望文本不一致,强制恢复到期望文本 + if current_text != expected_text: + cursor = self.text_display_widget.text_display.textCursor() + self.text_display_widget.text_display.setPlainText(expected_text) + self.text_display_widget.text_display.setTextCursor(cursor) + + # 显示错误信息 + if len(current_text) > 0: + expected_char = self.imported_content[len(current_text)-1] if len(current_text) <= len(self.imported_content) else '' + self.status_label.setText(f"输入错误!期望字符: '{expected_char}'") + return + + # 检查输入是否正确 + if self.typing_logic and len(current_text) > 0: + result = self.typing_logic.check_input(current_text) + + if not result['correct']: + # 输入错误,恢复到正确的状态 + expected_text = self.imported_content[:len(current_text)] + if current_text != expected_text: + cursor = self.text_display_widget.text_display.textCursor() + self.text_display_widget.text_display.setPlainText(expected_text) + self.text_display_widget.text_display.setTextCursor(cursor) + + # 显示错误信息 + self.status_label.setText(f"输入错误!期望字符: '{result.get('expected', '')}'") + else: + # 输入正确,更新进度 + self.current_position = len(current_text) + progress = (self.current_position / len(self.imported_content)) * 100 + + self.progress_label.setText( + f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)" + ) + + # 检查是否完成 + if result.get('completed', False): + self.status_label.setText("恭喜!学习完成!") + QMessageBox.information(self, "学习完成", "恭喜您完成了所有学习内容!") + else: + self.status_label.setText("继续输入以显示更多内容...") + + def show_about(self): + """ + 显示关于对话框 + """ + QMessageBox.about(self, "关于学习模式", + "MagicWord 学习模式\n\n" + "功能特点:\n" + "• 顶部显示UI界面图片\n" + "• 下方为打字输入区域\n" + "• 导入文件后逐步显示内容\n" + "• 实时显示学习进度\n\n" + "使用方法:\n" + "1. 点击'文件'->'导入文件'选择学习材料\n" + "2. 在下方文本区域开始打字\n" + "3. 系统会根据您的输入逐步显示内容") + + def closeEvent(self, event): + """ + 窗口关闭事件 + - 通知父窗口学习模式已关闭 + """ + if self.parent_window and hasattr(self.parent_window, 'on_learning_mode_closed'): + self.parent_window.on_learning_mode_closed() + + event.accept() + + def keyPressEvent(self, event): + """ + 按键事件处理 + - 处理特殊按键 + """ + # 处理Escape键关闭窗口 + if event.key() == Qt.Key_Escape: + self.close() + else: + super().keyPressEvent(event) \ No newline at end of file diff --git a/src/ui/114514.png b/src/ui/114514.png new file mode 100644 index 0000000..bbcf243 Binary files /dev/null and b/src/ui/114514.png differ diff --git a/src/ui/UI.png b/src/ui/UI.png new file mode 100644 index 0000000..de2ba66 Binary files /dev/null and b/src/ui/UI.png differ diff --git a/src/ui/components.py b/src/ui/components.py new file mode 100644 index 0000000..3746898 --- /dev/null +++ b/src/ui/components.py @@ -0,0 +1,492 @@ +# ui/components.py +from PyQt5.QtWidgets import QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QTextEdit, QProgressBar +from PyQt5.QtCore import Qt + +class CustomTitleBar(QWidget): + def __init__(self, parent=None): + """ + 自定义标题栏 + - 创建标题栏UI元素 + - 添加窗口控制按钮 + """ + super().__init__(parent) + self.parent = parent + self.setup_ui() + + def setup_ui(self): + """ + 设置标题栏UI + - 初始化所有UI组件 + - 设置组件属性和样式 + """ + # 创建水平布局 + layout = QHBoxLayout() + layout.setContentsMargins(10, 5, 10, 5) + layout.setSpacing(10) + + # 创建标题标签 + self.title_label = QLabel("MagicWord") + self.title_label.setStyleSheet("color: #333333; font-size: 12px; font-weight: normal;") + + # 创建控制按钮 + self.minimize_button = QPushButton("—") + self.maximize_button = QPushButton("□") + self.close_button = QPushButton("×") + + # 设置按钮样式 + button_style = """ + QPushButton { + background-color: transparent; + border: none; + color: #333333; + font-size: 12px; + font-weight: normal; + width: 30px; + height: 30px; + } + QPushButton:hover { + background-color: #d0d0d0; + } + """ + + self.minimize_button.setStyleSheet(button_style) + self.maximize_button.setStyleSheet(button_style) + self.close_button.setStyleSheet(button_style + "QPushButton:hover { background-color: #ff5555; color: white; }") + + # 添加组件到布局 + layout.addWidget(self.title_label) + layout.addStretch() + layout.addWidget(self.minimize_button) + layout.addWidget(self.maximize_button) + layout.addWidget(self.close_button) + + self.setLayout(layout) + + # 连接按钮事件 + self.minimize_button.clicked.connect(self.minimize_window) + self.maximize_button.clicked.connect(self.maximize_window) + self.close_button.clicked.connect(self.close_window) + + # 设置标题栏样式 + self.setStyleSheet(""" + CustomTitleBar { + background-color: #f0f0f0; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + border-bottom: 1px solid #d0d0d0; + } + """) + + def minimize_window(self): + """ + 最小化窗口 + - 触发窗口最小化事件 + """ + if self.parent: + self.parent.showMinimized() + + def maximize_window(self): + """ + 最大化窗口 + - 切换窗口最大化状态 + """ + if self.parent: + if self.parent.isMaximized(): + self.parent.showNormal() + self.maximize_button.setText("□") + else: + self.parent.showMaximized() + self.maximize_button.setText("❐") + + def close_window(self): + """ + 关闭窗口 + - 触发窗口关闭事件 + """ + if self.parent: + self.parent.close() + +class ProgressBarWidget(QWidget): + def __init__(self, parent=None): + """ + 进度条组件 + - 显示打字练习进度 + - 显示统计信息 + """ + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + """ + 设置进度条UI + - 初始化所有UI组件 + - 设置组件属性和样式 + """ + # 创建垂直布局 + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(5) + + # 创建水平布局用于统计信息 + stats_layout = QHBoxLayout() + stats_layout.setSpacing(15) + + # 创建统计信息标签 + self.wpm_label = QLabel("WPM: 0") + self.accuracy_label = QLabel("准确率: 0%") + self.time_label = QLabel("用时: 0s") + + # 设置标签样式 + label_style = "font-size: 12px; font-weight: normal; color: #333333;" + self.wpm_label.setStyleSheet(label_style) + self.accuracy_label.setStyleSheet(label_style) + self.time_label.setStyleSheet(label_style) + + # 添加标签到统计布局 + stats_layout.addWidget(self.wpm_label) + stats_layout.addWidget(self.accuracy_label) + stats_layout.addWidget(self.time_label) + stats_layout.addStretch() + + # 创建进度条 + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(0) + self.progress_bar.setTextVisible(True) + self.progress_bar.setFormat("进度: %p%") + + # 设置进度条样式 + self.progress_bar.setStyleSheet(""" + QProgressBar { + border: 1px solid #c0c0c0; + border-radius: 0px; + text-align: center; + background-color: #f0f0f0; + } + QProgressBar::chunk { + background-color: #0078d7; + border-radius: 0px; + } + """) + + # 添加组件到主布局 + layout.addLayout(stats_layout) + layout.addWidget(self.progress_bar) + + self.setLayout(layout) + + def update_progress(self, progress: float): + """ + 更新进度条 + - 设置进度值 + - 更新显示 + """ + self.progress_bar.setValue(int(progress)) + + def update_stats(self, wpm: int, accuracy: float, time_elapsed: int): + """ + 更新统计信息 + - wpm: 每分钟字数 + - accuracy: 准确率(%) + - time_elapsed: 用时(秒) + """ + self.wpm_label.setText(f"WPM: {wpm}") + self.accuracy_label.setText(f"准确率: {accuracy:.1f}%") + self.time_label.setText(f"用时: {time_elapsed}s") + +class StatsDisplayWidget(QWidget): + def __init__(self, parent=None): + """ + 统计信息显示组件 + - 显示准确率、WPM等统计信息 + """ + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + """ + 设置统计信息显示UI + - 初始化所有UI组件 + - 设置组件属性和样式 + """ + # 创建水平布局 + layout = QHBoxLayout() + layout.setContentsMargins(10, 5, 10, 5) + layout.setSpacing(15) + + # 创建统计信息标签 + self.wpm_label = QLabel("WPM: 0") + self.accuracy_label = QLabel("准确率: 0%") + + # 设置标签样式 + label_style = "font-size: 12px; font-weight: normal; color: #333333;" + self.wpm_label.setStyleSheet(label_style) + self.accuracy_label.setStyleSheet(label_style) + + # 添加组件到布局 + layout.addWidget(self.wpm_label) + layout.addWidget(self.accuracy_label) + layout.addStretch() + + self.setLayout(layout) + + # 设置样式 + self.setStyleSheet(""" + StatsDisplayWidget { + background-color: #f0f0f0; + border-bottom: 1px solid #d0d0d0; + } + """) + + def update_stats(self, wpm: int, accuracy: float): + """ + 更新统计信息 + - wpm: 每分钟字数 + - accuracy: 准确率(%) + """ + self.wpm_label.setText(f"WPM: {wpm}") + self.accuracy_label.setText(f"准确率: {accuracy:.1f}%") + +class QuoteDisplayWidget(QWidget): + def __init__(self, parent=None): + """ + 每日一言显示组件 + - 显示每日一言功能 + """ + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + """ + 设置每日一言显示UI + - 初始化所有UI组件 + - 设置组件属性和样式 + """ + # 创建水平布局 + layout = QHBoxLayout() + layout.setContentsMargins(10, 5, 10, 5) + layout.setSpacing(15) + + # 创建每日一言标签 + self.quote_label = QLabel("每日一言: 暂无") + self.quote_label.setStyleSheet("QLabel { color: #666666; font-style: italic; }") + + # 设置标签样式 + label_style = "font-size: 12px; font-weight: normal; color: #333333;" + self.quote_label.setStyleSheet(label_style) + + # 创建每日一言刷新按钮 + self.refresh_quote_button = QPushButton("刷新") + self.refresh_quote_button.setStyleSheet(""" + QPushButton { + background-color: #0078d7; + color: white; + border: none; + padding: 5px 10px; + border-radius: 3px; + font-size: 12px; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + # 添加组件到布局 + layout.addWidget(self.quote_label) + layout.addStretch() + layout.addWidget(self.refresh_quote_button) + + self.setLayout(layout) + + # 设置样式 + self.setStyleSheet(""" + QuoteDisplayWidget { + background-color: #f0f0f0; + border-bottom: 1px solid #d0d0d0; + } + """) + + def update_quote(self, quote: str): + """ + 更新每日一言 + - quote: 每日一言内容 + """ + self.quote_label.setText(f"每日一言: {quote}") + +class WeatherDisplayWidget(QWidget): + def __init__(self, parent=None): + """ + 天气显示组件 + - 显示天气信息 + """ + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + """ + 设置天气显示UI + - 初始化所有UI组件 + - 设置组件属性和样式 + """ + # 创建水平布局 + layout = QHBoxLayout() + layout.setContentsMargins(10, 5, 10, 5) + layout.setSpacing(15) + + # 创建天气信息标签 + self.weather_label = QLabel("天气: 暂无") + + # 设置标签样式 + label_style = "font-size: 12px; font-weight: normal; color: #333333;" + self.weather_label.setStyleSheet(label_style) + + # 创建天气刷新按钮 + self.refresh_weather_button = QPushButton("刷新") + self.refresh_weather_button.setStyleSheet(""" + QPushButton { + background-color: #0078d7; + color: white; + border: none; + padding: 5px 10px; + border-radius: 3px; + font-size: 12px; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + # 添加组件到布局 + layout.addWidget(self.weather_label) + layout.addStretch() + layout.addWidget(self.refresh_weather_button) + + self.setLayout(layout) + + # 设置样式 + self.setStyleSheet(""" + WeatherDisplayWidget { + background-color: #f0f0f0; + border-bottom: 1px solid #d0d0d0; + } + """) + + def update_weather(self, weather_info: dict): + """ + 更新天气信息 + - weather_info: 天气信息字典 + """ + if weather_info: + city = weather_info.get("city", "未知") + temperature = weather_info.get("temperature", "N/A") + description = weather_info.get("description", "N/A") + self.weather_label.setText(f"天气: {city} {temperature}°C {description}") + else: + self.weather_label.setText("天气: 获取失败") + +class TextDisplayWidget(QWidget): + def __init__(self, parent=None): + """ + 文本显示组件 + - 显示待练习文本 + - 高亮当前字符 + - 显示用户输入反馈 + """ + super().__init__(parent) + self.text_content = "" + self.current_index = 0 + self.setup_ui() + + def setup_ui(self): + """ + 设置文本显示UI + - 初始化文本显示区域 + - 设置样式和布局 + """ + # 创建垂直布局 + layout = QVBoxLayout() + layout.setContentsMargins(20, 20, 20, 20) + + # 创建文本显示区域 + self.text_display = QTextEdit() + self.text_display.setReadOnly(False) # 设置为可编辑 + self.text_display.setLineWrapMode(QTextEdit.WidgetWidth) + + # 设置文本显示样式 + self.text_display.setStyleSheet(""" + QTextEdit { + font-family: 'Calibri', 'Segoe UI', 'Microsoft YaHei', sans-serif; + font-size: 12pt; + border: 1px solid #d0d0d0; + border-radius: 0px; + padding: 15px; + background-color: white; + color: black; + } + """) + + # 添加组件到布局 + layout.addWidget(self.text_display) + self.setLayout(layout) + + def set_text(self, text: str): + """ + 设置显示文本 + - text: 要显示的文本内容 + """ + self.text_content = text + self.current_index = 0 + # 初始不显示内容,通过打字逐步显示 + self.text_display.setHtml("") + + def highlight_character(self, position: int): + """ + 高亮指定位置的字符 + - position: 字符位置索引 + """ + if 0 <= position < len(self.text_content): + self.current_index = position + # 不再直接高亮字符,而是通过用户输入来显示内容 + pass + + def _update_display(self, user_input: str = ""): + """ + 更新文本显示 + - user_input: 用户输入文本(可选) + """ + # 导入需要的模块 + from PyQt5.QtGui import QTextCursor + + if not self.text_content: + self.text_display.clear() + return + + # 简单显示文本,不使用任何高亮 + if user_input: + # 只显示用户已输入的部分文本 + displayed_text = self.text_content[:len(user_input)] + else: + # 没有用户输入,不显示任何内容 + displayed_text = "" + + # 更新文本显示 + self.text_display.setPlainText(displayed_text) + + # 安全地滚动到光标位置 + if user_input and displayed_text: + try: + cursor = self.text_display.textCursor() + # 将光标定位到文本末尾 + cursor.setPosition(len(displayed_text)) + self.text_display.setTextCursor(cursor) + self.text_display.ensureCursorVisible() + except Exception: + # 如果光标定位失败,忽略错误 + pass + + def show_user_input(self, input_text: str): + """ + 显示用户输入的文本 + - input_text: 用户输入的文本 + """ + self._update_display(input_text) \ No newline at end of file diff --git a/src/ui/theme_manager.py b/src/ui/theme_manager.py new file mode 100644 index 0000000..6b42bb1 --- /dev/null +++ b/src/ui/theme_manager.py @@ -0,0 +1,715 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +主题管理器 - 负责检测系统主题并管理应用主题切换 +""" + +from PyQt5.QtWidgets import QApplication +from PyQt5.QtGui import QPalette, QColor +from PyQt5.QtCore import QObject, pyqtSignal +import platform +import subprocess +import os + + +class ThemeManager(QObject): + """主题管理器类""" + + # 主题切换信号 + theme_changed = pyqtSignal(bool) # True表示深色模式,False表示浅色模式 + + def __init__(self): + super().__init__() + self._is_dark_theme = False + self._auto_detect = True + self._detection_timer = None + + def is_dark_theme(self): + """检测是否为深色主题""" + if self._auto_detect: + return self._detect_system_theme() + return self._is_dark_theme + + def _detect_system_theme(self): + """检测系统主题""" + try: + # macOS系统主题检测 + if platform.system() == 'Darwin': + return self._detect_macos_theme() + # Windows系统主题检测 + elif platform.system() == 'Windows': + return self._detect_windows_theme() + # Linux系统主题检测 + else: + return self._detect_linux_theme() + except Exception as e: + print(f"主题检测失败: {e}") + return False + + def _detect_macos_theme(self): + """检测macOS系统主题""" + try: + # 使用osascript命令检测macOS主题 + result = subprocess.run([ + 'osascript', '-e', + 'tell application "System Events" to tell appearance preferences to return dark mode' + ], capture_output=True, text=True, timeout=5) + + if result.returncode == 0: + return 'true' in result.stdout.lower() + + # 备用方法:检测系统设置 + result = subprocess.run([ + 'defaults', 'read', '-g', 'AppleInterfaceStyle' + ], capture_output=True, text=True, timeout=5) + + if result.returncode == 0: + return 'dark' in result.stdout.lower() + + except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError): + pass + + # 使用Qt的调色板检测作为备用方法 + return self._detect_by_palette() + + def _detect_windows_theme(self): + """检测Windows系统主题""" + try: + # Windows 10/11 注册表检测 + import winreg + + key_path = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize' + value_name = 'AppsUseLightTheme' + + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key: + value, _ = winreg.QueryValueEx(key, value_name) + return value == 0 # 0表示深色模式 + except (OSError, FileNotFoundError): + pass + + except ImportError: + pass + + # 使用Qt的调色板检测作为备用方法 + return self._detect_by_palette() + + def _detect_linux_theme(self): + """检测Linux系统主题""" + try: + # 检测GTK主题 + result = subprocess.run([ + 'gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme' + ], capture_output=True, text=True, timeout=5) + + if result.returncode == 0: + theme_name = result.stdout.strip().strip("'") + return any(dark_name in theme_name.lower() for dark_name in ['dark', 'night']) + + # 检测颜色方案 + result = subprocess.run([ + 'gsettings', 'get', 'org.gnome.desktop.interface', 'color-scheme' + ], capture_output=True, text=True, timeout=5) + + if result.returncode == 0: + return 'dark' in result.stdout.lower() + + except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError): + pass + + # 使用Qt的调色板检测作为备用方法 + return self._detect_by_palette() + + def _detect_by_palette(self): + """使用Qt调色板检测主题""" + app = QApplication.instance() + if app is None: + return False + + palette = app.palette() + + # 检测背景色和文本色的亮度 + window_color = palette.color(QPalette.Window) + text_color = palette.color(QPalette.WindowText) + + # 计算亮度 + def get_luminance(color): + return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255 + + window_luminance = get_luminance(window_color) + text_luminance = get_luminance(text_color) + + # 如果背景比文本暗,则为深色主题 + return window_luminance < text_luminance + + def get_theme_stylesheet(self, is_dark=None): + """获取主题样式表""" + if is_dark is None: + is_dark = self.is_dark_theme() + + if is_dark: + return self._get_dark_stylesheet() + else: + return self._get_light_stylesheet() + + def _get_dark_stylesheet(self): + """深色主题样式表""" + return """ + /* 深色主题样式 */ + + /* 全局文字颜色 */ + QWidget { + color: #e0e0e0; + } + + /* 主窗口 */ + QMainWindow { + background-color: #1e1e1e; + } + + /* 菜单栏 */ + QMenuBar { + background-color: #0078d7; + border: 1px solid #005a9e; + font-size: 12px; + color: #ffffff; + } + + QMenuBar::item { + background-color: transparent; + padding: 4px 10px; + color: #e0e0e0; + } + + QMenuBar::item:selected { + background-color: #106ebe; + } + + /* 菜单 */ + QMenu { + background-color: #2d2d2d; + border: 1px solid #3c3c3c; + font-size: 12px; + color: #e0e0e0; + } + + QMenu::item { + padding: 4px 20px; + color: #e0e0e0; + } + + QMenu::item:selected { + background-color: #3c3c3c; + } + + /* 功能区 */ + QFrame { + background-color: #2d2d2d; + border: 1px solid #3c3c3c; + } + + /* 组框 */ + QGroupBox { + font-size: 11px; + font-weight: normal; + color: #e0e0e0; + background-color: #2d2d2d; + border: 1px solid #3c3c3c; + border-radius: 0px; + margin-top: 5px; + padding-top: 5px; + } + + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + color: #e0e0e0; + } + + /* 按钮 */ + QToolButton { + border: 1px solid #3c3c3c; + border-radius: 3px; + background-color: #3c3c3c; + font-size: 11px; + color: #e0e0e0; + padding: 3px 6px; + } + + QToolButton:hover { + background-color: #4a4a4a; + border: 1px solid #5a5a5a; + } + + QToolButton:pressed { + background-color: #2a2a2a; + border: 1px solid #1a1a1a; + } + + QToolButton:checked { + background-color: #0078d4; + border: 1px solid #106ebe; + } + + /* 切换按钮 */ + QToolButton[checkable="true"] { + border: 1px solid #3c3c3c; + border-radius: 2px; + background-color: #3c3c3c; + font-size: 12px; + font-weight: bold; + color: #e0e0e0; + } + + QToolButton[checkable="true"]:hover { + background-color: #4a4a4a; + } + + QToolButton[checkable="true"]:checked { + background-color: #0078d4; + border: 1px solid #106ebe; + } + + /* 下拉框 - 修复文字不可见问题 */ + QComboBox { + background-color: #3c3c3c; + border: 1px solid #5a5a5a; + border-radius: 2px; + color: #e0e0e0; + padding: 2px 5px; + selection-background-color: #4a4a4a; + selection-color: #e0e0e0; + } + + QComboBox:hover { + background-color: #4a4a4a; + border: 1px solid #6a6a6a; + } + + QComboBox::drop-down { + border: none; + width: 15px; + background-color: #3c3c3c; + } + + QComboBox::down-arrow { + image: none; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 5px solid #e0e0e0; + } + + /* 下拉框弹出列表 */ + QComboBox QAbstractItemView { + background-color: #3c3c3c; + border: 1px solid #5a5a5a; + color: #e0e0e0; + selection-background-color: #4a4a4a; + selection-color: #e0e0e0; + } + + /* 字体下拉框特殊处理 */ + QFontComboBox { + background-color: #3c3c3c; + border: 1px solid #5a5a5a; + border-radius: 2px; + color: #e0e0e0; + padding: 2px 5px; + selection-background-color: #4a4a4a; + selection-color: #e0e0e0; + } + + QFontComboBox:hover { + background-color: #4a4a4a; + border: 1px solid #6a6a6a; + } + + QFontComboBox QAbstractItemView { + background-color: #3c3c3c; + border: 1px solid #5a5a5a; + color: #e0e0e0; + selection-background-color: #4a4a4a; + selection-color: #e0e0e0; + } + + /* 文本编辑器 */ + QTextEdit { + background-color: #1e1e1e; + border: 1px solid #3c3c3c; + color: #e0e0e0; + padding: 20px; + line-height: 1.5; + } + + /* 状态栏 */ + QStatusBar { + background-color: #2d2d2d; + border-top: 1px solid #3c3c3c; + font-size: 11px; + color: #e0e0e0; + } + + /* 标签 */ + QLabel { + color: #e0e0e0; + background-color: transparent; + } + + /* 消息框 - 修复黑色背景问题 */ + QMessageBox { + background-color: #2d2d2d; + color: #e0e0e0; + } + + QMessageBox QPushButton { + background-color: #3c3c3c; + color: #e0e0e0; + border: 1px solid #5a5a5a; + border-radius: 3px; + padding: 5px 15px; + min-width: 80px; + } + + QMessageBox QPushButton:hover { + background-color: #4a4a4a; + border: 1px solid #6a6a6a; + } + + QMessageBox QPushButton:pressed { + background-color: #2a2a2a; + border: 1px solid #1a1a1a; + } + + /* 滚动条 */ + QScrollBar:vertical { + background-color: #2d2d2d; + width: 12px; + border: none; + } + + QScrollBar::handle:vertical { + background-color: #5a5a5a; + border-radius: 6px; + min-height: 20px; + } + + QScrollBar::handle:vertical:hover { + background-color: #6a6a6a; + } + + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + border: none; + background: none; + } + """ + + def _get_light_stylesheet(self): + """浅色主题样式表 - 白底黑字""" + return """ + /* 浅色主题样式 - 白底黑字 */ + + /* 全局文字颜色 */ + QWidget { + color: #333333; + } + + /* 主窗口 */ + QMainWindow { + background-color: #f3f2f1; + } + + /* 菜单栏 */ + QMenuBar { + background-color: #0078d7; + border: 1px solid #005a9e; + font-size: 12px; + color: #ffffff; + } + + QMenuBar::item { + background-color: transparent; + padding: 4px 10px; + color: #333333; + } + + QMenuBar::item:selected { + background-color: #106ebe; + } + + /* 菜单 */ + QMenu { + background-color: #ffffff; + border: 1px solid #d0d0d0; + font-size: 12px; + color: #333333; + } + + QMenu::item { + padding: 4px 20px; + color: #333333; + } + + QMenu::item:selected { + background-color: #f0f0f0; + } + + /* 功能区 */ + QFrame { + background-color: #ffffff; + border: 1px solid #d0d0d0; + } + + /* 组框 */ + QGroupBox { + font-size: 11px; + font-weight: normal; + color: #333333; + background-color: #ffffff; + border: 1px solid #d0d0d0; + border-radius: 0px; + margin-top: 5px; + padding-top: 5px; + } + + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + color: #333333; + } + + /* 按钮 */ + QToolButton { + border: 1px solid #d0d0d0; + border-radius: 3px; + background-color: #ffffff; + font-size: 11px; + color: #333333; + padding: 3px 6px; + } + + QToolButton:hover { + background-color: #f0f0f0; + border: 1px solid #0078d7; + } + + QToolButton:pressed { + background-color: #e0e0e0; + border: 1px solid #005a9e; + } + + QToolButton:checked { + background-color: #0078d7; + border: 1px solid #005a9e; + color: #ffffff; + } + + /* 切换按钮 */ + QToolButton[checkable="true"] { + border: 1px solid #d0d0d0; + border-radius: 2px; + background-color: #ffffff; + font-size: 12px; + font-weight: bold; + color: #333333; + } + + QToolButton[checkable="true"]:hover { + background-color: #f0f0f0; + } + + QToolButton[checkable="true"]:checked { + background-color: #0078d7; + border: 1px solid #005a9e; + color: #ffffff; + } + + /* 下拉框 - 白底黑字 */ + QComboBox { + background-color: #ffffff; + border: 1px solid #d0d0d0; + border-radius: 2px; + color: #333333; + padding: 2px 5px; + selection-background-color: #f0f0f0; + selection-color: #333333; + } + + QComboBox:hover { + background-color: #f0f0f0; + border: 1px solid #0078d7; + } + + QComboBox::drop-down { + border: none; + width: 15px; + background-color: #ffffff; + } + + QComboBox::down-arrow { + image: none; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 5px solid #333333; + } + + /* 下拉框弹出列表 */ + QComboBox QAbstractItemView { + background-color: #ffffff; + border: 1px solid #d0d0d0; + color: #333333; + selection-background-color: #f0f0f0; + selection-color: #333333; + } + + /* 字体下拉框特殊处理 - 白底黑字 */ + QFontComboBox { + background-color: #ffffff; + border: 1px solid #d0d0d0; + border-radius: 2px; + color: #333333; + padding: 2px 5px; + selection-background-color: #f0f0f0; + selection-color: #333333; + } + + QFontComboBox:hover { + background-color: #f0f0f0; + border: 1px solid #0078d7; + } + + QFontComboBox::drop-down { + border: none; + width: 15px; + background-color: #ffffff; + } + + QFontComboBox::down-arrow { + image: none; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 5px solid #333333; + } + + QFontComboBox QAbstractItemView { + background-color: #ffffff; + border: 1px solid #d0d0d0; + color: #333333; + selection-background-color: #f0f0f0; + selection-color: #333333; + } + + /* 文本编辑器 */ + QTextEdit { + background-color: #ffffff; + border: 1px solid #d0d0d0; + color: #000000; + padding: 20px; + line-height: 1.5; + } + + /* 状态栏 */ + QStatusBar { + background-color: #ffffff; + border-top: 1px solid #d0d0d0; + font-size: 11px; + color: #333333; + } + + /* 标签 */ + QLabel { + color: #333333; + background-color: transparent; + } + + /* 消息框 - 修复黑色背景问题 */ + QMessageBox { + background-color: #ffffff; + color: #333333; + } + + QMessageBox QPushButton { + background-color: #ffffff; + color: #333333; + border: 1px solid #d0d0d0; + border-radius: 3px; + padding: 5px 15px; + min-width: 80px; + } + + QMessageBox QPushButton:hover { + background-color: #f0f0f0; + border: 1px solid #0078d7; + } + + QMessageBox QPushButton:pressed { + background-color: #e0e0e0; + border: 1px solid #005a9e; + } + + /* 滚动条 */ + QScrollBar:vertical { + background-color: #ffffff; + width: 12px; + border: none; + } + + QScrollBar::handle:vertical { + background-color: #c0c0c0; + border-radius: 6px; + min-height: 20px; + } + + QScrollBar::handle:vertical:hover { + background-color: #a0a0a0; + } + + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + border: none; + background: none; + } + """ + + def set_dark_theme(self, is_dark): + """设置深色主题""" + self._is_dark_theme = is_dark + self._auto_detect = False + self.theme_changed.emit(is_dark) + + def enable_auto_detection(self, enabled=True): + """启用/禁用自动主题检测""" + self._auto_detect = enabled + if enabled: + self.theme_changed.emit(self.is_dark_theme()) + + def get_current_theme_colors(self): + """获取当前主题颜色配置""" + is_dark = self.is_dark_theme() + + if is_dark: + return { + 'background': '#1e1e1e', + 'surface': '#2d2d2d', + 'surface_hover': '#3c3c3c', + 'text': '#e0e0e0', + 'text_secondary': '#b0b0b0', + 'border': '#3c3c3c', + 'accent': '#0078d4', + 'accent_hover': '#106ebe' + } + else: + return { + 'background': '#f3f2f1', + 'surface': '#ffffff', + 'surface_hover': '#f0f0f0', + 'text': '#333333', + 'text_secondary': '#666666', + 'border': '#d0d0d0', + 'accent': '#0078d7', + 'accent_hover': '#005a9e' + } + + +# 全局主题管理器实例 +theme_manager = ThemeManager() \ No newline at end of file diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 065a8de..5150f9f 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -10,6 +10,9 @@ import requests import json from datetime import datetime +# 导入主题管理器 +from .theme_manager import theme_manager + class WordRibbonTab(QWidget): def __init__(self, parent=None): super().__init__(parent) @@ -33,6 +36,9 @@ class WordRibbon(QFrame): self.quote_group = None # 每日一言组件组 self.ribbon_layout = None # 功能区布局 self.setup_ui() + + # 初始化主题 + self.init_theme() def setup_ui(self): self.setFrameStyle(QFrame.NoFrame) @@ -80,7 +86,7 @@ class WordRibbon(QFrame): self.font_combo.currentFontChanged.connect(self.on_font_changed) self.font_size_combo = QComboBox() self.font_size_combo.addItems(['8', '9', '10', '11', '12', '14', '16', '18', '20', '22', '24', '26', '28', '36', '48', '72']) - self.font_size_combo.setFixedWidth(50) + self.font_size_combo.setFixedWidth(70) self.font_size_combo.setCurrentText('12') self.font_size_combo.currentTextChanged.connect(self.on_font_size_changed) @@ -114,6 +120,7 @@ class WordRibbon(QFrame): # 段落组 paragraph_group = self.create_ribbon_group("段落") + paragraph_group.setFixedWidth(320) # 增加宽度以适应更宽的按钮 # 对齐方式 align_layout = QHBoxLayout() @@ -142,13 +149,24 @@ class WordRibbon(QFrame): # 编辑组 editing_group = self.create_ribbon_group("编辑") - self.find_btn = self.create_ribbon_button("查找", "Ctrl+F", "find") - self.replace_btn = self.create_ribbon_button("替换", "Ctrl+H", "replace") - editing_layout = QVBoxLayout() + # 创建查找替换按钮,使用更适合水平排列的样式 + self.find_btn = QToolButton() + self.find_btn.setText("查找") + self.find_btn.setToolButtonStyle(Qt.ToolButtonTextOnly) + self.find_btn.setFixedSize(50, 25) # 设置适合水平排列的尺寸 + + self.replace_btn = QToolButton() + self.replace_btn.setText("替换") + self.replace_btn.setToolButtonStyle(Qt.ToolButtonTextOnly) + self.replace_btn.setFixedSize(50, 25) # 设置适合水平排列的尺寸 + + editing_layout = QHBoxLayout() # 改为水平布局 editing_layout.addWidget(self.find_btn) editing_layout.addWidget(self.replace_btn) + editing_layout.addStretch() # 添加弹性空间 editing_group.setLayout(editing_layout) + editing_group.setFixedWidth(120) # 设置编辑组宽度以适应查找替换按钮 layout.addWidget(editing_group) layout.addStretch() @@ -177,6 +195,275 @@ class WordRibbon(QFrame): """字体颜色按钮点击处理""" pass + def init_theme(self): + """初始化主题""" + # 连接主题切换信号 + theme_manager.theme_changed.connect(self.on_theme_changed) + + # 应用当前主题 + self.apply_theme() + + def apply_theme(self): + """应用主题样式""" + is_dark = theme_manager.is_dark_theme() + + # 更新功能区背景 + colors = theme_manager.get_current_theme_colors() + self.ribbon_area.setStyleSheet(f""" + QFrame {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-top: none; + }} + QGroupBox {{ + background-color: {colors['surface']}; + color: {colors['text']}; + border: 1px solid {colors['border']}; + }} + QGroupBox::title {{ + color: {colors['text']}; + background-color: transparent; + }} + QLabel {{ + color: {colors['text']}; + background-color: transparent; + }} + """) + + # 更新下拉框样式 + self.update_combo_styles(is_dark) + + def update_combo_styles(self, is_dark): + """更新下拉框样式""" + colors = theme_manager.get_current_theme_colors() + + # 字体下拉框样式 + if hasattr(self, 'font_combo'): + self.font_combo.setStyleSheet(f""" + QFontComboBox {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 2px; + color: {colors['text']}; + padding: 2px 5px; + }} + + QFontComboBox:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['accent']}; + }} + + QFontComboBox::drop-down {{ + border: none; + width: 15px; + }} + + QFontComboBox::down-arrow {{ + image: none; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 5px solid {colors['text']}; + }} + """) + + # 字体大小下拉框样式 + if hasattr(self, 'font_size_combo'): + self.font_size_combo.setStyleSheet(f""" + QComboBox {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 2px; + color: {colors['text']}; + padding: 2px 5px; + }} + + QComboBox:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['accent']}; + }} + + QComboBox::drop-down {{ + border: none; + width: 15px; + }} + + QComboBox::down-arrow {{ + image: none; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 5px solid {colors['text']}; + }} + """) + + # 更新字体工具栏按钮样式 + self.update_font_button_styles(is_dark) + + def update_font_button_styles(self, is_dark): + """更新字体工具栏按钮样式""" + colors = theme_manager.get_current_theme_colors() + + # 字体样式按钮样式 (B, I, U) + font_button_style = f""" + QToolButton {{ + border: 1px solid {colors['border']}; + border-radius: 2px; + background-color: {colors['surface']}; + font-size: 12px; + font-weight: bold; + color: {colors['text']}; + }} + QToolButton:hover {{ + background-color: {colors['surface_hover']}; + }} + QToolButton:checked {{ + background-color: {colors['accent']}; + border: 1px solid {colors['accent']}; + color: {colors['surface']}; + }} + """ + + # 应用到字体样式按钮 + for btn_name in ['bold_btn', 'italic_btn', 'underline_btn']: + if hasattr(self, btn_name): + getattr(self, btn_name).setStyleSheet(font_button_style) + + # 颜色按钮样式 (A) + color_button_style = f""" + QToolButton {{ + border: 1px solid {colors['border']}; + border-radius: 2px; + background-color: {colors['surface']}; + font-size: 12px; + font-weight: bold; + color: {colors['text']}; + }} + QToolButton:hover {{ + background-color: {colors['surface_hover']}; + }} + QToolButton:pressed {{ + background-color: {colors['accent']}; + border: 1px solid {colors['accent']}; + color: {colors['surface']}; + }} + """ + + # 应用到颜色按钮 + if hasattr(self, 'color_btn'): + self.color_btn.setStyleSheet(color_button_style) + + # 段落对齐按钮样式 (左对齐、居中、右对齐、两端对齐) + paragraph_button_style = f""" + QToolButton {{ + border: 1px solid {colors['border']}; + border-radius: 2px; + background-color: {colors['surface']}; + font-size: 12px; + font-weight: bold; + color: {colors['text']}; + }} + QToolButton:hover {{ + background-color: {colors['surface_hover']}; + }} + QToolButton:checked {{ + background-color: {colors['accent']}; + border: 1px solid {colors['accent']}; + color: {colors['surface']}; + }} + """ + + # 应用到段落对齐按钮 + for btn_name in ['align_left_btn', 'align_center_btn', 'align_right_btn', 'align_justify_btn']: + if hasattr(self, btn_name): + getattr(self, btn_name).setStyleSheet(paragraph_button_style) + + # 编辑按钮样式 (查找、替换) + edit_button_style = f""" + QToolButton {{ + border: 1px solid {colors['border']}; + border-radius: 3px; + background-color: {colors['surface']}; + font-size: 11px; + color: {colors['text']}; + padding: 3px 6px; + }} + QToolButton:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['accent']}; + }} + QToolButton:pressed {{ + background-color: {colors['accent']}; + border: 1px solid {colors['accent']}; + color: {colors['surface']}; + }} + """ + + # 应用到编辑按钮 + for btn_name in ['find_btn', 'replace_btn']: + if hasattr(self, btn_name): + getattr(self, btn_name).setStyleSheet(edit_button_style) + + # 城市选择下拉框样式 + if hasattr(self, 'city_combo'): + self.city_combo.setStyleSheet(f""" + QComboBox {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 2px; + color: {colors['text']}; + padding: 2px 5px; + }} + + QComboBox:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['accent']}; + }} + + QComboBox::drop-down {{ + border: none; + width: 15px; + }} + + QComboBox::down-arrow {{ + image: none; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 5px solid {colors['text']}; + }} + """) + + # 每日一言类型下拉框样式 + if hasattr(self, 'quote_type_combo'): + self.quote_type_combo.setStyleSheet(f""" + QComboBox {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 2px; + color: {colors['text']}; + padding: 2px 5px; + }} + + QComboBox:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['accent']}; + }} + + QComboBox::drop-down {{ + border: none; + width: 15px; + }} + + QComboBox::down-arrow {{ + image: none; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 5px solid {colors['text']}; + }} + """) + + def on_theme_changed(self, is_dark): + """主题切换槽函数""" + self.apply_theme() + def create_weather_group(self): """创建天气组件组""" if self.weather_group is not None: @@ -423,7 +710,13 @@ class WordRibbon(QFrame): btn.setText(text) btn.setCheckable(True) btn.setToolButtonStyle(Qt.ToolButtonTextOnly) - btn.setFixedSize(30, 25) + # 根据文本长度设置宽度,中文字符需要更宽 + if len(text) <= 1: + btn.setFixedSize(30, 25) # 单个字符(如B、I、U) + elif len(text) <= 2: + btn.setFixedSize(45, 25) # 两个字符(如"居中") + else: + btn.setFixedSize(60, 25) # 三个字符及以上(如"左对齐"、"两端对齐") btn.setStyleSheet(""" QToolButton { border: 1px solid #d0d0d0; @@ -1339,8 +1632,8 @@ class daily_sentence_API: params['json'] = 'true' # 默认为 JSON try: - # 发送 GET 请求 - response = requests.get(self.api_url, params=params) + # 发送 GET 请求,禁用SSL验证以避免证书问题 + response = requests.get(self.api_url, params=params, verify=False, timeout=10) response.raise_for_status() # 如果请求失败(如 4xx 或 5xx),抛出异常 # 根据格式类型处理响应 @@ -1348,6 +1641,69 @@ class daily_sentence_API: return response.json() # 解析为字典 else: return response.text # 返回文本内容 + except requests.exceptions.SSLError as e: + print(f"SSL连接失败: {e}") + # 尝试使用备用API + return self.get_sentence_from_backup_api(format_type) except requests.exceptions.RequestException as e: print(f"请求失败: {e}") + # 尝试使用备用API + return self.get_sentence_from_backup_api(format_type) + except Exception as e: + print(f"未知错误: {e}") return None + + def get_sentence_from_backup_api(self, format_type='json'): + """使用备用API获取每日一言""" + try: + # 备用API:古诗词API + response = requests.get("https://v1.jinrishici.com/all.json", timeout=5) + if response.status_code == 200: + data = response.json() + content = data.get('content', '山重水复疑无路,柳暗花明又一村。') + author = data.get('author', '陆游') + title = data.get('origin', '游山西村') + + # 格式化返回数据,与原API格式保持一致 + if format_type == 'json': + return { + 'yiyan': f"{content} — {author}《{title}》", + 'id': 'backup_001', + 'createTime': '2024-01-01', + 'nick': '古诗词' + } + else: + return f"{content} — {author}《{title}》" + else: + # 如果备用API也失败,返回默认内容 + return self.get_default_sentence(format_type) + except Exception as e: + print(f"备用API请求失败: {e}") + return self.get_default_sentence(format_type) + + def get_default_sentence(self, format_type='json'): + """获取默认的每日一言内容""" + default_sentences = [ + "学而不思则罔,思而不学则殆。 — 孔子《论语》", + "天行健,君子以自强不息。 — 《周易》", + "千里之行,始于足下。 — 老子《道德经》", + "读书破万卷,下笔如有神。 — 杜甫《奉赠韦左丞丈二十二韵》", + "海内存知己,天涯若比邻。 — 王勃《送杜少府之任蜀州》" + ] + + import random + import time + + # 使用当前时间作为随机种子,确保每天显示不同的内容 + random.seed(int(time.time() / 86400)) # 每天变化一次 + sentence = random.choice(default_sentences) + + if format_type == 'json': + return { + 'yiyan': sentence, + 'id': 'default_001', + 'createTime': '2024-01-01', + 'nick': '经典名言' + } + else: + return sentence \ No newline at end of file diff --git a/src/word_main_window.py b/src/word_main_window.py index 91a4217..2a7f7da 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -17,6 +17,9 @@ from ui.word_style_ui import WeatherAPI from file_parser import FileParser from input_handler.input_processor import InputProcessor +# 导入主题管理器 +from ui.theme_manager import theme_manager + class WeatherFetchThread(QThread): weather_fetched = pyqtSignal(dict) @@ -98,6 +101,9 @@ class WordStyleMainWindow(QMainWindow): self.setWindowTitle("文档1 - MagicWord") self.setGeometry(100, 100, 1200, 800) + # 初始化主题 + self.init_theme() + # 设置应用程序图标 self.set_window_icon() @@ -120,6 +126,261 @@ class WordStyleMainWindow(QMainWindow): # 初始化时刷新天气 self.refresh_weather() + def init_theme(self): + """初始化主题""" + # 连接主题切换信号 + theme_manager.theme_changed.connect(self.on_theme_changed) + + # 设置默认为白色模式(禁用自动检测) + theme_manager.enable_auto_detection(False) + theme_manager.set_dark_theme(False) + + # 应用当前主题 + self.apply_theme() + + def apply_theme(self): + """应用主题样式""" + is_dark = theme_manager.is_dark_theme() + + # 应用主题样式表 + stylesheet = theme_manager.get_theme_stylesheet(is_dark) + if stylesheet.strip(): # 只在有样式内容时应用 + self.setStyleSheet(stylesheet) + + # 更新组件样式 + self.update_component_styles(is_dark) + + def update_component_styles(self, is_dark): + """更新组件样式""" + colors = theme_manager.get_current_theme_colors() + + # 更新菜单栏样式 - 使用微软蓝 + if hasattr(self, 'menubar'): + self.menubar.setStyleSheet(""" + QMenuBar { + background-color: #0078d7; + border: 1px solid #005a9e; + font-size: 12px; + color: #ffffff; + } + + QMenuBar::item { + background-color: transparent; + padding: 4px 10px; + color: #ffffff; + } + + QMenuBar::item:selected { + background-color: #106ebe; + } + + QMenuBar::item:pressed { + background-color: #005a9e; + color: #ffffff; + } + + #startMenu { + background-color: white; + color: #000000; + } + """) + + # 更新文件菜单样式 + if hasattr(self, 'file_menu') and self.file_menu is not None: + self.file_menu.setStyleSheet(f""" + QMenu {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + font-size: 12px; + color: {colors['text']}; + }} + + QMenu::item {{ + padding: 4px 20px; + color: {colors['text']}; + background-color: transparent; + }} + + QMenu::item:selected {{ + background-color: {colors['surface_hover']}; + }} + + QMenu::item:pressed {{ + background-color: {colors['accent']}; + color: {colors['surface']}; + }} + """) + + # 更新视图菜单样式 + if hasattr(self, 'view_menu') and self.view_menu is not None: + self.view_menu.setStyleSheet(f""" + QMenu {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + font-size: 12px; + color: {colors['text']}; + }} + + QMenu::item {{ + padding: 4px 20px; + color: {colors['text']}; + background-color: transparent; + }} + + QMenu::item:selected {{ + background-color: {colors['surface_hover']}; + }} + + QMenu::item:pressed {{ + background-color: {colors['accent']}; + color: {colors['surface']}; + }} + """) + + # 更新开始菜单样式 + if hasattr(self, 'start_menu') and self.start_menu is not None: + self.start_menu.setStyleSheet(f""" + QMenu {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + font-size: 12px; + color: {colors['text']}; + }} + + QMenu::item {{ + padding: 4px 20px; + color: {colors['text']}; + background-color: transparent; + }} + + QMenu::item:selected {{ + background-color: {colors['surface_hover']}; + }} + + QMenu::item:pressed {{ + background-color: {colors['accent']}; + color: {colors['surface']}; + }} + """) + + # 更新文本编辑器样式 + if hasattr(self, 'text_edit'): + self.text_edit.setStyleSheet(f""" + QTextEdit {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 0px; + font-family: 'Calibri', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 12pt; + color: {colors['text']}; + padding: 40px; + line-height: 1.5; + }} + """) + + # 更新滚动区域样式 + if hasattr(self, 'scroll_area'): + self.scroll_area.setStyleSheet(f""" + QScrollArea {{ + background-color: {colors['background']}; + border: none; + }} + QScrollArea QWidget {{ + background-color: {colors['background']}; + }} + """) + + # 更新图片列表样式(如果已存在) + if hasattr(self, 'image_list_widget') and self.image_list_widget is not None: + self.image_list_widget.setStyleSheet(f""" + QListWidget {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 4px; + font-size: 11px; + color: {colors['text']}; + }} + QListWidget::item {{ + padding: 5px; + border-bottom: 1px solid {colors['border']}; + color: {colors['text']}; + }} + QListWidget::item:selected {{ + background-color: {colors['accent']}; + color: {colors['surface']}; + }} + """) + + # 更新功能区下拉框样式 + if hasattr(self, 'ribbon'): + self.update_ribbon_styles(is_dark) + + def update_ribbon_styles(self, is_dark): + """更新功能区样式""" + colors = theme_manager.get_current_theme_colors() + + # 更新字体下拉框样式 + if hasattr(self.ribbon, 'font_combo'): + self.ribbon.font_combo.setStyleSheet(f""" + QComboBox {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 2px; + color: {colors['text']}; + padding: 2px 5px; + }} + + QComboBox:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['accent']}; + }} + + QComboBox::drop-down {{ + border: none; + width: 15px; + }} + + QComboBox::down-arrow {{ + image: none; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 5px solid {colors['text']}; + }} + """) + + # 更新字体大小下拉框样式 + if hasattr(self.ribbon, 'size_combo'): + self.ribbon.size_combo.setStyleSheet(f""" + QComboBox {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 2px; + color: {colors['text']}; + padding: 2px 5px; + }} + + QComboBox:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['accent']}; + }} + + QComboBox::drop-down {{ + border: none; + width: 15px; + }} + + QComboBox::down-arrow {{ + image: none; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 5px solid {colors['text']}; + }} + """) + + def on_theme_changed(self, is_dark): + """主题切换槽函数""" + self.apply_theme() + def set_window_icon(self): """设置窗口图标""" # 使用我们创建的Word风格图标 @@ -247,30 +508,12 @@ class WordStyleMainWindow(QMainWindow): def create_menu_bar(self): """创建菜单栏""" menubar = self.menuBar() - menubar.setStyleSheet(""" - QMenuBar { - background-color: #f3f2f1; - border-bottom: 1px solid #d0d0d0; - font-size: 12px; - color: #333333; - } - QMenuBar::item:selected { - background-color: #e1e1e1; - } - """) + menubar.setNativeMenuBar(False) + self.menubar = menubar # 保存为实例变量以便后续样式更新 # 文件菜单 file_menu = menubar.addMenu('文件(F)') - file_menu.setStyleSheet(""" - QMenu { - background-color: #ffffff; - border: 1px solid #d0d0d0; - font-size: 12px; - } - QMenu::item:selected { - background-color: #e1e1e1; - } - """) + self.file_menu = file_menu # 保存为实例变量 # 新建 new_action = QAction('新建(N)', self) @@ -304,6 +547,8 @@ class WordStyleMainWindow(QMainWindow): # 开始菜单 start_menu = menubar.addMenu('开始(S)') + start_menu.setObjectName("startMenu") + self.start_menu = start_menu # 保存为实例变量 # 撤销 undo_action = QAction('撤销(U)', self) @@ -339,6 +584,7 @@ class WordStyleMainWindow(QMainWindow): # 视图菜单 view_menu = menubar.addMenu('视图(V)') + self.view_menu = view_menu # 保存为实例变量 # 阅读视图 read_view_action = QAction('阅读视图', self) @@ -354,6 +600,25 @@ class WordStyleMainWindow(QMainWindow): view_menu.addSeparator() + # 模式选择子菜单 + theme_menu = view_menu.addMenu('模式') + + # 白色模式 + self.light_mode_action = QAction('白色模式', self) + self.light_mode_action.setCheckable(True) + self.light_mode_action.setChecked(not theme_manager.is_dark_theme()) # 根据当前主题设置 + self.light_mode_action.triggered.connect(self.set_light_mode) + theme_menu.addAction(self.light_mode_action) + + # 黑色模式 + self.dark_mode_action = QAction('黑色模式', self) + self.dark_mode_action.setCheckable(True) + self.dark_mode_action.setChecked(theme_manager.is_dark_theme()) # 根据当前主题设置 + self.dark_mode_action.triggered.connect(self.set_dark_mode) + theme_menu.addAction(self.dark_mode_action) + + view_menu.addSeparator() + # 视图模式选择 view_mode_menu = view_menu.addMenu('视图模式') @@ -402,24 +667,30 @@ class WordStyleMainWindow(QMainWindow): show_weather_action = QAction('显示详细天气', self) show_weather_action.triggered.connect(self.show_detailed_weather) weather_menu.addAction(show_weather_action) - #插入 + # 插入菜单 insert_menu = menubar.addMenu('插入(I)') - #绘图 + + # 绘图菜单 paint_menu = menubar.addMenu('绘图(D)') - #设计 + + # 设计菜单 design_menu = menubar.addMenu('设计(G)') - #布局 + + # 布局菜单 layout_menu = menubar.addMenu('布局(L)') - #引用 + + # 引用菜单 reference_menu = menubar.addMenu('引用(R)') - #邮件 + + # 邮件菜单 mail_menu = menubar.addMenu('邮件(M)') - #审阅 + + # 审阅菜单 review_menu = menubar.addMenu('审阅(W)') - #视图 - view_menu = menubar.addMenu('视图(V)') - #开发工具 + + # 开发工具菜单 developer_menu = menubar.addMenu('开发工具(Q)') + # 帮助菜单 help_menu = menubar.addMenu('帮助(H)') @@ -435,11 +706,11 @@ class WordStyleMainWindow(QMainWindow): # 创建滚动区域 from PyQt5.QtWidgets import QScrollArea - scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) - scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) - scroll_area.setStyleSheet(""" + self.scroll_area = QScrollArea() # 保存为实例变量 + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.scroll_area.setStyleSheet(""" QScrollArea { background-color: #e1e1e1; border: none; @@ -507,8 +778,8 @@ class WordStyleMainWindow(QMainWindow): document_layout.addWidget(self.image_list_widget) document_container.setLayout(document_layout) - scroll_area.setWidget(document_container) - main_layout.addWidget(scroll_area) + self.scroll_area.setWidget(document_container) + main_layout.addWidget(self.scroll_area) def init_network_services(self): """初始化网络服务""" @@ -1567,132 +1838,59 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage("打印布局功能开发中...", 3000) def set_view_mode(self, mode): - """设置视图模式 - 实现文档A和学习文档C的正确交互""" + """设置视图模式 - 打开学习模式窗口或切换到打字模式""" if mode not in ["typing", "learning"]: return - - # 保存当前模式的内容 - current_content = self.text_edit.toPlainText() - - # 根据当前模式保存特定信息 - if self.view_mode == "typing": - # 打字模式:保存打字内容到文档A - self.typing_mode_content = current_content - print(f"保存打字模式内容到文档A,长度: {len(self.typing_mode_content)}") - elif self.view_mode == "learning": - # 学习模式:保存当前学习进度和内容 - self.learning_text = current_content - if hasattr(self, 'displayed_chars') and self.imported_content: - self.learning_progress = self.displayed_chars - print(f"保存学习模式内容,学习进度: {self.learning_progress}/{len(self.imported_content) if self.imported_content else 0}") - # 更新模式 - self.view_mode = mode - self.last_edit_mode = mode - - # 更新菜单项状态 - self.typing_mode_action.setChecked(mode == "typing") - self.learning_mode_action.setChecked(mode == "learning") - - # 临时禁用文本变化信号,避免递归 - self.text_edit.textChanged.disconnect(self.on_text_changed) - - try: - if mode == "typing": - # 打字模式:显示学习模式下已输入的内容(文档A) - self.status_bar.showMessage("切换到打字模式 - 显示已输入的内容", 3000) - - # 设置文档A的内容(学习模式下已输入的内容) - self.text_edit.clear() - if hasattr(self, 'learning_progress') and self.learning_progress > 0: - # 显示学习模式下已输入的内容 - display_text = self.imported_content[:self.learning_progress] - self.text_edit.setPlainText(display_text) - else: - # 如果没有学习进度,显示默认提示 - self.text_edit.setPlainText("请先在学习模式下输入内容") + if mode == "learning": + # 学习模式:打开新的学习模式窗口 + try: + from learning_mode_window import LearningModeWindow - # 设置光标位置到文档末尾 - cursor = self.text_edit.textCursor() - cursor.movePosition(cursor.End) - self.text_edit.setTextCursor(cursor) + # 准备传递给学习窗口的参数 + imported_content = "" + current_position = 0 - # 重置打字逻辑,准备接受新的输入 - if self.typing_logic: + # 如果有已导入的内容,传递给学习窗口 + if hasattr(self, 'imported_content') and self.imported_content: + imported_content = self.imported_content if hasattr(self, 'learning_progress') and self.learning_progress > 0: - # 使用已输入的内容作为打字逻辑的基础 - self.typing_logic.reset(self.imported_content) - # 设置当前位置到已输入的末尾 - self.typing_logic.current_index = self.learning_progress - self.typing_logic.typed_chars = self.learning_progress - else: - self.typing_logic.reset("") + current_position = self.learning_progress - # 重置显示字符计数 - self.displayed_chars = 0 + # 创建学习模式窗口,直接传递导入内容 + self.learning_window = LearningModeWindow(self, imported_content, current_position) - # 重置图片插入记录 - if hasattr(self, 'inserted_images'): - self.inserted_images.clear() + # 显示学习模式窗口 + self.learning_window.show() - elif mode == "learning": - # 学习模式:从上次中断的地方继续显示学习文档C的内容 - if not self.imported_content: - self.status_bar.showMessage("学习模式需要导入文件 - 请先打开一个文件", 3000) - # 清空文本编辑器,等待导入文件 - self.text_edit.clear() - if self.typing_logic: - self.typing_logic.reset("在此输入您的内容...") - # 重置显示字符计数 - self.displayed_chars = 0 - # 重置图片插入记录 - if hasattr(self, 'inserted_images'): - self.inserted_images.clear() - else: - # 检查是否有保存的学习进度 - if hasattr(self, 'learning_progress') and self.learning_progress > 0: - self.status_bar.showMessage(f"切换到学习模式 - 从上次中断处继续 ({self.learning_progress}/{len(self.imported_content)})", 3000) - # 恢复学习进度 - self.displayed_chars = self.learning_progress - - # 重置打字逻辑,准备接受导入内容 - if self.typing_logic: - self.typing_logic.reset(self.imported_content) - - # 获取应该显示的学习文档C的内容部分 - display_text = self.imported_content[:self.displayed_chars] - - # 设置文本内容 - self.text_edit.clear() - self.text_edit.setPlainText(display_text) - - # 设置光标位置到文本末尾 - cursor = self.text_edit.textCursor() - cursor.movePosition(cursor.End) - self.text_edit.setTextCursor(cursor) - - # 重新插入图片 - if hasattr(self, 'inserted_images'): - self.inserted_images.clear() - self.insert_images_in_text() - else: - self.status_bar.showMessage("切换到学习模式 - 准备显示学习文档C内容", 3000) - # 重置打字逻辑,准备接受导入内容 - if self.typing_logic: - self.typing_logic.reset(self.imported_content) - - # 重置显示字符计数 - self.displayed_chars = 0 - - # 清空文本编辑器,等待用户开始打字 - self.text_edit.clear() - - # 重置图片插入记录 - if hasattr(self, 'inserted_images'): - self.inserted_images.clear() - finally: - # 重新连接文本变化信号 - self.text_edit.textChanged.connect(self.on_text_changed) + # 更新菜单状态 + self.learning_mode_action.setChecked(True) + self.typing_mode_action.setChecked(False) + + # 切换到打字模式(主窗口保持打字模式) + self.view_mode = "typing" + + self.status_bar.showMessage("学习模式窗口已打开", 3000) + + except ImportError as e: + QMessageBox.critical(self, "错误", f"无法加载学习模式窗口:\n{str(e)}") + except Exception as e: + QMessageBox.critical(self, "错误", f"打开学习模式窗口时出错:\n{str(e)}") + + elif mode == "typing": + # 打字模式:保持当前窗口状态 + self.view_mode = "typing" + self.typing_mode_action.setChecked(True) + self.learning_mode_action.setChecked(False) + self.status_bar.showMessage("当前为打字模式", 2000) + + def on_learning_mode_closed(self): + """学习模式窗口关闭时的回调""" + # 重置菜单状态 + self.learning_mode_action.setChecked(False) + self.typing_mode_action.setChecked(True) + self.view_mode = "typing" + self.status_bar.showMessage("学习模式窗口已关闭", 2000) def set_page_color(self, color): """设置页面颜色""" @@ -2009,10 +2207,59 @@ class WordStyleMainWindow(QMainWindow): def show_about(self): """显示关于对话框""" - QMessageBox.about( - self, "关于 MagicWord", - "MagicWord - 隐私学习软件\n\n" - "版本: 2.0\n" + # 创建自定义对话框 + dialog = QDialog(self) + dialog.setWindowTitle("关于 MagicWord") + dialog.setModal(True) + dialog.setFixedSize(500, 400) + + # 创建布局 + layout = QVBoxLayout() + + # 添加图标和标题布局 + header_layout = QHBoxLayout() + + # 添加应用程序图标 + try: + icon_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources", "icons", "app_icon_128X128.png") + if os.path.exists(icon_path): + icon_label = QLabel() + icon_label.setAlignment(Qt.AlignCenter) + pixmap = QPixmap(icon_path) + if not pixmap.isNull(): + # 缩放图标到合适大小 + scaled_pixmap = pixmap.scaled(80, 80, Qt.KeepAspectRatio, Qt.SmoothTransformation) + icon_label.setPixmap(scaled_pixmap) + header_layout.addWidget(icon_label) + except Exception as e: + print(f"加载图标失败: {e}") + + # 添加标题和版本信息 + title_layout = QVBoxLayout() + title_label = QLabel("MagicWord") + title_label.setStyleSheet("font-size: 24px; font-weight: bold; color: #0078d7;") + title_label.setAlignment(Qt.AlignCenter) + + version_label = QLabel("版本 1.0") + version_label.setStyleSheet("font-size: 14px; color: #666;") + version_label.setAlignment(Qt.AlignCenter) + + title_layout.addWidget(title_label) + title_layout.addWidget(version_label) + header_layout.addLayout(title_layout) + + layout.addLayout(header_layout) + + # 添加分隔线 + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Sunken) + separator.setStyleSheet("color: #ddd;") + layout.addWidget(separator) + + # 添加关于信息 + about_text = QLabel( + "隐私学习软件\n\n" "基于 Microsoft Word 界面设计\n\n" "功能特色:\n" "• 仿Word界面设计\n" @@ -2021,6 +2268,79 @@ class WordStyleMainWindow(QMainWindow): "• 实时进度跟踪\n" "• 天气和名言显示" ) + about_text.setAlignment(Qt.AlignCenter) + about_text.setStyleSheet("font-size: 12px; line-height: 1.5;") + layout.addWidget(about_text) + + # 添加按钮布局 + button_layout = QHBoxLayout() + button_layout.addStretch() # 添加弹性空间使按钮居中 + + # 添加"沃式烁"按钮 + woshishuo_button = QPushButton("沃式烁") + woshishuo_button.clicked.connect(lambda: self.show_woshishuo_image()) + button_layout.addWidget(woshishuo_button) + + # 添加OK按钮 + ok_button = QPushButton("OK") + ok_button.clicked.connect(dialog.accept) + button_layout.addWidget(ok_button) + + button_layout.addStretch() # 添加弹性空间使按钮居中 + + layout.addLayout(button_layout) + dialog.setLayout(layout) + + # 显示对话框 + dialog.exec_() + + def show_woshishuo_image(self): + """显示沃式烁图片""" + try: + # 图片路径 + image_path = os.path.join(os.path.dirname(__file__), "ui", "114514.png") + + # 检查图片是否存在 + if not os.path.exists(image_path): + QMessageBox.warning(self, "错误", "图片文件不存在: 114514.png") + return + + # 创建图片查看对话框 + image_dialog = QDialog(self) + image_dialog.setWindowTitle("沃式烁") + image_dialog.setModal(True) + image_dialog.setFixedSize(500, 400) + + # 创建布局 + layout = QVBoxLayout() + + # 添加图片标签 + image_label = QLabel() + image_label.setAlignment(Qt.AlignCenter) + image_label.setScaledContents(True) + + # 加载图片 + pixmap = QPixmap(image_path) + if pixmap.isNull(): + QMessageBox.warning(self, "错误", "无法加载图片文件") + return + + # 缩放图片以适应对话框 + scaled_pixmap = pixmap.scaled(450, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation) + image_label.setPixmap(scaled_pixmap) + + layout.addWidget(image_label) + + # 添加关闭按钮 + close_button = QPushButton("关闭") + close_button.clicked.connect(image_dialog.accept) + layout.addWidget(close_button) + + image_dialog.setLayout(layout) + image_dialog.exec_() + + except Exception as e: + QMessageBox.warning(self, "错误", f"显示图片时出错: {str(e)}") def highlight_next_char(self, position, expected_char): """高亮显示下一个期望字符""" @@ -2067,50 +2387,137 @@ class WordStyleMainWindow(QMainWindow): except Exception as e: self.status_bar.showMessage(f"显示图片失败: {str(e)}", 3000) + def set_light_mode(self): + """设置为白色模式""" + theme_manager.set_dark_theme(False) + self.light_mode_action.setChecked(True) + self.dark_mode_action.setChecked(False) + self.status_bar.showMessage("已切换到白色模式", 2000) + + def set_dark_mode(self): + """设置为黑色模式""" + theme_manager.set_dark_theme(True) + self.light_mode_action.setChecked(False) + self.dark_mode_action.setChecked(True) + self.status_bar.showMessage("已切换到黑色模式", 2000) + def show_image_viewer(self, filename, image_data): - """显示图片查看器""" + """显示图片查看器 - 图片真正铺满整个窗口上方""" try: - # 创建图片查看窗口 + # 创建自定义图片查看窗口 viewer = QDialog(self) viewer.setWindowTitle(f"图片查看 - {filename}") viewer.setModal(False) - viewer.resize(800, 600) - # 创建布局 - layout = QVBoxLayout() + # 移除窗口边框和标题栏装饰,设置为工具窗口样式 + viewer.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - # 创建滚动区域 - scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) + # 设置窗口背景为黑色,完全无边距 + viewer.setStyleSheet(""" + QDialog { + background-color: #000000; + border: none; + margin: 0px; + padding: 0px; + } + """) + + # 创建布局,完全移除边距 + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) # 移除布局边距 + layout.setSpacing(0) # 移除组件间距 + layout.setAlignment(Qt.AlignCenter) # 布局居中对齐 - # 创建图片标签 + # 创建图片标签,设置为完全填充模式 image_label = QLabel() image_label.setAlignment(Qt.AlignCenter) + image_label.setScaledContents(True) # 关键:允许图片缩放以填充标签 + image_label.setMinimumSize(1, 1) # 设置最小尺寸 + image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # 设置大小策略为扩展 + image_label.setStyleSheet(""" + QLabel { + border: none; + margin: 0px; + padding: 0px; + background-color: #000000; + } + """) # 加载图片 pixmap = QPixmap() - if pixmap.loadFromData(image_data): - image_label.setPixmap(pixmap) - image_label.setScaledContents(False) - - # 设置标签大小为图片实际大小 - image_label.setFixedSize(pixmap.size()) - else: - image_label.setText("无法加载图片") + if not pixmap.loadFromData(image_data): + self.status_bar.showMessage(f"加载图片失败: {filename}", 3000) + return - scroll_area.setWidget(image_label) - layout.addWidget(scroll_area) + layout.addWidget(image_label) + viewer.setLayout(layout) - # 添加关闭按钮 - close_btn = QPushButton("关闭") - close_btn.clicked.connect(viewer.close) - layout.addWidget(close_btn) + # 计算位置和大小 + if self: + parent_geometry = self.geometry() + screen_geometry = QApplication.primaryScreen().geometry() + + # 设置窗口宽度与主窗口相同,高度为屏幕高度的40% + window_width = parent_geometry.width() + window_height = int(screen_geometry.height() * 0.4) + + # 计算位置:显示在主窗口正上方 + x = parent_geometry.x() + y = parent_geometry.y() - window_height + + # 确保不会超出屏幕边界 + if y < screen_geometry.top(): + y = parent_geometry.y() + 50 # 如果上方空间不足,显示在下方 + + # 调整宽度确保不超出屏幕 + if x + window_width > screen_geometry.right(): + window_width = screen_geometry.right() - x + + viewer.setGeometry(x, y, window_width, window_height) - viewer.setLayout(layout) viewer.show() + # 关键:强制图片立即填充整个标签区域 + def force_image_fill(): + try: + if pixmap and not pixmap.isNull(): + # 获取标签的实际大小 + label_size = image_label.size() + if label_size.width() > 10 and label_size.height() > 10: # 确保尺寸有效 + # 完全填充,忽略宽高比,真正铺满 + scaled_pixmap = pixmap.scaled( + label_size, + Qt.IgnoreAspectRatio, # 关键:忽略宽高比,强制填充 + Qt.SmoothTransformation + ) + image_label.setPixmap(scaled_pixmap) + print(f"图片已强制缩放至 {label_size.width()}x{label_size.height()}") + + # 确保标签完全填充布局 + image_label.setMinimumSize(label_size) + except Exception as e: + print(f"图片缩放失败: {e}") + + # 使用多个定时器确保图片正确填充 + from PyQt5.QtCore import QTimer + QTimer.singleShot(50, force_image_fill) # 50毫秒后执行 + QTimer.singleShot(200, force_image_fill) # 200毫秒后执行 + QTimer.singleShot(1000, force_image_fill) # 1000毫秒后再执行一次 + + # 连接窗口大小变化事件 + viewer.resizeEvent = lambda event: force_image_fill() + + # 添加点击关闭功能 + def close_viewer(): + viewer.close() + + image_label.mousePressEvent = lambda event: close_viewer() + viewer.mousePressEvent = lambda event: close_viewer() + except Exception as e: self.status_bar.showMessage(f"创建图片查看器失败: {str(e)}", 3000) + import traceback + traceback.print_exc() def insert_images_in_text(self): """在文本中插入图片 - 修复图片显示逻辑"""