【开源工具】Python+PyQt5打造智能桌面单词记忆工具:悬浮窗+热键切换+自定义词库

📚【深度解析】Python+PyQt5打造智能桌面单词记忆工具:悬浮窗+热键切换+自定义词库


🌈 个人主页:创客白泽 - CSDN博客

🔥 系列专栏:🐍《Python开源项目实战》

💡 热爱不止于代码,热情源自每一个灵感闪现的夜晚。愿以开源之火,点亮前行之路。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦

一、概述:当单词记忆遇上Python GUI

在英语学习过程中,高频重复 是记忆单词的关键。传统背单词软件往往需要用户主动打开应用,而本项目的创新之处在于开发了一个桌面悬浮窗单词记忆工具,具有以下核心特点:

  • 无干扰学习:半透明悬浮窗始终置顶显示
  • 智能记忆算法:支持顺序/逆序/随机三种循环模式
  • 快捷交互:F8热键一键切换中英释义
  • 高度可定制:字体/颜色/间隔时间全面可配置
  • 轻量化设计:系统托盘运行,内存占用<50MB

本文将深入解析200+行代码的实现原理,并提供完整的可执行方案。

二、功能架构设计

2.1 核心功能模块

  1. 单词显示模块

    • 定时切换显示
    • 中英双语切换
    • 视觉样式定制
  2. 交互控制模块

    • 全局热键监听
    • 拖拽移动窗口
    • 系统托盘菜单
  3. 配置管理模块

    • QSettings持久化
    • 词库动态加载
    • 设置实时生效

2.2 技术选型对比

技术方案 优势 本项目选择原因
PyQt5 成熟GUI框架 跨平台支持好
pynput 全局热键监听 比win32api更简洁
QSettings 配置存储 无需额外数据库

三、效果展示

3.1 主界面效果

3.2 设置面板

3.3 系统托盘菜单

四、实现步骤详解

4.1 环境准备

bash 复制代码
pip install PyQt5 pynput

4.2 词库文件格式

创建words.txt(每行一个单词+释义):

复制代码
apple 苹果
banana 香蕉

4.3 核心类解析

4.3.1 热键监听线程
python 复制代码
class HotkeyWorker(QObject):
    toggle_signal = pyqtSignal()
    
    def run(self):
        from pynput import keyboard
        
        def on_activate_f8():
            self.toggle_signal.emit()
            
        with keyboard.GlobalHotKeys({'<F8>': on_activate_f8}) as listener:
            listener.join()

关键点

  • 使用QObject实现跨线程信号通信
  • pynput库实现全局热键监听
  • 异常处理保障稳定性
4.3.2 主窗口类
python 复制代码
class WordDisplayApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
        self.setAttribute(Qt.WA_TranslucentBackground)

窗口特性

  • FramelessWindowHint:无边框
  • WindowStaysOnTopHint:始终置顶
  • WA_TranslucentBackground:半透明效果

4.4 配置持久化实现

python 复制代码
self.settings = QSettings("WordDisplay", "WordDisplayApp")

# 保存配置
self.settings.setValue("interval", self.interval)

# 读取配置
self.interval = self.settings.value("interval", 5, type=int)

存储位置

  • Windows:注册表HKEY_CURRENT_USER\Software\WordDisplay\WordDisplayApp
  • Mac:~/Library/Preferences/com.WordDisplay.WordDisplayApp.plist

五、代码深度解析

5.1 单词加载算法

python 复制代码
def load_words(self):
    if self.order == "reverse":
        self.word_list.reverse()
    elif self.order == "random":
        random.shuffle(self.word_list)

记忆算法优化

  • 随机模式:避免固定顺序记忆
  • 逆序模式:强化尾部单词记忆
  • 间隔重复:通过定时器控制显示节奏

5.2 拖拽移动实现

python 复制代码
def mousePressEvent(self, event):
    self.dragging = True
    self.offset = event.globalPos() - self.pos()

