diff --git a/src/ui/quote_floating_widget.py b/src/ui/quote_floating_widget.py new file mode 100644 index 0000000..ae740b7 --- /dev/null +++ b/src/ui/quote_floating_widget.py @@ -0,0 +1,415 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +每日谏言悬浮窗口 +""" + +import sys +from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QPushButton, QFrame, QGraphicsDropShadowEffect) +from PyQt5.QtCore import Qt, QPoint, pyqtSignal +from PyQt5.QtGui import QFont, QColor + + +class QuoteFloatingWidget(QWidget): + """每日谏言悬浮窗口""" + + # 定义信号 + closed = pyqtSignal() # 窗口关闭信号 + refresh_requested = pyqtSignal() # 刷新请求信号 + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool) + self.setAttribute(Qt.WA_TranslucentBackground) + + # 初始化变量 + self.is_dragging = False + self.drag_position = QPoint() + + # 设置默认谏言数据 + self.quote_data = { + "quote": "书山有路勤为径,学海无涯苦作舟。", + "author": "韩愈", + "source": "《古今贤文·劝学篇》" + } + + # 初始化UI + self.init_ui() + self.setup_styles() + self.apply_theme(is_dark=True) # 默认使用深色主题 + + def init_ui(self): + """初始化UI""" + # 主框架 + self.main_frame = QFrame(self) + self.main_frame.setObjectName("mainFrame") + self.main_frame.setFixedSize(360, 200) # 设置窗口大小 + + main_layout = QVBoxLayout(self.main_frame) + main_layout.setContentsMargins(12, 12, 12, 12) + main_layout.setSpacing(8) + + # 标题栏 + title_layout = QHBoxLayout() + title_layout.setContentsMargins(0, 0, 0, 0) + title_layout.setSpacing(0) + + self.title_label = QLabel("每日谏言") + self.title_label.setFont(QFont("Arial", 12, QFont.Bold)) + title_layout.addWidget(self.title_label) + title_layout.addStretch() + # 添加一个小的固定空间,使关闭按钮向左移动 + title_layout.addSpacing(25) # 向左移动25个单位 + + # 关闭按钮 + self.close_btn = QPushButton("×") + self.close_btn.setFixedSize(20, 20) + self.close_btn.setObjectName("closeButton") + self.close_btn.clicked.connect(self.close_window) + title_layout.addWidget(self.close_btn) + + main_layout.addLayout(title_layout) + + # 分隔线 + separator = QFrame() + separator.setObjectName("separator") + separator.setFixedHeight(1) + main_layout.addWidget(separator) + + # 谏言内容区域 + content_layout = QVBoxLayout() + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(6) + + # 谏言文本 + self.quote_label = QLabel() + self.quote_label.setObjectName("quoteLabel") + self.quote_label.setWordWrap(True) + self.quote_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + content_layout.addWidget(self.quote_label) + + # 作者信息 + self.author_label = QLabel() + self.author_label.setObjectName("authorLabel") + self.author_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + content_layout.addWidget(self.author_label) + + main_layout.addLayout(content_layout) + main_layout.addStretch() + + # 底部按钮区域 + bottom_layout = QHBoxLayout() + bottom_layout.setContentsMargins(0, 0, 0, 0) + bottom_layout.setSpacing(8) + + # 刷新按钮 + self.refresh_btn = QPushButton("换一句") + self.refresh_btn.setObjectName("refreshButton") + self.refresh_btn.clicked.connect(self.on_refresh_clicked) + bottom_layout.addWidget(self.refresh_btn) + + bottom_layout.addStretch() + main_layout.addLayout(bottom_layout) + + # 设置主布局 + outer_layout = QVBoxLayout(self) + outer_layout.setContentsMargins(0, 0, 0, 0) + outer_layout.addWidget(self.main_frame) + + # 更新显示 + self.update_quote() + + def setup_styles(self): + """设置样式""" + pass # 样式将在apply_theme中设置 + + def apply_theme(self, is_dark=True): + """应用主题""" + if is_dark: + # 深色主题配色 + colors = { + 'surface': '#2d2d2d', + 'border': '#444444', + 'text': '#ffffff', + 'text_secondary': '#cccccc', + 'accent': '#4CAF50', + 'accent_hover': '#45a049', + 'button_hover': '#555555', + 'error': '#f44336' + } + + self.main_frame.setStyleSheet(f""" + QFrame#mainFrame {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + }} + QLabel {{ + color: {colors['text']}; + background-color: transparent; + padding: 4px 6px; + margin: 2px; + }} + QLabel#quoteLabel {{ + color: {colors['text']}; + font-size: 14px; + font-weight: 500; + padding: 6px 8px; + margin: 3px; + }} + QLabel#authorLabel {{ + color: {colors['text_secondary']}; + font-size: 12px; + font-style: italic; + padding: 4px 6px; + margin: 2px; + }} + QFrame#separator {{ + background-color: {colors['border']}; + }} + QPushButton#closeButton {{ + background-color: rgba(255, 255, 255, 0.1); + border: none; + color: {colors['text']}; + font-size: 18px; + font-weight: bold; + border-radius: 6px; + padding: 2px 4px; + }} + QPushButton#closeButton:hover {{ + background-color: #e81123; + color: white; + border-radius: 6px; + }} + QPushButton#refreshButton {{ + background-color: {colors['accent']}; + color: white; + border: none; + border-radius: 6px; + padding: 6px 16px; + font-size: 11px; + font-weight: 500; + }} + QPushButton#refreshButton:hover {{ + background-color: {colors['accent_hover']}; + }} + """) + else: + # 浅色主题配色 + colors = { + 'surface': '#ffffff', + 'border': '#dddddd', + 'text': '#333333', + 'text_secondary': '#666666', + 'accent': '#4CAF50', + 'accent_hover': '#45a049', + 'button_hover': '#f0f0f0', + 'error': '#f44336' + } + + self.main_frame.setStyleSheet(f""" + QFrame#mainFrame {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + }} + QLabel {{ + color: {colors['text']}; + background-color: transparent; + padding: 4px 6px; + margin: 2px; + }} + QLabel#quoteLabel {{ + color: {colors['text']}; + font-size: 14px; + font-weight: 500; + padding: 6px 8px; + margin: 3px; + }} + QLabel#authorLabel {{ + color: {colors['text_secondary']}; + font-size: 12px; + font-style: italic; + padding: 4px 6px; + margin: 2px; + }} + QFrame#separator {{ + background-color: {colors['border']}; + }} + QPushButton#closeButton {{ + background-color: rgba(0, 0, 0, 0.05); + border: none; + color: {colors['text']}; + font-size: 18px; + font-weight: bold; + border-radius: 6px; + padding: 2px 4px; + }} + QPushButton#closeButton:hover {{ + background-color: #e81123; + color: white; + border-radius: 6px; + }} + QPushButton#refreshButton {{ + background-color: {colors['accent']}; + color: white; + border: none; + border-radius: 6px; + padding: 6px 16px; + font-size: 11px; + font-weight: 500; + }} + QPushButton#refreshButton:hover {{ + background-color: {colors['accent_hover']}; + }} + """) + + def update_quote(self, quote_data=None): + """更新谏言显示""" + if quote_data: + self.quote_data = quote_data + else: + # 如果没有提供数据,使用默认数据 + if not hasattr(self, 'quote_data'): + self.quote_data = { + "quote": "书山有路勤为径,学海无涯苦作舟。", + "author": "韩愈", + "source": "《古今贤文·劝学篇》" + } + + # 更新显示 + self.quote_label.setText(self.quote_data["quote"]) + author_info = f"— {self.quote_data['author']}" + if self.quote_data.get("source"): + author_info += f" 《{self.quote_data['source']}》" + self.author_label.setText(author_info) + + def on_refresh_clicked(self): + """刷新按钮点击事件""" + # 发送刷新请求信号 + self.refresh_requested.emit() + # 同时直接获取新的内容并更新显示 + self.fetch_and_update_quote() + + def fetch_and_update_quote(self): + """获取新的谏言内容并更新显示""" + try: + # 尝试获取古诗词 + import requests + import random + + try: + # 使用古诗词·一言API + response = requests.get("https://v1.jinrishici.com/all.json", timeout=5, verify=False) + if response.status_code == 200: + data = response.json() + content = data.get('content', '') + author = data.get('author', '佚名') + origin = data.get('origin', '') + + if content: + quote_data = { + "quote": content, + "author": author, + "source": origin + } + self.update_quote(quote_data) + return + except Exception as e: + print(f"获取古诗词失败: {e}") + + # 如果古诗词获取失败,使用备用API + try: + # 使用每日一言API + response = requests.get("https://api.nxvav.cn/api/yiyan?json=true", timeout=5, verify=False) + if response.status_code == 200: + data = response.json() + yiyan = data.get('yiyan', '') + nick = data.get('nick', '佚名') + + if yiyan: + quote_data = { + "quote": yiyan, + "author": nick, + "source": "" + } + self.update_quote(quote_data) + return + except Exception as e: + print(f"获取每日一言失败: {e}") + + # 如果API都失败,使用预设内容 + quotes = [ + {"quote": "学而时习之,不亦说乎?", "author": "孔子", "source": "《论语》"}, + {"quote": "千里之行,始于足下。", "author": "老子", "source": "《道德经》"}, + {"quote": "天行健,君子以自强不息。", "author": "佚名", "source": "《周易》"}, + {"quote": "书山有路勤为径,学海无涯苦作舟。", "author": "韩愈", "source": "《古今贤文·劝学篇》"}, + {"quote": "山重水复疑无路,柳暗花明又一村。", "author": "陆游", "source": "《游山西村》"} + ] + + # 随机选择一个名言 + new_quote = random.choice(quotes) + self.update_quote(new_quote) + + except Exception as e: + print(f"获取新谏言失败: {e}") + # 出错时显示默认内容 + default_quote = { + "quote": "书山有路勤为径,学海无涯苦作舟。", + "author": "韩愈", + "source": "《古今贤文·劝学篇》" + } + self.update_quote(default_quote) + + def close_window(self): + """关闭窗口 - 只是隐藏而不是销毁""" + try: + self.closed.emit() + self.hide() # 隐藏窗口而不是销毁 + except Exception as e: + print(f"Error in close_window: {e}") + + def mousePressEvent(self, event): + """鼠标按下事件,用于拖拽""" + if event.button() == Qt.LeftButton: + # 检查是否点击在标题栏区域 + if event.pos().y() <= 40: # 假设标题栏高度为40像素 + self.is_dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + event.accept() + + def mouseMoveEvent(self, event): + """鼠标移动事件,用于拖拽""" + if self.is_dragging and event.buttons() == Qt.LeftButton: + self.move(event.globalPos() - self.drag_position) + event.accept() + + def mouseReleaseEvent(self, event): + """鼠标释放事件""" + self.is_dragging = False + + +def main(): + """测试函数""" + app = QApplication(sys.argv) + + # 创建并显示窗口 + widget = QuoteFloatingWidget() + widget.show() + + # 移动到屏幕中心 + screen_geometry = app.desktop().screenGeometry() + widget.move( + (screen_geometry.width() - widget.width()) // 2, + (screen_geometry.height() - widget.height()) // 2 + ) + + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 102b28c..07ad4ff 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -34,6 +34,7 @@ class WordRibbon(QFrame): self.weather_group = None # 天气组件组 self.quote_visible = False # 每日一言组件显示状态 self.quote_group = None # 每日一言组件组 + self.current_quote_type = "普通箴言" # 每日一言类型 self.ribbon_layout = None # 功能区布局 self.setup_ui() @@ -704,9 +705,16 @@ class WordRibbon(QFrame): self.floating_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }") self.floating_weather_btn.setToolTip("切换天气悬浮窗口") + # 每日谏言悬浮窗口按钮 + self.floating_quote_btn = QPushButton("📜 悬浮") + self.floating_quote_btn.setFixedSize(60, 30) + self.floating_quote_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }") + self.floating_quote_btn.setToolTip("切换每日谏言悬浮窗口") + control_layout.addWidget(self.city_combo) control_layout.addWidget(self.refresh_weather_btn) control_layout.addWidget(self.floating_weather_btn) + control_layout.addWidget(self.floating_quote_btn) # 添加右侧弹性空间,确保内容居中 control_layout.addStretch() diff --git a/src/word_main_window.py b/src/word_main_window.py index eb7afbb..8d8929a 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -20,6 +20,7 @@ from file_parser import FileParser from input_handler.input_processor import InputProcessor from ui.calendar_widget import CalendarWidget from ui.weather_floating_widget import WeatherFloatingWidget +from ui.quote_floating_widget import QuoteFloatingWidget # 导入主题管理器 from ui.theme_manager import theme_manager @@ -115,6 +116,12 @@ class WordStyleMainWindow(QMainWindow): self.weather_floating_widget.closed.connect(self.on_weather_floating_closed) self.weather_floating_widget.refresh_requested.connect(self.refresh_weather) + # 初始化每日谏言悬浮窗口 + self.quote_floating_widget = QuoteFloatingWidget(self) + self.quote_floating_widget.hide() # 默认隐藏 + self.quote_floating_widget.closed.connect(self.on_quote_floating_closed) + self.quote_floating_widget.refresh_requested.connect(self.refresh_daily_quote) + # 设置窗口属性 self.setWindowTitle("文档1 - MagicWord") self.setGeometry(100, 100, 1200, 800) @@ -710,6 +717,11 @@ class WordStyleMainWindow(QMainWindow): toggle_floating_weather_action = QAction('天气悬浮窗口', self) toggle_floating_weather_action.triggered.connect(self.toggle_floating_weather) weather_menu.addAction(toggle_floating_weather_action) + + # 每日谏言悬浮窗口切换动作 + toggle_floating_quote_action = QAction('每日谏言悬浮窗口', self) + toggle_floating_quote_action.triggered.connect(self.toggle_floating_quote) + weather_menu.addAction(toggle_floating_quote_action) # 插入菜单 insert_menu = menubar.addMenu('插入(I)') @@ -946,9 +958,10 @@ class WordStyleMainWindow(QMainWindow): self.ribbon.city_combo.currentTextChanged.connect(self.on_city_changed) if hasattr(self.ribbon, 'refresh_weather_btn'): self.ribbon.refresh_weather_btn.clicked.connect(self.refresh_weather) - if hasattr(self.ribbon, 'floating_weather_btn'): self.ribbon.floating_weather_btn.clicked.connect(self.toggle_floating_weather) + if hasattr(self.ribbon, 'floating_quote_btn'): + self.ribbon.floating_quote_btn.clicked.connect(self.toggle_floating_quote) # 日历组件信号 if hasattr(self, 'calendar_widget'): @@ -1771,6 +1784,22 @@ class WordStyleMainWindow(QMainWindow): """天气悬浮窗口关闭时的处理""" self.status_bar.showMessage("天气悬浮窗口已关闭", 2000) + def toggle_floating_quote(self): + """切换每日谏言悬浮窗口显示/隐藏""" + if hasattr(self, 'quote_floating_widget'): + if self.quote_floating_widget.isVisible(): + self.quote_floating_widget.hide() + self.status_bar.showMessage("每日谏言悬浮窗口已隐藏", 2000) + else: + self.quote_floating_widget.show() + # 确保窗口在屏幕内 + self.quote_floating_widget.move(100, 100) + self.status_bar.showMessage("每日谏言悬浮窗口已显示", 2000) + + def on_quote_floating_closed(self): + """每日谏言悬浮窗口关闭时的处理""" + self.status_bar.showMessage("每日谏言悬浮窗口已关闭", 2000) + def toggle_weather_tools(self, checked): """切换天气工具组显示""" if checked: @@ -1805,6 +1834,12 @@ class WordStyleMainWindow(QMainWindow): if hasattr(self, 'ribbon'): # 直接调用WordRibbon中的刷新方法 self.ribbon.on_refresh_quote() + + # 同时更新浮动窗口中的内容(如果浮动窗口存在且可见) + if hasattr(self, 'quote_floating_widget') and self.quote_floating_widget.isVisible(): + # 调用浮动窗口的获取新内容方法 + if hasattr(self.quote_floating_widget, 'fetch_and_update_quote'): + self.quote_floating_widget.fetch_and_update_quote() def on_quote_fetched(self, quote_data): """处理名言获取成功"""