From 9588630f515543d4bc62ea86aae6f898095f0e13 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 17 Nov 2025 11:19:20 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E6=8F=92=E5=85=A5=E5=A4=A9=E6=B0=94?= =?UTF-8?q?=E3=80=81=E6=AF=8F=E6=97=A5=E8=B0=8F=E8=A8=80=E3=80=81=E6=97=A5?= =?UTF-8?q?=E5=8E=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/network_service.py | 56 ++++++++++++++++- src/word_main_window.py | 104 ++++++++++++++++++++++++++++++++ test_quote.py | 44 ++++++++++++++ test_weather_insert.py | 60 ++++++++++++++++++ 4 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 test_quote.py create mode 100644 test_weather_insert.py diff --git a/src/services/network_service.py b/src/services/network_service.py index 60e7f4a..e67dedc 100644 --- a/src/services/network_service.py +++ b/src/services/network_service.py @@ -51,7 +51,61 @@ class NetworkService: # 5. 返回天气信息字典 return formatted_weather else: - # 模拟天气数据(无API密钥时) + # 当没有API密钥时,使用免费的天气API获取真实数据 + # 首先尝试获取城市ID(需要映射城市名到ID) + city_id_map = { + "Beijing": "101010100", + "Shanghai": "101020100", + "Tianjin": "101030100", + "Chongqing": "101040100", + "Hong Kong": "101320101", + "Macau": "101330101" + } + + # 尝试映射英文城市名到ID + city_id = city_id_map.get(city) + + # 如果找不到映射,尝试直接使用城市名 + if not city_id: + # 对于中国主要城市,直接使用拼音映射 + city_pinyin_map = { + "Beijing": "北京", + "Shanghai": "上海", + "Tianjin": "天津", + "Chongqing": "重庆" + } + chinese_city = city_pinyin_map.get(city, city) + + # 使用免费天气API + try: + # 使用和风天气免费API的替代方案 - sojson天气API + weather_url = f"http://t.weather.sojson.com/api/weather/city/101010100" # 默认北京 + weather_response = self.session.get(weather_url, timeout=5, verify=False) + weather_data = weather_response.json() + + if weather_data.get("status") == 200: + # 解析天气数据 + current_data = weather_data.get("data", {}) + wendu = current_data.get("wendu", "N/A") + shidu = current_data.get("shidu", "N/A") + forecast = current_data.get("forecast", []) + + # 获取第一个预报项作为当前天气 + current_weather = forecast[0] if forecast else {} + weather_type = current_weather.get("type", "晴") + + formatted_weather = { + "city": city, + "temperature": float(wendu) if wendu != "N/A" else 20, + "description": weather_type, + "humidity": shidu.replace("%", "") if shidu != "N/A" else "60", + "wind_speed": "3.5" # 默认风速 + } + return formatted_weather + except Exception as e: + print(f"获取免费天气数据时出错: {e}") + + # 如果以上都失败,返回默认数据 return { "city": city, "temperature": 20, diff --git a/src/word_main_window.py b/src/word_main_window.py index 80508e8..04341b2 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -694,6 +694,21 @@ class WordStyleMainWindow(QMainWindow): insert_image_action.triggered.connect(self.insert_image_in_typing_mode) insert_menu.addAction(insert_image_action) + # 插入天气信息功能 + insert_weather_action = QAction('插入天气信息', self) + insert_weather_action.triggered.connect(self.insert_weather_info) + insert_menu.addAction(insert_weather_action) + + # 插入每日一句名言功能 + insert_quote_action = QAction('插入每日一句名言', self) + insert_quote_action.triggered.connect(self.insert_daily_quote) + insert_menu.addAction(insert_quote_action) + + # 插入古诗词功能 + insert_poetry_action = QAction('插入古诗词', self) + insert_poetry_action.triggered.connect(self.insert_chinese_poetry) + insert_menu.addAction(insert_poetry_action) + # 绘图菜单 paint_menu = menubar.addMenu('绘图(D)') @@ -3515,6 +3530,95 @@ class WordStyleMainWindow(QMainWindow): self.calendar_widget.show() self.calendar_widget.raise_() # 确保日历组件在最上层显示 + def insert_weather_info(self): + """在光标位置插入天气信息""" + # 检查是否处于打字模式 + if self.view_mode != "typing": + self.status_bar.showMessage("请在打字模式下使用插入天气信息功能", 3000) + return + + # 检查是否已经定位了天气(即是否有有效的天气数据) + if not hasattr(self, 'current_weather_data') or not self.current_weather_data: + # 弹出对话框提示用户先定位天气 + QMessageBox.information(self, "附加工具", "先定位天气") + return + + try: + # 直接使用已经获取到的天气数据 + weather_data = self.current_weather_data + + # 格式化天气信息 + if weather_data: + # 处理嵌套的天气数据结构 + city = weather_data.get('city', '未知城市') + current_data = weather_data.get('current', {}) + temp = current_data.get('temp', 'N/A') + desc = current_data.get('weather', 'N/A') + weather_info = f"天气: {desc}, 温度: {temp}°C, 城市: {city}" + else: + weather_info = "天气信息获取失败" + + # 在光标位置插入天气信息 + cursor = self.text_edit.textCursor() + cursor.insertText(weather_info) + + # 更新状态栏 + self.status_bar.showMessage("已插入天气信息", 2000) + + except Exception as e: + QMessageBox.warning(self, "错误", f"插入天气信息失败: {str(e)}") + + def insert_daily_quote(self): + """在光标位置插入每日一句名言""" + # 检查是否处于打字模式 + if self.view_mode != "typing": + self.status_bar.showMessage("请在打字模式下使用插入每日一句名言功能", 3000) + return + + try: + # 使用与Ribbon界面相同的API获取每日一言,确保内容一致 + from ui.word_style_ui import daily_sentence_API + quote_api = daily_sentence_API("https://api.nxvav.cn/api/yiyan") + quote_data = quote_api.get_sentence('json') + + # 处理获取到的数据 + if quote_data and isinstance(quote_data, dict): + quote_text = quote_data.get('yiyan', '暂无每日一言') + quote_info = quote_text + else: + quote_info = "每日一句名言获取失败" + + # 在光标位置插入名言信息 + cursor = self.text_edit.textCursor() + cursor.insertText(quote_info) + + # 更新状态栏 + self.status_bar.showMessage("已插入每日一句名言", 2000) + + except Exception as e: + QMessageBox.warning(self, "错误", f"插入每日一句名言失败: {str(e)}") + + def insert_chinese_poetry(self): + """在光标位置插入古诗词""" + # 检查是否处于打字模式 + if self.view_mode != "typing": + self.status_bar.showMessage("请在打字模式下使用插入古诗词功能", 3000) + return + + try: + # 获取古诗词 + poetry_data = self.ribbon.get_chinese_poetry() + + # 在光标位置插入古诗词 + cursor = self.text_edit.textCursor() + cursor.insertText(poetry_data) + + # 更新状态栏 + self.status_bar.showMessage("已插入古诗词", 2000) + + except Exception as e: + QMessageBox.warning(self, "错误", f"插入古诗词失败: {str(e)}") + if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/test_quote.py b/test_quote.py new file mode 100644 index 0000000..569ff84 --- /dev/null +++ b/test_quote.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import os + +# 添加src目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from ui.word_style_ui import daily_sentence_API +from services.network_service import NetworkService + +def test_daily_sentence_api(): + print("测试 daily_sentence_API 类...") + try: + # 使用与Ribbon界面相同的API获取每日一言 + quote_api = daily_sentence_API("https://api.nxvav.cn/api/yiyan") + quote_data = quote_api.get_sentence('json') + + print("API返回的数据:") + print(quote_data) + + # 处理获取到的数据 + if quote_data and isinstance(quote_data, dict): + quote_text = quote_data.get('yiyan', '暂无每日一言') + print(f"解析后的每日一言: {quote_text}") + else: + print("获取每日一言失败") + + except Exception as e: + print(f"测试 daily_sentence_API 类时出错: {e}") + +def test_network_service_quote(): + print("\n测试 NetworkService 类的 get_daily_quote 方法...") + try: + network_service = NetworkService() + quote = network_service.get_daily_quote() + print(f"NetworkService 获取的每日一言: {quote}") + except Exception as e: + print(f"测试 NetworkService 类时出错: {e}") + +if __name__ == "__main__": + test_daily_sentence_api() + test_network_service_quote() \ No newline at end of file diff --git a/test_weather_insert.py b/test_weather_insert.py new file mode 100644 index 0000000..c2d0d1b --- /dev/null +++ b/test_weather_insert.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import os + +# 添加src目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from word_main_window import WordStyleMainWindow +from PyQt5.QtWidgets import QApplication, QMessageBox +from unittest.mock import patch + +def test_insert_weather_without_location(): + """测试在未定位天气时插入天气信息""" + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + # 创建主窗口实例 + window = WordStyleMainWindow() + + # 模拟没有定位天气的情况(删除current_weather_data属性) + if hasattr(window, 'current_weather_data'): + delattr(window, 'current_weather_data') + + # 模拟用户点击插入天气信息按钮 + with patch('PyQt5.QtWidgets.QMessageBox.information') as mock_info: + window.insert_weather_info() + # 验证是否弹出了"先定位天气"对话框 + mock_info.assert_called_once_with(window, "附加工具", "先定位天气") + + print("测试通过:未定位天气时正确弹出提示对话框") + +def test_insert_weather_with_location(): + """测试在已定位天气时插入天气信息""" + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + # 创建主窗口实例 + window = WordStyleMainWindow() + + # 模拟已定位天气的情况 + window.current_weather_data = { + 'city': '北京', + 'temperature': 25, + 'description': '晴天' + } + + # 模拟用户点击插入天气信息按钮 + # 注意:这个测试不会真正插入文本,因为我们没有设置完整的UI环境 + window.insert_weather_info() + + print("测试通过:已定位天气时正确执行插入操作") + +if __name__ == "__main__": + test_insert_weather_without_location() + test_insert_weather_with_location() + print("所有测试完成!") \ No newline at end of file -- 2.34.1 From 2cdf760dd16f2417abb12b152d4cb536ba8007b0 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 17 Nov 2025 11:27:53 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E6=97=A5=E5=8E=86=E9=BB=91=E7=99=BD?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/calendar_widget.py | 293 ++++++++++++++++++++++++++++++++++++++ src/word_main_window.py | 5 + 2 files changed, 298 insertions(+) diff --git a/src/ui/calendar_widget.py b/src/ui/calendar_widget.py index ed66583..5331faf 100644 --- a/src/ui/calendar_widget.py +++ b/src/ui/calendar_widget.py @@ -13,6 +13,9 @@ from PyQt5.QtWidgets import ( from PyQt5.QtCore import QDate, Qt, pyqtSignal from PyQt5.QtGui import QFont +# 导入主题管理器 +from .theme_manager import theme_manager + class CalendarWidget(QWidget): """日历组件类""" @@ -24,6 +27,7 @@ class CalendarWidget(QWidget): super().__init__(parent) self.setup_ui() self.setup_connections() + self.setup_theme() def setup_ui(self): """设置UI界面""" @@ -266,6 +270,295 @@ class CalendarWidget(QWidget): date = QDate.fromString(date, "yyyy-MM-dd") self.calendar.setSelectedDate(date) self.update_date_label(date) + + def setup_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() + + if is_dark: + # 深色主题样式 + self.setStyleSheet(""" + QWidget { + background-color: #2c2c2e; + color: #f0f0f0; + } + """) + + # 更新日历控件样式 + self.calendar.setStyleSheet(""" + QCalendarWidget { + background-color: #2c2c2e; + border: 1px solid #404040; + border-radius: 4px; + } + QCalendarWidget QToolButton { + height: 30px; + width: 80px; + color: #f0f0f0; + font-size: 12px; + font-weight: bold; + background-color: #3a3a3c; + border: 1px solid #4a4a4c; + border-radius: 4px; + } + QCalendarWidget QToolButton:hover { + background-color: #4a4a4c; + } + QCalendarWidget QMenu { + width: 150px; + left: 20px; + color: #f0f0f0; + font-size: 12px; + background-color: #3a3a3c; + border: 1px solid #4a4a4c; + } + QCalendarWidget QSpinBox { + width: 80px; + font-size: 12px; + background-color: #3a3a3c; + selection-background-color: #0a84ff; + selection-color: #ffffff; + border: 1px solid #4a4a4c; + border-radius: 4px; + color: #f0f0f0; + } + QCalendarWidget QSpinBox::up-button { + subcontrol-origin: border; + subcontrol-position: top right; + width: 20px; + } + QCalendarWidget QSpinBox::down-button { + subcontrol-origin: border; + subcontrol-position: bottom right; + width: 20px; + } + QCalendarWidget QSpinBox::up-arrow { + width: 10px; + height: 10px; + } + QCalendarWidget QSpinBox::down-arrow { + width: 10px; + height: 10px; + } + QCalendarWidget QWidget { + alternate-background-color: #3a3a3c; + } + QCalendarWidget QAbstractItemView:enabled { + font-size: 12px; + selection-background-color: #0a84ff; + selection-color: #ffffff; + background-color: #2c2c2e; + color: #f0f0f0; + } + QCalendarWidget QWidget#qt_calendar_navigationbar { + background-color: #3a3a3c; + } + """) + + # 更新标签样式 + self.date_label.setStyleSheet("QLabel { color: #a0a0a0; }") + + # 更新按钮样式 + self.close_btn.setStyleSheet(""" + QPushButton { + background-color: #3a3a3c; + border: 1px solid #4a4a4c; + border-radius: 12px; + font-size: 16px; + font-weight: bold; + color: #f0f0f0; + } + QPushButton:hover { + background-color: #4a4a4c; + } + """) + + self.today_btn.setStyleSheet(""" + QPushButton { + background-color: #0a84ff; + color: white; + border: none; + border-radius: 4px; + padding: 5px 10px; + } + QPushButton:hover { + background-color: #0066cc; + } + """) + + self.clear_btn.setStyleSheet(""" + QPushButton { + background-color: #3a3a3c; + color: #f0f0f0; + border: 1px solid #4a4a4c; + border-radius: 4px; + padding: 5px 10px; + } + QPushButton:hover { + background-color: #4a4a4c; + } + """) + + self.insert_btn.setStyleSheet(""" + QPushButton { + background-color: #32d74b; + color: #000000; + border: none; + border-radius: 4px; + padding: 5px 10px; + } + QPushButton:hover { + background-color: #24b334; + } + """) + else: + # 浅色主题样式 + self.setStyleSheet(""" + QWidget { + background-color: white; + color: #333333; + } + """) + + # 更新日历控件样式 + self.calendar.setStyleSheet(""" + QCalendarWidget { + background-color: white; + border: 1px solid #ccc; + border-radius: 4px; + } + QCalendarWidget QToolButton { + height: 30px; + width: 80px; + color: #333; + font-size: 12px; + font-weight: bold; + background-color: #f0f0f0; + border: 1px solid #ccc; + border-radius: 4px; + } + QCalendarWidget QToolButton:hover { + background-color: #e0e0e0; + } + QCalendarWidget QMenu { + width: 150px; + left: 20px; + color: #333; + font-size: 12px; + background-color: white; + border: 1px solid #ccc; + } + QCalendarWidget QSpinBox { + width: 80px; + font-size: 12px; + background-color: #f0f0f0; + selection-background-color: #0078d7; + selection-color: white; + border: 1px solid #ccc; + border-radius: 4px; + color: #333; + } + QCalendarWidget QSpinBox::up-button { + subcontrol-origin: border; + subcontrol-position: top right; + width: 20px; + } + QCalendarWidget QSpinBox::down-button { + subcontrol-origin: border; + subcontrol-position: bottom right; + width: 20px; + } + QCalendarWidget QSpinBox::up-arrow { + width: 10px; + height: 10px; + } + QCalendarWidget QSpinBox::down-arrow { + width: 10px; + height: 10px; + } + QCalendarWidget QWidget { + alternate-background-color: #f0f0f0; + } + QCalendarWidget QAbstractItemView:enabled { + font-size: 12px; + selection-background-color: #0078d7; + selection-color: white; + background-color: white; + color: #333; + } + QCalendarWidget QWidget#qt_calendar_navigationbar { + background-color: #f8f8f8; + } + """) + + # 更新标签样式 + self.date_label.setStyleSheet("QLabel { color: #666; }") + + # 更新按钮样式 + self.close_btn.setStyleSheet(""" + QPushButton { + background-color: #f0f0f0; + border: 1px solid #ccc; + border-radius: 12px; + font-size: 16px; + font-weight: bold; + color: #333; + } + QPushButton:hover { + background-color: #e0e0e0; + } + """) + + self.today_btn.setStyleSheet(""" + QPushButton { + background-color: #0078d7; + color: white; + border: none; + border-radius: 4px; + padding: 5px 10px; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.clear_btn.setStyleSheet(""" + QPushButton { + background-color: #f0f0f0; + color: #333; + border: 1px solid #ccc; + border-radius: 4px; + padding: 5px 10px; + } + QPushButton:hover { + background-color: #e0e0e0; + } + """) + + self.insert_btn.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + color: white; + border: none; + border-radius: 4px; + padding: 5px 10px; + } + QPushButton:hover { + background-color: #45a049; + } + """) + + def on_theme_changed(self, is_dark): + """主题切换槽函数""" + self.apply_theme() if __name__ == "__main__": diff --git a/src/word_main_window.py b/src/word_main_window.py index 04341b2..dbf2859 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -323,6 +323,11 @@ class WordStyleMainWindow(QMainWindow): # 更新功能区下拉框样式 if hasattr(self, 'ribbon'): self.update_ribbon_styles(is_dark) + + # 更新日历组件样式 + if hasattr(self, 'calendar_widget') and self.calendar_widget is not None: + # 日历组件有自己的主题管理机制,只需触发其主题更新 + self.calendar_widget.apply_theme() def update_ribbon_styles(self, is_dark): """更新功能区样式""" -- 2.34.1 From 43c0bd4cb6b805e36be6b5d14cc1b82368eb4025 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 17 Nov 2025 11:36:24 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E5=B0=8F=E4=BF=AE=E8=A1=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/word_main_window.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/word_main_window.py b/src/word_main_window.py index dbf2859..3e2a48c 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -3299,6 +3299,8 @@ class WordStyleMainWindow(QMainWindow): if file_path: try: + import os + import tempfile from docx import Document from docx.shared import Inches @@ -3323,10 +3325,6 @@ class WordStyleMainWindow(QMainWindow): break if img_data: - # 创建临时图片文件 - import tempfile - import os - # 检测图片类型 if img_name.lower().endswith('.png'): img_ext = '.png' -- 2.34.1 From 029ac389f8ff5b92e10d7555f154ca1806007fab Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Wed, 19 Nov 2025 08:56:51 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E4=BF=AE=E8=A1=A5=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/theme_manager.py | 2 -- src/ui/word_style_ui.py | 78 +++++++++++------------------------------ src/word_main_window.py | 1 - 3 files changed, 21 insertions(+), 60 deletions(-) diff --git a/src/ui/theme_manager.py b/src/ui/theme_manager.py index 988b05a..0978872 100644 --- a/src/ui/theme_manager.py +++ b/src/ui/theme_manager.py @@ -206,7 +206,6 @@ class ThemeManager(QObject): color: #f0f0f0; padding: 4px 0; margin: 2px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } QMenu::item { @@ -528,7 +527,6 @@ class ThemeManager(QObject): color: #333333; padding: 4px 0; margin: 2px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } QMenu::item { diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 40938a9..9b21604 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -158,13 +158,9 @@ class WordRibbon(QFrame): font_layout.addWidget(self.font_size_combo) self.bold_btn = self.create_toggle_button("B", "bold") - self.bold_btn.clicked.connect(self.on_bold_clicked) self.italic_btn = self.create_toggle_button("I", "italic") - self.italic_btn.clicked.connect(self.on_italic_clicked) self.underline_btn = self.create_toggle_button("U", "underline") - self.underline_btn.clicked.connect(self.on_underline_clicked) self.color_btn = self.create_color_button("A", "color") - self.color_btn.clicked.connect(self.on_color_clicked) font_layout.addWidget(self.bold_btn) font_layout.addWidget(self.italic_btn) @@ -192,6 +188,26 @@ class WordRibbon(QFrame): self.init_style_preview_group() self.add_separator() + # ---------- 快速样式组 ---------- + quick_style_group, quick_style_layout = self.create_ribbon_group("快速样式") + + # 创建样式按钮 + self.heading1_btn = self.create_style_button("标题1") + self.heading2_btn = self.create_style_button("标题2") + self.heading3_btn = self.create_style_button("标题3") + self.heading4_btn = self.create_style_button("标题4") + self.body_text_btn = self.create_style_button("正文") + + # 添加到布局 + quick_style_layout.addWidget(self.heading1_btn) + quick_style_layout.addWidget(self.heading2_btn) + quick_style_layout.addWidget(self.heading3_btn) + quick_style_layout.addWidget(self.heading4_btn) + quick_style_layout.addWidget(self.body_text_btn) + quick_style_layout.addStretch() + layout.addWidget(quick_style_group) + self.add_separator() + # ---------- 编辑组 ---------- @@ -221,57 +237,7 @@ class WordRibbon(QFrame): """字体大小变化处理""" pass - def on_bold_clicked(self): - """粗体按钮点击处理""" - pass - - def on_italic_clicked(self): - """斜体按钮点击处理""" - pass - - def on_underline_clicked(self): - """下划线按钮点击处理""" - pass - - def on_color_clicked(self): - """字体颜色按钮点击处理""" - pass - - def on_heading1_clicked(self): - """一级标题按钮点击处理""" - pass - - def on_heading2_clicked(self): - """二级标题按钮点击处理""" - pass - - def on_heading3_clicked(self): - """三级标题按钮点击处理""" - pass - - def on_heading4_clicked(self): - """四级标题按钮点击处理""" - pass - - def on_body_text_clicked(self): - """正文按钮点击处理""" - pass - - def on_align_left_clicked(self): - """左对齐按钮点击处理""" - pass - - def on_align_center_clicked(self): - """居中对齐按钮点击处理""" - pass - - def on_align_right_clicked(self): - """右对齐按钮点击处理""" - pass - - def on_align_justify_clicked(self): - """两端对齐按钮点击处理""" - pass + def init_style_preview_group(self): """加入 Word 风格的样式预览区域""" @@ -750,7 +716,6 @@ class WordRibbon(QFrame): # 刷新按钮 self.refresh_weather_btn = QPushButton("🔄 刷新") - self.refresh_weather_btn.clicked.connect(self.on_refresh_weather) self.refresh_weather_btn.setFixedSize(60, 30) # 增大刷新按钮尺寸 self.refresh_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }") self.refresh_weather_btn.setToolTip("刷新天气") @@ -805,7 +770,6 @@ class WordRibbon(QFrame): # 刷新按钮 self.refresh_quote_btn = QPushButton("刷新箴言") - self.refresh_quote_btn.clicked.connect(self.on_refresh_quote) self.refresh_quote_btn.setFixedSize(80, 25) # 添加到第一行布局 diff --git a/src/word_main_window.py b/src/word_main_window.py index 3e2a48c..3bf8569 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -3172,7 +3172,6 @@ class WordStyleMainWindow(QMainWindow): max-width: 100%; height: auto; border-radius: 3px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); }} .image-caption {{ margin-top: 8px; -- 2.34.1 From 7642789edc7c24739fb79e1d95066abd8c6076ff Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 15:35:25 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E6=97=A5=E5=8E=86UI=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/word_main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/word_main_window.py b/src/word_main_window.py index 3bf8569..9bec582 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -3513,7 +3513,7 @@ class WordStyleMainWindow(QMainWindow): # 如果日历组件可见,调整其大小和位置以适应窗口底部 if hasattr(self, 'calendar_widget') and self.calendar_widget.isVisible(): - calendar_height = 250 # 减小高度使比例更美观 + calendar_height = 350 # 增加高度以确保所有日期都能完整显示 self.calendar_widget.setGeometry(0, self.height() - calendar_height, self.width(), calendar_height) @@ -3524,7 +3524,7 @@ class WordStyleMainWindow(QMainWindow): self.calendar_widget.hide() else: # 设置日历组件位置在窗口底部 - calendar_height = 250 # 减小高度使比例更美观 + calendar_height = 350 # 增加高度以确保所有日期都能完整显示 # 将日历组件放置在窗口底部,占据整个宽度 self.calendar_widget.setGeometry(0, self.height() - calendar_height, -- 2.34.1 From df99246ae4c477402740251f828e47e8c9db28b0 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 16:19:17 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/learning_mode_window.py | 256 ++++++++++++++++++++++++++++-------- src/word_main_window.py | 129 ++++++++---------- 2 files changed, 256 insertions(+), 129 deletions(-) diff --git a/src/learning_mode_window.py b/src/learning_mode_window.py index 49305f4..18ee641 100644 --- a/src/learning_mode_window.py +++ b/src/learning_mode_window.py @@ -4,21 +4,24 @@ import os from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QFrame, QMenuBar, QAction, QFileDialog, QMessageBox, QApplication, - QSplitter, QScrollArea, QStatusBar, QProgressBar) + QSplitter, QScrollArea, QStatusBar, QProgressBar, QTextBrowser, QSizePolicy) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect -from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor +from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor, QTextImageFormat -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 +# 修复导入路径 +from ui.components import CustomTitleBar, TextDisplayWidget +from typing_logic import TypingLogic +from file_parser import FileParser +from ui.theme_manager import theme_manager +import tempfile +import hashlib class LearningModeWindow(QMainWindow): # 定义内容变化信号 content_changed = pyqtSignal(str, int) # 参数:内容,位置 # 定义关闭信号 closed = pyqtSignal() - def __init__(self, parent=None, imported_content="", current_position=0): + def __init__(self, parent=None, imported_content="", current_position=0, image_data=None, image_positions=None): """ 学习模式窗口 - 顶部显示UI.png图片 @@ -29,13 +32,18 @@ class LearningModeWindow(QMainWindow): parent: 父窗口 imported_content: 从主窗口传递的导入内容 current_position: 当前学习进度位置 + image_data: 图片数据字典 {文件名: 二进制数据} + image_positions: 图片位置信息列表 """ super().__init__(parent) self.parent_window = parent self.imported_content = imported_content self.current_position = current_position + self.image_data = image_data or {} + self.image_positions = image_positions or [] self.typing_logic = None self.is_loading_file = False + self.inserted_images = set() # 用于跟踪已插入的图片 # 初始化UI self.initUI() @@ -60,6 +68,12 @@ class LearningModeWindow(QMainWindow): # 重置打字逻辑 if self.typing_logic: self.typing_logic.reset(self.imported_content) + + # 设置图片数据到打字逻辑 + if self.image_data: + self.typing_logic.set_image_data(self.image_data) + if self.image_positions: + self.typing_logic.set_image_positions(self.image_positions) # 显示已学习的内容 display_text = self.imported_content[:self.current_position] @@ -144,38 +158,48 @@ class LearningModeWindow(QMainWindow): 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()) + if not pixmap.isNull(): + # 保存原始图片用于缩放计算 + self.original_pixmap = pixmap + + # 设置图片到标签 + self.image_label.setPixmap(pixmap) + self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签 + + # 设置大小策略 + self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # 计算合适的最小高度(保持图片比例) + window_width = 900 # 默认窗口宽度 + original_width = pixmap.width() + original_height = pixmap.height() + if original_width > 0: + min_height = int(window_width * original_height / original_width) + self.image_label.setMinimumHeight(min_height) + else: + self.image_label.setMinimumHeight(200) + else: + self.image_label.setText("UI图片加载失败") + self.image_label.setStyleSheet(""" + QLabel { + background-color: #f8f9fa; + color: #666666; + font-size: 14px; + qproperty-alignment: AlignCenter; + } + """) else: self.image_label.setText("UI图片未找到") self.image_label.setStyleSheet(""" QLabel { background-color: #f8f9fa; - border: none; color: #666666; font-size: 14px; - padding: 20px; + qproperty-alignment: AlignCenter; } """) - self.image_label.setMinimumHeight(200) - # 直接添加图片标签到主布局,不使用滚动区域 + # 添加到主布局 main_layout.addWidget(self.image_label) def resizeEvent(self, event): @@ -301,38 +325,67 @@ class LearningModeWindow(QMainWindow): ) if file_path: + self.is_loading_file = True try: - self.is_loading_file = True - - # 使用文件解析器 - parser = FileParser() - content = parser.parse_file(file_path) + # 获取文件扩展名 + _, ext = os.path.splitext(file_path) + ext = ext.lower() - 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开始打字以显示学习内容。") - + # 对于docx文件,直接解析而不转换为txt + if ext == '.docx': + # 直接解析docx文件内容 + content = FileParser.parse_docx(file_path) + # 提取图片数据 + images = FileParser.extract_images_from_docx(file_path) else: - QMessageBox.warning(self, "导入失败", "无法解析文件内容,请检查文件格式。") + # 其他文件类型使用原来的转换方法 + result = FileParser.parse_and_convert_to_txt(file_path) + content = result['content'] + images = result.get('images', []) + + if not content: + QMessageBox.warning(self, "导入失败", "文件内容为空或解析失败!") + return + + # 保存导入的内容 + self.imported_content = content + self.current_position = 0 + + # 重置已插入的图片集合 + self.inserted_images.clear() + + # 设置打字逻辑 + if self.typing_logic: + self.typing_logic.reset(content) + # 如果有图片,设置图片数据到打字逻辑 + if images: + image_data_dict = {} + image_positions = [] + + # 为每张图片生成位置信息 + for i, (filename, image_data) in enumerate(images): + image_data_dict[filename] = image_data + + # 计算图片插入位置(简单地每隔一定字符插入一张图片) + image_pos = len(content) // (len(images) + 1) * (i + 1) + image_positions.append({ + 'start_pos': image_pos, + 'end_pos': image_pos + 5, # 图片占位符长度 + 'filename': filename + }) + + # 设置图片数据到打字逻辑 + self.typing_logic.set_image_data(image_data_dict) + self.typing_logic.set_image_positions(image_positions) + + # 显示初始内容(空) + self.text_display_widget.text_display.clear() + self.status_label.setText("已导入文件,请开始打字学习...") + self.progress_label.setText("进度: 0%") + except Exception as e: - QMessageBox.critical(self, "导入错误", f"导入文件时出错:\n{str(e)}") + QMessageBox.critical(self, "导入错误", f"导入文件时发生错误:\n{str(e)}") finally: self.is_loading_file = False @@ -343,6 +396,7 @@ class LearningModeWindow(QMainWindow): - 根据导入的内容逐步显示 - 更新学习进度 - 同步内容到打字模式 + - 处理图片插入 """ # 如果正在加载文件,跳过处理 if self.is_loading_file: @@ -398,6 +452,9 @@ class LearningModeWindow(QMainWindow): f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)" ) + # 处理图片插入 + self.insert_images_if_needed() + # 只在用户新输入的字符上同步到打字模式 if self.parent_window and hasattr(self.parent_window, 'text_edit'): # 获取用户这一轮新输入的字符(与上一轮相比的新内容) @@ -414,6 +471,90 @@ class LearningModeWindow(QMainWindow): else: self.status_label.setText("继续输入以显示更多内容...") + def insert_images_if_needed(self): + """ + 根据当前进度插入图片 + """ + try: + # 检查是否有图片需要插入 + if not self.typing_logic or not hasattr(self.typing_logic, 'image_positions'): + return + + # 获取需要显示的图片列表 + images_to_display = self.typing_logic.get_images_to_display(self.current_position) + + # 检查当前显示位置是否有图片需要插入 + for image_info in images_to_display: + image_key = f"{image_info['start_pos']}_{image_info['filename']}" + + # 跳过已经插入过的图片 + if image_key in self.inserted_images: + continue + + # 当打字进度达到图片位置时插入图片 + if self.current_position >= image_info['start_pos']: + # 在图片位置插入图片 + cursor = self.text_display_widget.text_display.textCursor() + + # 计算图片应该插入的位置(基于原始内容位置) + insert_position = image_info['start_pos'] + + # 确保插入位置有效(不能超过当前显示内容长度) + if insert_position <= len(self.text_display_widget.text_display.toPlainText()): + # 移动光标到插入位置 + cursor.setPosition(insert_position) + self.text_display_widget.text_display.setTextCursor(cursor) + + # 创建图片格式 + image_format = QTextImageFormat() + + # 获取图片数据 + image_data = None + if hasattr(self.typing_logic, 'image_data') and image_info['filename'] in self.typing_logic.image_data: + image_data = self.typing_logic.image_data[image_info['filename']] + elif 'data' in image_info: + image_data = image_info.get('data') + + # 如果有图片数据,插入图片 + if image_data: + pixmap = QPixmap() + if pixmap.loadFromData(image_data): + # 生成临时文件名 + safe_filename = "".join(c for c in image_info['filename'] if c.isalnum() or c in ('.', '_', '-')) + temp_file = os.path.join(tempfile.gettempdir(), f"magicword_temp_{safe_filename}") + + # 保存图片到临时文件 + if pixmap.save(temp_file): + # 设置图片格式 + image_format.setName(temp_file) + image_format.setWidth(200) + image_format.setHeight(150) + + # 插入图片 + cursor.insertImage(image_format) + + # 标记图片已插入 + self.inserted_images.add(image_key) + + print(f"图片 {image_info['filename']} 已在位置 {insert_position} 插入") + else: + print(f"保存临时图片文件失败: {image_info['filename']}") + else: + print(f"加载图片数据失败: {image_info['filename']}") + else: + # 如果没有图片数据,插入占位符文本 + cursor.insertText(f"[图片: {image_info['filename']}]\n") + + # 恢复光标位置到文本末尾 + cursor = self.text_display_widget.text_display.textCursor() + cursor.movePosition(cursor.End) + self.text_display_widget.text_display.setTextCursor(cursor) + + except Exception as e: + print(f"插入图片失败: {str(e)}") + import traceback + traceback.print_exc() + def show_about(self): """ 显示关于对话框 @@ -424,7 +565,8 @@ class LearningModeWindow(QMainWindow): "• 顶部显示UI界面图片\n" "• 下方为打字输入区域\n" "• 导入文件后逐步显示内容\n" - "• 实时显示学习进度\n\n" + "• 实时显示学习进度\n" + "• 支持图片显示\n\n" "使用方法:\n" "1. 点击'文件'->'导入文件'选择学习材料\n" "2. 在下方文本区域开始打字\n" diff --git a/src/word_main_window.py b/src/word_main_window.py index 9bec582..caab2e5 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -6,7 +6,8 @@ from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QSplitter, QFrame, QMenuBar, QAction, QFileDialog, QMessageBox, QApplication, QDialog, QLineEdit, QCheckBox, QPushButton, QListWidget, - QListWidgetItem, QScrollArea) + QListWidgetItem, QScrollArea, QSizePolicy, + QGraphicsScene, QGraphicsView) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect, QByteArray, QBuffer, QIODevice from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor, QTextDocument, QImage, QTextImageFormat, QTextFormat, QTextBlockFormat @@ -1853,7 +1854,7 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage("新建文档 - 打字模式,可以自由开始打字", 3000) def import_file(self): - """导入文件 - 仅在导入时存储内容,不立即显示""" + """导入文件 - 根据模式决定是否立即显示""" file_path, _ = QFileDialog.getOpenFileName( self, "导入文件", "", "文档文件 (*.docx *.txt *.pdf *.html);;所有文件 (*.*)" @@ -1874,7 +1875,7 @@ class WordStyleMainWindow(QMainWindow): if result.get('is_temp_file', False): self.temp_files.append(txt_path) - # 存储完整内容但不立即显示 + # 存储完整内容 self.imported_content = content self.displayed_chars = 0 @@ -1916,8 +1917,9 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},开始打字逐步显示学习内容!", 5000) else: - # 打字模式:不显示导入内容,保持当前内容 - self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},切换到学习模式查看内容", 5000) + # 打字模式:直接显示完整内容 + self.text_edit.setPlainText(content) + self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)}", 5000) # 提取并显示图片(如果有) if images: @@ -1934,7 +1936,7 @@ class WordStyleMainWindow(QMainWindow): content = parser.parse_file(file_path) if content: - # 存储完整内容但不立即显示 + # 存储完整内容 self.imported_content = content self.displayed_chars = 0 @@ -1949,8 +1951,9 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)},开始打字逐步显示学习内容!", 5000) else: - # 打字模式:不显示导入内容,保持当前内容 - self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)},切换到学习模式查看内容", 5000) + # 打字模式:直接显示完整内容 + self.text_edit.setPlainText(content) + self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)}", 5000) except Exception as fallback_e: QMessageBox.critical(self, "错误", f"无法导入文件:\n{str(e)}\n\n回退方法也失败:\n{str(fallback_e)}") @@ -1975,7 +1978,7 @@ class WordStyleMainWindow(QMainWindow): # 设置文件加载标志 self.is_loading_file = True - # 存储完整内容但不立即显示 + # 存储完整内容 self.imported_content = content self.displayed_chars = 0 @@ -2149,8 +2152,17 @@ class WordStyleMainWindow(QMainWindow): current_position = 0 self.imported_content = current_text - # 创建学习模式窗口,直接传递导入内容 - self.learning_window = LearningModeWindow(self, imported_content, current_position) + # 准备图片数据 + image_data = None + image_positions = None + if hasattr(self, 'typing_logic') and self.typing_logic: + if hasattr(self.typing_logic, 'image_data'): + image_data = self.typing_logic.image_data + if hasattr(self.typing_logic, 'image_positions'): + image_positions = self.typing_logic.image_positions + + # 创建学习模式窗口,传递导入内容和图片数据 + self.learning_window = LearningModeWindow(self, imported_content, current_position, image_data, image_positions) # 连接学习模式窗口的内容变化信号 self.learning_window.content_changed.connect(self.on_learning_content_changed) @@ -2738,46 +2750,37 @@ class WordStyleMainWindow(QMainWindow): 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.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) + # 设置窗口标志,保留标题栏以便用户可以移动和调整大小 + viewer.setWindowFlags(Qt.Tool | Qt.WindowCloseButtonHint | Qt.WindowMinMaxButtonsHint) - # 设置窗口背景为黑色,完全无边距 + # 设置窗口背景为黑色 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) # 布局居中对齐 + # 创建场景和视图 + scene = QGraphicsScene(viewer) + view = QGraphicsView(scene) + view.setStyleSheet("border: none;") # 移除视图边框 - # 创建图片标签,设置为完全填充模式 - 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; - } - """) + # 设置视图为可交互的,并启用滚动条 + view.setDragMode(QGraphicsView.ScrollHandDrag) + view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) + + # 创建布局 + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(view) + viewer.setLayout(layout) # 加载图片 pixmap = QPixmap() @@ -2785,10 +2788,10 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage(f"加载图片失败: {filename}", 3000) return - layout.addWidget(image_label) - viewer.setLayout(layout) + # 将图片添加到场景 + scene.addPixmap(pixmap) - # 计算位置和大小 + # 设置视图大小和位置 if self: parent_geometry = self.geometry() screen_geometry = QApplication.primaryScreen().geometry() @@ -2813,42 +2816,24 @@ class WordStyleMainWindow(QMainWindow): 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}") + # 设置视图适应图片大小 + view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio) - # 使用多个定时器确保图片正确填充 - 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毫秒后再执行一次 + # 重写视图的滚轮事件以支持缩放 + def wheelEvent(event): + factor = 1.2 + if event.angleDelta().y() > 0: + view.scale(factor, factor) + else: + view.scale(1.0/factor, 1.0/factor) - # 连接窗口大小变化事件 - viewer.resizeEvent = lambda event: force_image_fill() + view.wheelEvent = wheelEvent - # 添加点击关闭功能 - def close_viewer(): - viewer.close() + # 添加双击重置视图功能 + def mouseDoubleClickEvent(event): + view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio) - image_label.mousePressEvent = lambda event: close_viewer() - viewer.mousePressEvent = lambda event: close_viewer() + view.mouseDoubleClickEvent = mouseDoubleClickEvent except Exception as e: self.status_bar.showMessage(f"创建图片查看器失败: {str(e)}", 3000) -- 2.34.1 From f89f4b9e3598059891696cbbb174ee2c4ec5b707 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 16:38:49 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E5=AD=A6=E4=B9=A0=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/learning_mode_window.py | 318 +++++++++++++++++++++++++----------- 1 file changed, 222 insertions(+), 96 deletions(-) diff --git a/src/learning_mode_window.py b/src/learning_mode_window.py index 18ee641..4634578 100644 --- a/src/learning_mode_window.py +++ b/src/learning_mode_window.py @@ -4,9 +4,10 @@ import os from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QFrame, QMenuBar, QAction, QFileDialog, QMessageBox, QApplication, - QSplitter, QScrollArea, QStatusBar, QProgressBar, QTextBrowser, QSizePolicy) + QSplitter, QScrollArea, QStatusBar, QProgressBar, QTextBrowser, QSizePolicy, + QListWidget, QListWidgetItem, QDialog, QGraphicsScene, QGraphicsView) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect -from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor, QTextImageFormat +from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor # 修复导入路径 from ui.components import CustomTitleBar, TextDisplayWidget @@ -43,7 +44,7 @@ class LearningModeWindow(QMainWindow): self.image_positions = image_positions or [] self.typing_logic = None self.is_loading_file = False - self.inserted_images = set() # 用于跟踪已插入的图片 + self.extracted_images = [] # 用于存储提取的图片数据 # 初始化UI self.initUI() @@ -234,6 +235,7 @@ class LearningModeWindow(QMainWindow): 创建输入区域 - 创建文本显示组件 - 设置与主系统相同的样式 + - 创建图片列表区域 """ # 创建文本显示组件(复用主系统的组件) self.text_display_widget = TextDisplayWidget(self) @@ -250,7 +252,40 @@ class LearningModeWindow(QMainWindow): # 连接文本变化信号 self.text_display_widget.text_display.textChanged.connect(self.on_text_changed) - main_layout.addWidget(self.text_display_widget, 1) # 占据剩余空间 + # 创建图片显示区域 + self.image_list_widget = QListWidget() + self.image_list_widget.setMaximumHeight(150) + self.image_list_widget.setStyleSheet(""" + QListWidget { + background-color: #f8f8f8; + border: 1px solid #d0d0d0; + border-radius: 4px; + font-size: 11px; + } + QListWidget::item { + padding: 5px; + border-bottom: 1px solid #e0e0e0; + } + QListWidget::item:selected { + background-color: #e3f2fd; + color: #1976d2; + } + """) + self.image_list_widget.setVisible(False) # 默认隐藏 + self.image_list_widget.itemDoubleClicked.connect(self.on_image_item_double_clicked) + + # 创建布局容器 + input_container = QWidget() + input_layout = QVBoxLayout() + input_layout.setContentsMargins(0, 0, 0, 0) + input_layout.setSpacing(5) + + input_layout.addWidget(self.text_display_widget, 1) # 文本显示区域占据剩余空间 + input_layout.addWidget(self.image_list_widget) # 图片列表区域 + + input_container.setLayout(input_layout) + + main_layout.addWidget(input_container, 1) # 占据剩余空间 def create_menu_bar(self): """ @@ -351,8 +386,8 @@ class LearningModeWindow(QMainWindow): self.imported_content = content self.current_position = 0 - # 重置已插入的图片集合 - self.inserted_images.clear() + # 保存提取的图片数据 + self.extracted_images = images # 设置打字逻辑 if self.typing_logic: @@ -363,21 +398,47 @@ class LearningModeWindow(QMainWindow): image_data_dict = {} image_positions = [] - # 为每张图片生成位置信息 + # 为每张图片生成位置信息 - 改进位置计算逻辑 for i, (filename, image_data) in enumerate(images): image_data_dict[filename] = image_data - # 计算图片插入位置(简单地每隔一定字符插入一张图片) - image_pos = len(content) // (len(images) + 1) * (i + 1) + # 改进图片位置计算,确保图片能在用户早期打字时显示 + content_length = len(content) + if content_length == 0: + content_length = 1000 # 备用长度 + + if len(images) == 1: + # 只有一张图片,放在文档开始位置附近(前10%),确保用户能快速看到 + image_pos = max(10, content_length // 10) + else: + # 多张图片:前几张放在较前位置,确保用户能看到 + if i < 3: + # 前3张图片放在文档前30% + segment = content_length // 3 + image_pos = max(10, segment * (i + 1) // 4) + else: + # 其余图片均匀分布 + remaining_start = content_length // 2 + remaining_index = i - 3 + remaining_count = len(images) - 3 + if remaining_count > 0: + segment = (content_length - remaining_start) // (remaining_count + 1) + image_pos = remaining_start + segment * (remaining_index + 1) + else: + image_pos = content_length // 2 + image_positions.append({ 'start_pos': image_pos, - 'end_pos': image_pos + 5, # 图片占位符长度 + 'end_pos': image_pos + 50, # 图片占位符长度 'filename': filename }) # 设置图片数据到打字逻辑 self.typing_logic.set_image_data(image_data_dict) self.typing_logic.set_image_positions(image_positions) + + # 显示图片列表 + self.display_image_list(images) # 显示初始内容(空) self.text_display_widget.text_display.clear() @@ -452,8 +513,7 @@ class LearningModeWindow(QMainWindow): f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)" ) - # 处理图片插入 - self.insert_images_if_needed() + # 只在用户新输入的字符上同步到打字模式 if self.parent_window and hasattr(self.parent_window, 'text_edit'): @@ -471,89 +531,7 @@ class LearningModeWindow(QMainWindow): else: self.status_label.setText("继续输入以显示更多内容...") - def insert_images_if_needed(self): - """ - 根据当前进度插入图片 - """ - try: - # 检查是否有图片需要插入 - if not self.typing_logic or not hasattr(self.typing_logic, 'image_positions'): - return - - # 获取需要显示的图片列表 - images_to_display = self.typing_logic.get_images_to_display(self.current_position) - - # 检查当前显示位置是否有图片需要插入 - for image_info in images_to_display: - image_key = f"{image_info['start_pos']}_{image_info['filename']}" - - # 跳过已经插入过的图片 - if image_key in self.inserted_images: - continue - - # 当打字进度达到图片位置时插入图片 - if self.current_position >= image_info['start_pos']: - # 在图片位置插入图片 - cursor = self.text_display_widget.text_display.textCursor() - - # 计算图片应该插入的位置(基于原始内容位置) - insert_position = image_info['start_pos'] - - # 确保插入位置有效(不能超过当前显示内容长度) - if insert_position <= len(self.text_display_widget.text_display.toPlainText()): - # 移动光标到插入位置 - cursor.setPosition(insert_position) - self.text_display_widget.text_display.setTextCursor(cursor) - - # 创建图片格式 - image_format = QTextImageFormat() - - # 获取图片数据 - image_data = None - if hasattr(self.typing_logic, 'image_data') and image_info['filename'] in self.typing_logic.image_data: - image_data = self.typing_logic.image_data[image_info['filename']] - elif 'data' in image_info: - image_data = image_info.get('data') - - # 如果有图片数据,插入图片 - if image_data: - pixmap = QPixmap() - if pixmap.loadFromData(image_data): - # 生成临时文件名 - safe_filename = "".join(c for c in image_info['filename'] if c.isalnum() or c in ('.', '_', '-')) - temp_file = os.path.join(tempfile.gettempdir(), f"magicword_temp_{safe_filename}") - - # 保存图片到临时文件 - if pixmap.save(temp_file): - # 设置图片格式 - image_format.setName(temp_file) - image_format.setWidth(200) - image_format.setHeight(150) - - # 插入图片 - cursor.insertImage(image_format) - - # 标记图片已插入 - self.inserted_images.add(image_key) - - print(f"图片 {image_info['filename']} 已在位置 {insert_position} 插入") - else: - print(f"保存临时图片文件失败: {image_info['filename']}") - else: - print(f"加载图片数据失败: {image_info['filename']}") - else: - # 如果没有图片数据,插入占位符文本 - cursor.insertText(f"[图片: {image_info['filename']}]\n") - - # 恢复光标位置到文本末尾 - cursor = self.text_display_widget.text_display.textCursor() - cursor.movePosition(cursor.End) - self.text_display_widget.text_display.setTextCursor(cursor) - - except Exception as e: - print(f"插入图片失败: {str(e)}") - import traceback - traceback.print_exc() + def show_about(self): """ @@ -594,4 +572,152 @@ class LearningModeWindow(QMainWindow): if event.key() == Qt.Key_Escape: self.close() else: - super().keyPressEvent(event) \ No newline at end of file + super().keyPressEvent(event) + + def display_image_list(self, images): + """ + 显示图片列表 + """ + try: + # 清空之前的图片列表 + self.image_list_widget.clear() + + # 如果没有图片,隐藏图片列表区域 + if not images: + self.image_list_widget.setVisible(False) + return + + # 显示图片列表区域 + self.image_list_widget.setVisible(True) + + # 添加图片项到列表 + for index, (filename, image_data) in enumerate(images): + # 创建缩略图 + pixmap = QPixmap() + if pixmap.loadFromData(image_data): + # 创建缩略图 + thumbnail = pixmap.scaled(60, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation) + + # 创建列表项 + item = QListWidgetItem() + item.setIcon(QIcon(thumbnail)) + item.setText(f"{filename} ({pixmap.width()}x{pixmap.height()})") + item.setData(Qt.UserRole, index) # 保存图片索引 + self.image_list_widget.addItem(item) + else: + # 如果无法加载图片,显示默认文本 + item = QListWidgetItem(f"{filename} (无法预览)") + item.setData(Qt.UserRole, index) + self.image_list_widget.addItem(item) + + # 更新状态栏 + self.status_label.setText(f"已提取 {len(images)} 张图片,双击查看大图") + + except Exception as e: + self.status_label.setText(f"显示图片列表失败: {str(e)}") + + def on_image_item_double_clicked(self, item): + """ + 双击图片项时显示大图 + """ + try: + # 获取图片索引 + index = item.data(Qt.UserRole) + if 0 <= index < len(self.extracted_images): + image_filename, image_data = self.extracted_images[index] + self.show_image_viewer(image_filename, image_data) + except Exception as e: + self.status_label.setText(f"显示图片失败: {str(e)}") + + def show_image_viewer(self, filename, image_data): + """ + 显示图片查看器 - 支持缩放功能 + """ + try: + # 创建自定义图片查看窗口 + viewer = QDialog(self) + viewer.setWindowTitle(f"图片查看 - {filename}") + viewer.setModal(False) + + # 设置窗口标志,保留标题栏以便用户可以移动和调整大小 + viewer.setWindowFlags(Qt.Tool | Qt.WindowCloseButtonHint | Qt.WindowMinMaxButtonsHint) + + # 设置窗口背景为黑色 + viewer.setStyleSheet(""" + QDialog { + background-color: #000000; + } + """) + + # 创建场景和视图 + scene = QGraphicsScene(viewer) + view = QGraphicsView(scene) + view.setStyleSheet("border: none;") # 移除视图边框 + + # 设置视图为可交互的,并启用滚动条 + view.setDragMode(QGraphicsView.ScrollHandDrag) + view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) + + # 创建布局 + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(view) + viewer.setLayout(layout) + + # 加载图片 + pixmap = QPixmap() + if not pixmap.loadFromData(image_data): + self.status_label.setText(f"加载图片失败: {filename}") + return + + # 将图片添加到场景 + scene.addPixmap(pixmap) + + # 设置视图大小和位置 + 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.show() + + # 设置视图适应图片大小 + view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio) + + # 重写视图的滚轮事件以支持缩放 + def wheelEvent(event): + factor = 1.2 + if event.angleDelta().y() > 0: + view.scale(factor, factor) + else: + view.scale(1.0/factor, 1.0/factor) + + view.wheelEvent = wheelEvent + + # 添加双击重置视图功能 + def mouseDoubleClickEvent(event): + view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio) + + view.mouseDoubleClickEvent = mouseDoubleClickEvent + + except Exception as e: + self.status_label.setText(f"创建图片查看器失败: {str(e)}") + import traceback + traceback.print_exc() \ No newline at end of file -- 2.34.1 From 3ef9200515fa5eaecb239e53a1f72cb91e9fb121 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 16:41:39 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E9=BB=84=E5=BD=AA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/word_main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/word_main_window.py b/src/word_main_window.py index caab2e5..9a76eb6 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -3406,8 +3406,8 @@ class WordStyleMainWindow(QMainWindow): # 为每张图片创建位置信息 - 修复位置计算,确保早期显示 content_length = len(self.imported_content) - if content_length == 0: - content_length = len(content) if 'content' in locals() else 1000 # 备用长度 + #if content_length == 0: + #content_length = len(content) if 'content' in locals() else 1000 # 备用长度 # 修复图片位置计算,确保图片能在用户早期打字时显示 if len(images) == 1: -- 2.34.1