def mouseMoveEvent(self, event):
    if self.dragging:
        self.move(event.globalPos() - self.offset)

交互细节

  • 记录鼠标点击位置与窗口位置的偏移量
  • 实时计算新窗口位置
  • 鼠标释放时重置状态

5.3 系统托盘集成

python 复制代码
self.tray_icon = QSystemTrayIcon(self)
tray_menu = QMenu()
exit_action = QAction("退出", self)
exit_action.triggered.connect(self.quit_app)
tray_menu.addAction(exit_action)

多平台适配

  • Windows/Mac/Linux通用实现
  • 双击图标显示/隐藏窗口
  • 右键弹出功能菜单

六、完整源码下载

python 复制代码
import sys
import random
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QSystemTrayIcon,
                            QMenu, QAction, QWidget, QVBoxLayout, QPushButton,
                            QSpinBox, QComboBox, QColorDialog, QMessageBox, 
                            QDesktopWidget, QFileDialog)
from PyQt5.QtGui import QColor, QFont, QIcon, QPixmap, QCursor
from PyQt5.QtCore import Qt, QTimer, QSettings, QSize, QThread, pyqtSignal, QObject, QPoint

class HotkeyWorker(QObject):
    """处理全局快捷键的工作线程"""
    toggle_signal = pyqtSignal()
    error_signal = pyqtSignal(str)

    def __init__(self):
        super().__init__()
        self.listener = None

    def run(self):
        """启动热键监听"""
        try:
            from pynput import keyboard

            def on_activate_f8():
                self.toggle_signal.emit()

            with keyboard.GlobalHotKeys({'<F8>': on_activate_f8}) as self.listener:
                self.listener.join()
        except ImportError as e:
            self.error_signal.emit("未安装pynput库,无法使用F8快捷键功能")
        except Exception as e:
            self.error_signal.emit(f"快捷键初始化失败: {str(e)}")

    def stop(self):
        """停止热键监听"""
        if self.listener:
            self.listener.stop()

class WordDisplayApp(QMainWindow):
    def __init__(self):
        super().__init__()
        
        # 初始化设置
        self.settings = QSettings("WordDisplay", "WordDisplayApp")
        self.load_settings()
        
        # 初始化UI
        self.init_ui()
        
        # 加载单词数据
        self.word_list = []
        self.current_word_file = "words.txt"  # 默认词库文件
        self.load_words()
        
        # 设置当前单词索引
        self.current_index = 0
        
        # 显示第一个单词
        self.show_word()
        
        # 设置定时器
        self.timer = QTimer()
        self.timer.timeout.connect(self.show_next_word)
        self.timer.start(self.interval * 1000)
        
        # 中文释义是否显示
        self.show_translation = False
        
        # 初始化热键线程
        self.init_hotkey_thread()
        
        # 拖动相关变量
        self.dragging = False
        self.offset = QPoint()

    def init_hotkey_thread(self):
        """初始化热键监听线程"""
        self.hotkey_thread = QThread()
        self.hotkey_worker = HotkeyWorker()
        self.hotkey_worker.moveToThread(self.hotkey_thread)
        
        # 连接信号
        self.hotkey_worker.toggle_signal.connect(self.toggle_translation)
        self.hotkey_worker.error_signal.connect(self.show_error_message)
        
        # 启动线程
        self.hotkey_thread.started.connect(self.hotkey_worker.run)
        self.hotkey_thread.start()

    def show_error_message(self, message):
        """显示错误消息"""
        QMessageBox.warning(self, "警告", message)

    def init_ui(self):
        """初始化用户界面"""
        self.setWindowTitle("单词显示")
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
        self.setAttribute(Qt.WA_TranslucentBackground)
        
        # 创建主部件
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        
        # 布局
        layout = QVBoxLayout()
        layout.setContentsMargins(10, 10, 10, 10)
        main_widget.setLayout(layout)
        
        # 英文单词标签
        self.word_label = QLabel("单词加载中...")
        self.word_label.setAlignment(Qt.AlignCenter)
        self.word_label.setFont(QFont("Arial", self.english_font_size))
        self.word_label.setStyleSheet(f"color: {self.word_color.name()};")
        
        # 中文翻译标签
        self.translation_label = QLabel()
        self.translation_label.setAlignment(Qt.AlignCenter)
        self.translation_label.setFont(QFont("微软雅黑", self.chinese_font_size))
        self.translation_label.setStyleSheet(f"color: {self.translation_color.name()};")
        self.translation_label.hide()
        
        # 添加到布局
        layout.addWidget(self.word_label)
        layout.addWidget(self.translation_label)
        
        # 系统托盘图标
        self.init_system_tray()
        
        # 移动到右上角
        self.move_to_top_right()

    def init_system_tray(self):
        """初始化系统托盘图标"""
        self.tray_icon = QSystemTrayIcon(self)
        
        # 设置图标
        icon_path = os.path.join(os.path.dirname(__file__), "icon.ico")
        if os.path.exists(icon_path):
            self.tray_icon.setIcon(QIcon(icon_path))
        else:
            # 创建默认图标
            pixmap = QPixmap(QSize(64, 64))
            pixmap.fill(Qt.transparent)
            self.tray_icon.setIcon(QIcon(pixmap))
            QMessageBox.warning(self, "图标未找到", f"未找到图标文件: {icon_path}")
        
        # 创建托盘菜单
        tray_menu = QMenu()
        
        # 显示/隐藏主窗口
        toggle_action = QAction("显示/隐藏", self)
        toggle_action.triggered.connect(self.toggle_visibility)
        tray_menu.addAction(toggle_action)
        
        # 重置位置
        reset_pos_action = QAction("重置位置", self)
        reset_pos_action.triggered.connect(self.move_to_top_right)
        tray_menu.addAction(reset_pos_action)
        
        # 设置
        settings_action = QAction("设置", self)
        settings_action.triggered.connect(self.show_settings_dialog)
        tray_menu.addAction(settings_action)
        
        # 加载词库
        load_dict_action = QAction("加载词库", self)
        load_dict_action.triggered.connect(self.load_new_dictionary)
        tray_menu.addAction(load_dict_action)
        
        # 退出
        exit_action = QAction("退出", self)
        exit_action.triggered.connect(self.quit_app)
        tray_menu.addAction(exit_action)
        
        self.tray_icon.setContextMenu(tray_menu)
        self.tray_icon.show()
        
        # 托盘图标点击事件
        self.tray_icon.activated.connect(self.on_tray_icon_activated)

    def on_tray_icon_activated(self, reason):
        """托盘图标点击事件处理"""
        if reason == QSystemTrayIcon.DoubleClick:
            self.toggle_visibility()

    def move_to_top_right(self):
        """将窗口移动到屏幕右上角"""
        screen = QDesktopWidget().screenGeometry()
        self.move(screen.width() - self.width() - 20, 20)

    def mousePressEvent(self, event):
        """鼠标按下事件"""
        if event.button() == Qt.LeftButton:
            self.dragging = True
            self.offset = event.globalPos() - self.pos()
            event.accept()
            self.setCursor(QCursor(Qt.SizeAllCursor))

    def mouseMoveEvent(self, event):
        """鼠标移动事件"""
        if self.dragging and event.buttons() & Qt.LeftButton:
            self.move(event.globalPos() - self.offset)
            event.accept()

    def mouseReleaseEvent(self, event):
        """鼠标释放事件"""
        if event.button() == Qt.LeftButton:
            self.dragging = False
            self.setCursor(QCursor(Qt.ArrowCursor))
            event.accept()

    def load_words(self, file_path=None):
        """从文件加载单词数据"""
        try:
            if file_path is None:
                file_path = self.current_word_file
            
            with open(file_path, "r", encoding="utf-8") as f:
                self.word_list = []
                for line in f:
                    line = line.strip()
                    if line:
                        parts = line.split(maxsplit=1)
                        if len(parts) == 2:
                            self.word_list.append({
                                "word": parts[0],
                                "translation": parts[1]
                            })
            
            # 根据顺序设置处理单词列表
            if self.order == "reverse":
                self.word_list.reverse()
            elif self.order == "random":
                random.shuffle(self.word_list)
                
            if not self.word_list:
                self.word_list.append({
                    "word": "示例单词",
                    "translation": "example translation"
                })
                
        except FileNotFoundError:
            QMessageBox.critical(self, "错误", f"未找到词库文件: {file_path}")
            self.word_list = [{
                "word": "示例单词",
                "translation": "example translation"
            }]
        except Exception as e:
            QMessageBox.critical(self, "错误", f"加载词库失败: {str(e)}")
            self.word_list = [{
                "word": "加载失败",
                "translation": "load failed"
            }]

    def load_new_dictionary(self):
        """加载新的词库文件"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择词库文件", "", "文本文件 (*.txt);;所有文件 (*)"
        )
        
        if file_path:
            self.current_word_file = file_path
            self.load_words(file_path)
            self.current_index = 0
            self.show_word()
            
            # 保存当前词库路径
            self.settings.setValue("current_word_file", file_path)

    def show_word(self):
        """显示当前单词"""
        if not self.word_list:
            return
            
        word_data = self.word_list[self.current_index]
        self.word_label.setText(word_data["word"])
        self.translation_label.setText(word_data["translation"])

    def show_next_word(self):
        """显示下一个单词"""
        if not self.word_list:
            return
            
        if self.order == "random":
            self.current_index = random.randint(0, len(self.word_list) - 1)
        else:
            self.current_index += 1
            if self.current_index >= len(self.word_list):
                self.current_index = 0
        
        self.show_word()

    def toggle_translation(self):
        """切换翻译显示状态"""
        self.show_translation = not self.show_translation
        self.translation_label.setVisible(self.show_translation)
        
        # 根据翻译显示状态控制定时器
        if self.show_translation:
            self.timer.stop()  # 显示翻译时暂停计时器
        else:
            self.timer.start(self.interval * 1000)  # 隐藏翻译时恢复计时器

    def show_settings_dialog(self):
        """显示设置对话框"""
        self.settings_dialog = QWidget()
        self.settings_dialog.setWindowTitle("设置")
        self.settings_dialog.setWindowModality(Qt.ApplicationModal)
        self.settings_dialog.setFixedSize(350, 450)
        
        layout = QVBoxLayout()
        layout.setContentsMargins(10, 10, 10, 10)
        
        # 间隔时间设置
        interval_label = QLabel("显示间隔(秒):")
        self.interval_spinbox = QSpinBox()
        self.interval_spinbox.setRange(1, 3600)
        self.interval_spinbox.setValue(self.interval)
        
        # 单词顺序设置
        order_label = QLabel("单词顺序:")
        self.order_combobox = QComboBox()
        self.order_combobox.addItems(["顺序", "逆序", "随机"])
        self.order_combobox.setCurrentIndex(["normal", "reverse", "random"].index(self.order))
        
        # 英文字体大小设置
        english_fontsize_label = QLabel("英文单词字体大小:")
        self.english_fontsize_spinbox = QSpinBox()
        self.english_fontsize_spinbox.setRange(8, 72)
        self.english_fontsize_spinbox.setValue(self.english_font_size)
        
        # 中文字体大小设置
        chinese_fontsize_label = QLabel("中文翻译字体大小:")
        self.chinese_fontsize_spinbox = QSpinBox()
        self.chinese_fontsize_spinbox.setRange(8, 72)
        self.chinese_fontsize_spinbox.setValue(self.chinese_font_size)
        
        # 单词颜色设置
        word_color_label = QLabel("单词颜色:")
        self.word_color_button = QPushButton()
        self.word_color_button.setStyleSheet(f"background-color: {self.word_color.name()};")
        self.word_color_button.clicked.connect(lambda: self.choose_color("word"))
        
        # 翻译颜色设置
        trans_color_label = QLabel("翻译颜色:")
        self.trans_color_button = QPushButton()
        self.trans_color_button.setStyleSheet(f"background-color: {self.translation_color.name()};")
        self.trans_color_button.clicked.connect(lambda: self.choose_color("translation"))
        
        # 保存按钮
        save_button = QPushButton("保存设置")
        save_button.clicked.connect(self.save_settings)
        
        # 重新加载当前词库按钮
        reload_button = QPushButton("重新加载当前词库")
        reload_button.clicked.connect(lambda: self.load_words(self.current_word_file))
        
        # 添加到布局
        layout.addWidget(interval_label)
        layout.addWidget(self.interval_spinbox)
        layout.addWidget(order_label)
        layout.addWidget(self.order_combobox)
        layout.addWidget(english_fontsize_label)
        layout.addWidget(self.english_fontsize_spinbox)
        layout.addWidget(chinese_fontsize_label)
        layout.addWidget(self.chinese_fontsize_spinbox)
        layout.addWidget(word_color_label)
        layout.addWidget(self.word_color_button)
        layout.addWidget(trans_color_label)
        layout.addWidget(self.trans_color_button)
        layout.addWidget(save_button)
        layout.addWidget(reload_button)
        layout.addStretch()
        
        self.settings_dialog.setLayout(layout)
        self.settings_dialog.show()

    def choose_color(self, color_type):
        """选择颜色"""
        color = QColorDialog.getColor()
        if color.isValid():
            if color_type == "word":
                self.word_color = color
                self.word_color_button.setStyleSheet(f"background-color: {color.name()};")
            else:
                self.translation_color = color
                self.trans_color_button.setStyleSheet(f"background-color: {color.name()};")

    def save_settings(self):
        """保存设置"""
        self.interval = self.interval_spinbox.value()
        
        order_index = self.order_combobox.currentIndex()
        if order_index == 0:
            self.order = "normal"
        elif order_index == 1:
            self.order = "reverse"
        else:
            self.order = "random"
        
        self.english_font_size = self.english_fontsize_spinbox.value()
        self.chinese_font_size = self.chinese_fontsize_spinbox.value()
        
        # 保存到QSettings
        self.settings.setValue("interval", self.interval)
        self.settings.setValue("order", self.order)
        self.settings.setValue("english_font_size", self.english_font_size)
        self.settings.setValue("chinese_font_size", self.chinese_font_size)
        self.settings.setValue("word_color", self.word_color.name())
        self.settings.setValue("translation_color", self.translation_color.name())
        
        # 应用设置
        self.apply_settings()
        self.settings_dialog.close()

    def apply_settings(self):
        """应用当前设置"""
        # 更新定时器间隔
        self.timer.stop()
        if not self.show_translation:  # 只有隐藏翻译时才启动计时器
            self.timer.start(self.interval * 1000)
        
        # 更新字体大小
        self.word_label.setFont(QFont("Arial", self.english_font_size))
        self.translation_label.setFont(QFont("微软雅黑", self.chinese_font_size))
        
        # 更新颜色
        self.word_label.setStyleSheet(f"color: {self.word_color.name()};")
        self.translation_label.setStyleSheet(f"color: {self.translation_color.name()};")
        
        # 如果需要,重新排序单词
        if self.order == "reverse":
            self.word_list.reverse()
        elif self.order == "random":
            random.shuffle(self.word_list)
        
        # 重置索引
        self.current_index = 0
        self.show_word()

    def toggle_visibility(self):
        """切换窗口可见性"""
        if self.isVisible():
            self.hide()
        else:
            self.show()
            self.move_to_top_right()

    def quit_app(self):
        """退出应用程序"""
        # 停止热键线程
        if hasattr(self, 'hotkey_thread'):
            self.hotkey_worker.stop()
            self.hotkey_thread.quit()
            self.hotkey_thread.wait()
        
        # 保存设置
        self.settings.sync()
        QApplication.quit()

    def closeEvent(self, event):
        """重写关闭事件"""
        event.ignore()
        self.hide()

    def load_settings(self):
        """加载设置"""
        # 默认设置
        self.interval = self.settings.value("interval", 5, type=int)
        self.order = self.settings.value("order", "normal")
        self.english_font_size = self.settings.value("english_font_size", 24, type=int)
        self.chinese_font_size = self.settings.value("chinese_font_size", 20, type=int)
        self.current_word_file = self.settings.value("current_word_file", "words.txt")
        
        # 颜色设置
        self.word_color = QColor(self.settings.value("word_color", "#000000"))
        self.translation_color = QColor(self.settings.value("translation_color", "#888888"))

if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setQuitOnLastWindowClosed(False)
    
    # 设置应用程序图标
    icon_path = os.path.join(os.path.dirname(__file__), "icon.ico")
    if os.path.exists(icon_path):
        app.setWindowIcon(QIcon(icon_path))
    
    word_app = WordDisplayApp()
    word_app.hide()
    
    sys.exit(app.exec_())

项目结构:

复制代码
word-display/
├── main.py            # 主程序
├── words.txt          # 示例词库
├── icon.ico           # 程序图标
└── requirements.txt   # 依赖库

七、扩展开发建议

7.1 功能增强方向

  1. 记忆曲线算法:根据艾宾浩斯曲线调整单词出现频率
  2. 云端同步:增加词库网络同步功能
  3. 发音功能:集成TTS引擎朗读单词

7.2 性能优化建议

python 复制代码
# 使用LRU缓存最近显示的单词
from functools import lru_cache

@lru_cache(maxsize=50)
def get_word_style(word):
    return generate_style(word)

八、总结

本文详细解析了基于PyQt5的单词记忆工具开发全过程,关键技术点包括:

  1. 多线程热键监听的实现方案
  2. QSettings配置持久化的最佳实践
  3. 无边框窗口的交互细节处理
  4. 系统托盘应用的开发模式

该项目的创新价值在于:

  • 被动记忆 转化为主动感知
  • 极简主义设计哲学的应用
  • 可扩展的架构设计

"The limits of my language mean the limits of my world." - Ludwig Wittgenstein

通过这个项目,我们不仅学习了PyQt5开发技巧,更创造了一个真正能提升学习效率的工具。期待读者基于此项目开发出更多有趣的应用!


相关资源推荐

问题交流:欢迎在评论区留言讨论!

相关推荐
Amo Xiang7 小时前
Python 解释器安装全攻略(适用于 Linux / Windows / macOS)
linux·windows·python·环境安装
程序员杰哥7 小时前
接口自动化测试之pytest 运行方式及前置后置封装
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·pytest
浩皓素7 小时前
用Python开启游戏开发之旅
python
hello kitty w7 小时前
Python学习(6) ----- Python2和Python3的区别
开发语言·python·学习
互联网杂货铺9 小时前
功能测试、性能测试、安全测试详解
自动化测试·软件测试·python·功能测试·测试工具·性能测试·安全性测试
土豆杨6269 小时前
隐藏层-机器学习
python·机器学习
Dxy123931021610 小时前
DrissionPage调试工具:网页自动化与数据采集的革新利器
爬虫·python·drissionpage
不争先.11 小时前
URL 结构说明+路由(接口)的认识
python·pycharm·flask·apifox
(・Д・)ノ11 小时前
python打卡day44
人工智能·python·机器学习
胡西风_foxww11 小时前
Python 入门到进阶全指南:从语言特性到实战项目
开发语言·python·快速入门