admin管理员组文章数量:1027947
PyQt 日签应用制作
前言
前段时间,我迷上了“每日一句”这类 App:一大早起来,滑动屏幕就能看到一句文案,配上一张唯美背景图,仿佛给新的一天打了鸡血。渐渐地,我也想动手做一个桌面版“日签应用”,每天自动切换一句文案和一张背景图,简单、优雅,还能自己折腾界面和功能。
于是,一款基于 PyQt 的“日签应用”就在我的构思中萌芽。这篇博客,我将带你走过从需求收集、架构设计,到代码实现、调试优化,再到打包发布的全流程,以及我在开发过程中踩到的坑和解决思路,希望对你有所启发。
一、动机与需求
我先聊聊自己的使用场景:
- 自动化:每天不用手动切换,软件能根据本地时间自动加载当日文案和对应背景。
- 离线可用:文案和图片可以预先缓存在本地,如果没有网络也能正常显示。
- 可扩展:未来想接入在线 API、添加更多主题、支持自定义文案集。
- 简洁美观:界面极简,一句话+一张背景+日期+作者。灯箱式效果,赏心悦目。
- 轻量易打包:打包成一个可执行,朋友拿去不用配置,打开就能用。
根据这些思路,我草拟了几个核心功能:
- 配置管理:管理文案与背景图存放路径、定时更新时刻、缓存策略等;
- 本地资源加载:扫描指定文件夹,将文案(JSON/文本)和图片加载到内存或数据库;
- 定时刷新:用
QTimer
根据本地时间,切换到当日文案; - 界面展示:自定义窗口风格,通过 QSS 美化,使用 QLabel、QGraphicsView 展示文字和图片;
- 离线优先,在线备选:如果本地缓存不存在当日资源,再尝试向网络请求(备用 API);
- 打包发布:用 PyInstaller 一键生成可执行。
有了这些需求,下一步就要做整体架构设计,避免一头热写到一半逻辑凌乱。
二、整体架构与模块剖析
在编码之前,我习惯把整个项目拆成几个模块,画一张流程图理清各模块之间的交互。
模块划分
- MainWindow:主窗口,承载 UI 布局、信号连接、系统托盘行为。
- ConfigManager:管理用户设置(如资源路径、定时刷新时刻等),读写 JSON 配置文件。
- ResourceManager:负责加载、缓存和提供“日签”资源,包括本地扫描与在线下载。
- Cache:本地资源管理,按“年月日”组织子文件夹,便于查找与过期清理。
- OnlineFetcher:备用模块,可选地从网络 API 拉取每日文案与图片。
- TimerController:定时器逻辑,每天一点或应用启动时触发一次刷新,也支持手动调用。
- SystemTray:系统托盘图标、右键菜单、双击还原窗口。
有了这个高层结构,接下来依次对各模块落地,并在 MainWindow 中组装。
三、项目目录与文件结构
在本地新建项目文件夹 daily_quote_app/
,并组织如下目录结构:
daily_quote_app/
├── main.py
├── main_window.py
├── config_manager.py
├── resource_manager.py
├── timer_controller.py
├── online_fetcher.py
├── system_tray.py
├── resources/
│ ├── icons/
│ │ ├── tray.png
│ │ ├── refresh.png
│ │ └── settings.png
│ ├── style.qss
│ └── default_quotes.json
├── cache/ # 应用自动创建,用于存放缓存资源
└── config.json # 存储用户设置
- resources/:静态资源,包括图标、QSS 样式、内置文案 JSON
- cache/:按
cache/YYYY-MM-DD/
存放对应日期的文案与背景图 - config.json:记录资源目录、刷新时刻、在线 API 地址等配置
创建好文件夹后,先用命令生成 resources_rc.py
:
pyrcc5 resources/resources.qrc -o resources_rc.py
这一步会将图标、QSS、默认文案等打包成 Python 模块,方便在代码中通过 :/icons/tray.png
等路径引用。
四、配置管理 ConfigManager
配置管理决定应用的灵活性。这里我选择用 JSON 存储配置,简单易读,也便于手动修改。
代码语言:python代码运行次数:0运行复制# config_manager.py
import json
import os
class ConfigManager:
def __init__(self, config_path="config.json"):
self.config_path = config_path
# 默认配置
self.config = {
"refresh_time": "00:00", # 每天几点刷新日签
"use_online": False, # 是否启用在线抓取
"online_api": "", # 在线 API 地址
"resource_dir": "cache", # 本地缓存目录
"theme": "light" # 主题风格:light/dark
}
self.load()
def load(self):
if os.path.exists(self.config_path):
try:
with open(self.config_path, "r", encoding="utf-8") as f:
data = json.load(f)
self.config.update(data)
except Exception as e:
print(f"加载配置失败,使用默认设置:{e}")
self.save()
else:
self.save()
def save(self):
try:
with open(self.config_path, "w", encoding="utf-8") as f:
json.dump(self.config, f, ensure_ascii=False, indent=4)
print("配置已保存。")
except Exception as e:
print(f"保存配置失败:{e}")
def get(self, key):
return self.config.get(key)
def set(self, key, value):
self.config[key] = value
self.save()
关键点解析:
ensure_ascii=False
保证中文不被转义;indent=4
让 JSON 可读性更好;- 在加载失败时,自动重写文件,避免反复报错。
五、资源管理 ResourceManager
- 职责:根据日期提供当天文案及图片路径;
- 策略:先查本地
cache/YYYY-MM-DD/
,若资源齐全,则直接返回;否则,从内置 JSON 或在线接口取得资源后,存入缓存并返回。
# resource_manager.py
import os
import json
import shutil
from datetime import datetime
from PyQt5.QtCore import pyqtSignal, QObject
class ResourceManager(QObject):
updated = pyqtSignal(str, str) # 文案文本, 图片路径
def __init__(self, config):
super().__init__()
self.config = config
self.cache_dir = config.get("resource_dir")
os.makedirs(self.cache_dir, exist_ok=True)
# 内置默认文案
with open("resources/default_quotes.json", "r", encoding="utf-8") as f:
self.default_quotes = json.load(f)
def get_today(self):
today = datetime.now().strftime("%Y-%m-%d")
today_dir = os.path.join(self.cache_dir, today)
txt_path = os.path.join(today_dir, "quote.txt")
img_path = os.path.join(today_dir, "bg.jpg")
# 本地缓存检查
if os.path.exists(txt_path) and os.path.exists(img_path):
with open(txt_path, "r", encoding="utf-8") as f:
text = f.read()
self.updated.emit(text, img_path)
return
# 否则,准备缓存目录
shutil.rmtree(today_dir, ignore_errors=True)
os.makedirs(today_dir, exist_ok=True)
# 尝试在线获取
if self.config.get("use_online"):
from online_fetcher import OnlineFetcher
quote, img_data = OnlineFetcher(self.config.get("online_api")).fetch()
else:
# 从内置列表随机挑一句
import random
entry = random.choice(self.default_quotes)
quote = entry["text"]
img_data = None # 外置无需下载图片
# 写入本地
with open(txt_path, "w", encoding="utf-8") as f:
f.write(quote)
if img_data:
# 在线返回的是二进制
with open(img_path, "wb") as f:
f.write(img_data)
else:
# 本地内置背景图,可以在 resources 下准备若干图片,随机复制一个
bg_list = os.listdir("resources/backgrounds")
src = os.path.join("resources/backgrounds", random.choice(bg_list))
shutil.copy(src, img_path)
self.updated.emit(quote, img_path)
细节说明:
default_quotes.json
存了一组内置的{ "date": "...", "text": "..." }
或纯[{ "text": "...", "author": "..." }, ...]
。- 在线接口返回
(quote_text, image_binary)
,自定义OnlineFetcher
读取。 - 本地背景图放
resources/backgrounds/
,随机选一张。 - 使用
shutil.rmtree
清理旧缓存,避免文件残留。
六、在线获取 OnlineFetcher
如果用户开启 use_online
,就调用在线 API。如下示例,假设接口返回 JSON:
{
"text": "今日份文案……",
"image_url": ".jpg"
}
实现时要注意超时与异常处理:
代码语言:python代码运行次数:0运行复制# online_fetcher.py
import requests
from PyQt5.QtCore import QThread, pyqtSignal
class OnlineFetcher:
def __init__(self, api_url, timeout=5):
self.api_url = api_url
self.timeout = timeout
def fetch(self):
try:
resp = requests.get(self.api_url, timeout=self.timeout)
data = resp.json()
text = data.get("text", "")
# 下载图片二进制
img_data = requests.get(data.get("image_url", ""), timeout=self.timeout).content
return text, img_data
except Exception as e:
print(f"在线获取失败:{e}")
return "今日在线获取失败,换个姿势吧~", None
若环境不便安装 requests
,也可切换到标准库 urllib
。
七、定时刷新 TimerController
刷新有两种触发方式:应用启动时 和 每天设定时刻。我用 QTimer
每分钟检查一次当前时间是否到刷新时刻(配置里的 refresh_time
),也支持手动点击“刷新”按钮。
# timer_controller.py
from PyQt5.QtCore import QObject, QTimer, pyqtSignal
from datetime import datetime
class TimerController(QObject):
trigger = pyqtSignal()
def __init__(self, config):
super().__init__()
self.config = config
self.timer = QTimer(self)
self.timer.timeout.connect(self._check_time)
self.timer.start(60 * 1000) # 每分钟检查一次
def _check_time(self):
now = datetime.now().strftime("%H:%M")
if now == self.config.get("refresh_time"):
self.trigger.emit()
在 MainWindow
中,既要连接 trigger
信号,也要在应用启动后 立即 调用一次,保证打开时显示的是当天最新日签。
八、主界面 MainWindow
UI 核心是一个全屏或可拖拽、无边框的小窗体,显示背景图和一句文案,也要有“刷新”按钮和“设置”按钮。示例布局:
代码语言:python代码运行次数:0运行复制# main_window.py
from PyQt5.QtWidgets import QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget
from PyQt5.QtGui import QPixmap, QFont, QPalette, QColor
from PyQt5.QtCore import Qt
from config_manager import ConfigManager
from resource_manager import ResourceManager
from timer_controller import TimerController
from system_tray import SystemTray
import resources_rc
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.config = ConfigManager()
self.rm = ResourceManager(self.config)
self.tc = TimerController(self.config)
self.tray = SystemTray(self)
self._init_ui()
self._connect_signals()
# 首次刷新
self.refresh()
def _init_ui(self):
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.resize(600, 400)
# 背景标签
self.bg_label = QLabel(self)
self.bg_label.setScaledContents(True)
self.bg_label.setGeometry(0, 0, 600, 400)
# 文案标签
self.quote_label = QLabel("", self)
self.quote_label.setWordWrap(True)
self.quote_label.setAlignment(Qt.AlignCenter)
self.quote_label.setFont(QFont("楷体", 24))
self.quote_label.setStyleSheet("color: white;")
self.quote_label.setGeometry(20, 100, 560, 200)
# 刷新按钮
self.btn_refresh = QPushButton(QIcon(":/icons/refresh.png"), "", self)
self.btn_refresh.setGeometry(550, 350, 32, 32)
# 设置按钮
self.btn_settings = QPushButton(QIcon(":/icons/settings.png"), "", self)
self.btn_settings.setGeometry(510, 350, 32, 32)
def _connect_signals(self):
self.btn_refresh.clicked.connect(self.refresh)
self.rm.updated.connect(self._update_ui)
self.tc.trigger.connect(self.refresh)
def refresh(self):
self.rm.get_today()
def _update_ui(self, text, img_path):
pix = QPixmap(img_path).scaled(self.size(), Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
self.bg_label.setPixmap(pix)
self.quote_label.setText(text)
def mousePressEvent(self, event):
# 无边框窗体拖拽
self._drag_pos = event.globalPos()
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton:
self.move(self.pos() + event.globalPos() - self._drag_pos)
self._drag_pos = event.globalPos()
def closeEvent(self, event):
event.ignore()
self.hide()
self.tray.showMessage("日签应用已最小化到托盘", "双击恢复窗口")
要点提示:
- 设置无边框
Qt.FramelessWindowHint
,并用mousePressEvent
/mouseMoveEvent
实现窗口拖动; - 背景图片用
scaled
保证铺满窗口并保持比例; - 文案用半透明字体或加阴影效果,可在 QSS 中增强可读性;
- “刷新”、“设置”按钮放在角落,方便点击。
九、系统托盘 SystemTray
托盘交互与之前项目类似,右键菜单可“显示”、“设置”、“退出”。
代码语言:python代码运行次数:0运行复制# system_tray.py
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction
from PyQt5.QtGui import QIcon
import resources_rc
class SystemTray(QSystemTrayIcon):
def __init__(self, window):
super().__init__(QIcon(":/icons/tray.png"), window)
self.window = window
menu = QMenu()
show_act = QAction("显示主窗口", window)
exit_act = QAction("退出", window)
menu.addAction(show_act)
menu.addAction(exit_act)
self.setContextMenu(menu)
show_act.triggered.connect(self._show_window)
exit_act.triggered.connect(window.close)
self.activated.connect(self._on_activated)
self.show()
def _show_window(self):
self.window.show()
def _on_activated(self, reason):
if reason == QSystemTrayIcon.DoubleClick:
self._show_window()
十、样式美化与 QSS
为了让日签更有质感,我在 resources/style.qss
中写了如下片段:
QMainWindow {
background: transparent;
}
QLabel {
color: #ffffff;
text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
}
QPushButton {
border: none;
background: rgba(255,255,255,0.3);
border-radius: 4px;
}
QPushButton:hover {
background: rgba(255,255,255,0.5);
}
并在 main.py
中加载:
# main.py
import sys
from PyQt5.QtWidgets import QApplication
from main_window import MainWindow
import resources_rc
if __name__ == "__main__":
app = QApplication(sys.argv)
with open("resources/style.qss", "r", encoding="utf-8") as f:
app.setStyleSheet(f.read())
win = MainWindow()
win.show()
sys.exit(app.exec_())
十一、异常处理与容错
开发过程中,我遇到过几种意外情况:
- 缓存文件损坏或缺失:undefined
open(txt_path)
时报错,导致应用卡住。解决:在ResourceManager.get_today
加try/except
,失败后删除当日缓存目录重试或使用内置文案。 - 在线接口不可用:undefined
requests.get
超时或返回非 JSON。解决:捕获所有异常并返回 fallback 文案。 - 图片格式不支持:undefined一些远程图片为 WebP,PyQt 默认可能无法加载。解决:可使用 PIL(Pillow)先转换成 JPEG,再传入。
- 窗口位置超屏:undefined如果上次运行时拖到了多屏偏移位置,打开后窗口可能不可见。解决:在启动时判断
win.geometry()
,若超出主屏可视区,则居中显示。
十二、打包发布
完成所有功能后,就用 PyInstaller 打包,命令如下:
代码语言:bash复制pyinstaller --noconfirm --clean --windowed \
--name DailyQuote \
--add-data "resources/;resources/" \
main.py
--windowed
:不弹命令行黑框;--add-data
:把resources/
目录打包进去;- 打包后生成
dist/DailyQuote/
,包含可执行和依赖。
用户只需下载此文件夹,双击运行 DailyQuote.exe
(或无后缀可执行文件)即可体验每天一句的小确幸。
总结
从思考动机,到需求拆解,再到架构图、模块落地、代码实现、细节优化,最后到美化和打包,整个“日签应用”开发过程,既是一场 PyQt 的深度研习,也是一次 桌面小工具 的完整落地实践。
希望这篇 开发纪实,不仅分享了核心思路和代码片段,也能帮助你掌握如何从零构建一个可维护、易扩展的 PyQt 项目。若你正好也想做个“桌面日签”“待办小助手”“天气预报”“文件管理”等工具,不妨借鉴本文的模块划分与工作流程。
最后,祝你编码顺利,每天都有一句好文案,给工作和生活增添一丝小确幸。
PyQt 日签应用制作
前言
前段时间,我迷上了“每日一句”这类 App:一大早起来,滑动屏幕就能看到一句文案,配上一张唯美背景图,仿佛给新的一天打了鸡血。渐渐地,我也想动手做一个桌面版“日签应用”,每天自动切换一句文案和一张背景图,简单、优雅,还能自己折腾界面和功能。
于是,一款基于 PyQt 的“日签应用”就在我的构思中萌芽。这篇博客,我将带你走过从需求收集、架构设计,到代码实现、调试优化,再到打包发布的全流程,以及我在开发过程中踩到的坑和解决思路,希望对你有所启发。
一、动机与需求
我先聊聊自己的使用场景:
- 自动化:每天不用手动切换,软件能根据本地时间自动加载当日文案和对应背景。
- 离线可用:文案和图片可以预先缓存在本地,如果没有网络也能正常显示。
- 可扩展:未来想接入在线 API、添加更多主题、支持自定义文案集。
- 简洁美观:界面极简,一句话+一张背景+日期+作者。灯箱式效果,赏心悦目。
- 轻量易打包:打包成一个可执行,朋友拿去不用配置,打开就能用。
根据这些思路,我草拟了几个核心功能:
- 配置管理:管理文案与背景图存放路径、定时更新时刻、缓存策略等;
- 本地资源加载:扫描指定文件夹,将文案(JSON/文本)和图片加载到内存或数据库;
- 定时刷新:用
QTimer
根据本地时间,切换到当日文案; - 界面展示:自定义窗口风格,通过 QSS 美化,使用 QLabel、QGraphicsView 展示文字和图片;
- 离线优先,在线备选:如果本地缓存不存在当日资源,再尝试向网络请求(备用 API);
- 打包发布:用 PyInstaller 一键生成可执行。
有了这些需求,下一步就要做整体架构设计,避免一头热写到一半逻辑凌乱。
二、整体架构与模块剖析
在编码之前,我习惯把整个项目拆成几个模块,画一张流程图理清各模块之间的交互。
模块划分
- MainWindow:主窗口,承载 UI 布局、信号连接、系统托盘行为。
- ConfigManager:管理用户设置(如资源路径、定时刷新时刻等),读写 JSON 配置文件。
- ResourceManager:负责加载、缓存和提供“日签”资源,包括本地扫描与在线下载。
- Cache:本地资源管理,按“年月日”组织子文件夹,便于查找与过期清理。
- OnlineFetcher:备用模块,可选地从网络 API 拉取每日文案与图片。
- TimerController:定时器逻辑,每天一点或应用启动时触发一次刷新,也支持手动调用。
- SystemTray:系统托盘图标、右键菜单、双击还原窗口。
有了这个高层结构,接下来依次对各模块落地,并在 MainWindow 中组装。
三、项目目录与文件结构
在本地新建项目文件夹 daily_quote_app/
,并组织如下目录结构:
daily_quote_app/
├── main.py
├── main_window.py
├── config_manager.py
├── resource_manager.py
├── timer_controller.py
├── online_fetcher.py
├── system_tray.py
├── resources/
│ ├── icons/
│ │ ├── tray.png
│ │ ├── refresh.png
│ │ └── settings.png
│ ├── style.qss
│ └── default_quotes.json
├── cache/ # 应用自动创建,用于存放缓存资源
└── config.json # 存储用户设置
- resources/:静态资源,包括图标、QSS 样式、内置文案 JSON
- cache/:按
cache/YYYY-MM-DD/
存放对应日期的文案与背景图 - config.json:记录资源目录、刷新时刻、在线 API 地址等配置
创建好文件夹后,先用命令生成 resources_rc.py
:
pyrcc5 resources/resources.qrc -o resources_rc.py
这一步会将图标、QSS、默认文案等打包成 Python 模块,方便在代码中通过 :/icons/tray.png
等路径引用。
四、配置管理 ConfigManager
配置管理决定应用的灵活性。这里我选择用 JSON 存储配置,简单易读,也便于手动修改。
代码语言:python代码运行次数:0运行复制# config_manager.py
import json
import os
class ConfigManager:
def __init__(self, config_path="config.json"):
self.config_path = config_path
# 默认配置
self.config = {
"refresh_time": "00:00", # 每天几点刷新日签
"use_online": False, # 是否启用在线抓取
"online_api": "", # 在线 API 地址
"resource_dir": "cache", # 本地缓存目录
"theme": "light" # 主题风格:light/dark
}
self.load()
def load(self):
if os.path.exists(self.config_path):
try:
with open(self.config_path, "r", encoding="utf-8") as f:
data = json.load(f)
self.config.update(data)
except Exception as e:
print(f"加载配置失败,使用默认设置:{e}")
self.save()
else:
self.save()
def save(self):
try:
with open(self.config_path, "w", encoding="utf-8") as f:
json.dump(self.config, f, ensure_ascii=False, indent=4)
print("配置已保存。")
except Exception as e:
print(f"保存配置失败:{e}")
def get(self, key):
return self.config.get(key)
def set(self, key, value):
self.config[key] = value
self.save()
关键点解析:
ensure_ascii=False
保证中文不被转义;indent=4
让 JSON 可读性更好;- 在加载失败时,自动重写文件,避免反复报错。
五、资源管理 ResourceManager
- 职责:根据日期提供当天文案及图片路径;
- 策略:先查本地
cache/YYYY-MM-DD/
,若资源齐全,则直接返回;否则,从内置 JSON 或在线接口取得资源后,存入缓存并返回。
# resource_manager.py
import os
import json
import shutil
from datetime import datetime
from PyQt5.QtCore import pyqtSignal, QObject
class ResourceManager(QObject):
updated = pyqtSignal(str, str) # 文案文本, 图片路径
def __init__(self, config):
super().__init__()
self.config = config
self.cache_dir = config.get("resource_dir")
os.makedirs(self.cache_dir, exist_ok=True)
# 内置默认文案
with open("resources/default_quotes.json", "r", encoding="utf-8") as f:
self.default_quotes = json.load(f)
def get_today(self):
today = datetime.now().strftime("%Y-%m-%d")
today_dir = os.path.join(self.cache_dir, today)
txt_path = os.path.join(today_dir, "quote.txt")
img_path = os.path.join(today_dir, "bg.jpg")
# 本地缓存检查
if os.path.exists(txt_path) and os.path.exists(img_path):
with open(txt_path, "r", encoding="utf-8") as f:
text = f.read()
self.updated.emit(text, img_path)
return
# 否则,准备缓存目录
shutil.rmtree(today_dir, ignore_errors=True)
os.makedirs(today_dir, exist_ok=True)
# 尝试在线获取
if self.config.get("use_online"):
from online_fetcher import OnlineFetcher
quote, img_data = OnlineFetcher(self.config.get("online_api")).fetch()
else:
# 从内置列表随机挑一句
import random
entry = random.choice(self.default_quotes)
quote = entry["text"]
img_data = None # 外置无需下载图片
# 写入本地
with open(txt_path, "w", encoding="utf-8") as f:
f.write(quote)
if img_data:
# 在线返回的是二进制
with open(img_path, "wb") as f:
f.write(img_data)
else:
# 本地内置背景图,可以在 resources 下准备若干图片,随机复制一个
bg_list = os.listdir("resources/backgrounds")
src = os.path.join("resources/backgrounds", random.choice(bg_list))
shutil.copy(src, img_path)
self.updated.emit(quote, img_path)
细节说明:
default_quotes.json
存了一组内置的{ "date": "...", "text": "..." }
或纯[{ "text": "...", "author": "..." }, ...]
。- 在线接口返回
(quote_text, image_binary)
,自定义OnlineFetcher
读取。 - 本地背景图放
resources/backgrounds/
,随机选一张。 - 使用
shutil.rmtree
清理旧缓存,避免文件残留。
六、在线获取 OnlineFetcher
如果用户开启 use_online
,就调用在线 API。如下示例,假设接口返回 JSON:
{
"text": "今日份文案……",
"image_url": ".jpg"
}
实现时要注意超时与异常处理:
代码语言:python代码运行次数:0运行复制# online_fetcher.py
import requests
from PyQt5.QtCore import QThread, pyqtSignal
class OnlineFetcher:
def __init__(self, api_url, timeout=5):
self.api_url = api_url
self.timeout = timeout
def fetch(self):
try:
resp = requests.get(self.api_url, timeout=self.timeout)
data = resp.json()
text = data.get("text", "")
# 下载图片二进制
img_data = requests.get(data.get("image_url", ""), timeout=self.timeout).content
return text, img_data
except Exception as e:
print(f"在线获取失败:{e}")
return "今日在线获取失败,换个姿势吧~", None
若环境不便安装 requests
,也可切换到标准库 urllib
。
七、定时刷新 TimerController
刷新有两种触发方式:应用启动时 和 每天设定时刻。我用 QTimer
每分钟检查一次当前时间是否到刷新时刻(配置里的 refresh_time
),也支持手动点击“刷新”按钮。
# timer_controller.py
from PyQt5.QtCore import QObject, QTimer, pyqtSignal
from datetime import datetime
class TimerController(QObject):
trigger = pyqtSignal()
def __init__(self, config):
super().__init__()
self.config = config
self.timer = QTimer(self)
self.timer.timeout.connect(self._check_time)
self.timer.start(60 * 1000) # 每分钟检查一次
def _check_time(self):
now = datetime.now().strftime("%H:%M")
if now == self.config.get("refresh_time"):
self.trigger.emit()
在 MainWindow
中,既要连接 trigger
信号,也要在应用启动后 立即 调用一次,保证打开时显示的是当天最新日签。
八、主界面 MainWindow
UI 核心是一个全屏或可拖拽、无边框的小窗体,显示背景图和一句文案,也要有“刷新”按钮和“设置”按钮。示例布局:
代码语言:python代码运行次数:0运行复制# main_window.py
from PyQt5.QtWidgets import QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget
from PyQt5.QtGui import QPixmap, QFont, QPalette, QColor
from PyQt5.QtCore import Qt
from config_manager import ConfigManager
from resource_manager import ResourceManager
from timer_controller import TimerController
from system_tray import SystemTray
import resources_rc
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.config = ConfigManager()
self.rm = ResourceManager(self.config)
self.tc = TimerController(self.config)
self.tray = SystemTray(self)
self._init_ui()
self._connect_signals()
# 首次刷新
self.refresh()
def _init_ui(self):
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.resize(600, 400)
# 背景标签
self.bg_label = QLabel(self)
self.bg_label.setScaledContents(True)
self.bg_label.setGeometry(0, 0, 600, 400)
# 文案标签
self.quote_label = QLabel("", self)
self.quote_label.setWordWrap(True)
self.quote_label.setAlignment(Qt.AlignCenter)
self.quote_label.setFont(QFont("楷体", 24))
self.quote_label.setStyleSheet("color: white;")
self.quote_label.setGeometry(20, 100, 560, 200)
# 刷新按钮
self.btn_refresh = QPushButton(QIcon(":/icons/refresh.png"), "", self)
self.btn_refresh.setGeometry(550, 350, 32, 32)
# 设置按钮
self.btn_settings = QPushButton(QIcon(":/icons/settings.png"), "", self)
self.btn_settings.setGeometry(510, 350, 32, 32)
def _connect_signals(self):
self.btn_refresh.clicked.connect(self.refresh)
self.rm.updated.connect(self._update_ui)
self.tc.trigger.connect(self.refresh)
def refresh(self):
self.rm.get_today()
def _update_ui(self, text, img_path):
pix = QPixmap(img_path).scaled(self.size(), Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
self.bg_label.setPixmap(pix)
self.quote_label.setText(text)
def mousePressEvent(self, event):
# 无边框窗体拖拽
self._drag_pos = event.globalPos()
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton:
self.move(self.pos() + event.globalPos() - self._drag_pos)
self._drag_pos = event.globalPos()
def closeEvent(self, event):
event.ignore()
self.hide()
self.tray.showMessage("日签应用已最小化到托盘", "双击恢复窗口")
要点提示:
- 设置无边框
Qt.FramelessWindowHint
,并用mousePressEvent
/mouseMoveEvent
实现窗口拖动; - 背景图片用
scaled
保证铺满窗口并保持比例; - 文案用半透明字体或加阴影效果,可在 QSS 中增强可读性;
- “刷新”、“设置”按钮放在角落,方便点击。
九、系统托盘 SystemTray
托盘交互与之前项目类似,右键菜单可“显示”、“设置”、“退出”。
代码语言:python代码运行次数:0运行复制# system_tray.py
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction
from PyQt5.QtGui import QIcon
import resources_rc
class SystemTray(QSystemTrayIcon):
def __init__(self, window):
super().__init__(QIcon(":/icons/tray.png"), window)
self.window = window
menu = QMenu()
show_act = QAction("显示主窗口", window)
exit_act = QAction("退出", window)
menu.addAction(show_act)
menu.addAction(exit_act)
self.setContextMenu(menu)
show_act.triggered.connect(self._show_window)
exit_act.triggered.connect(window.close)
self.activated.connect(self._on_activated)
self.show()
def _show_window(self):
self.window.show()
def _on_activated(self, reason):
if reason == QSystemTrayIcon.DoubleClick:
self._show_window()
十、样式美化与 QSS
为了让日签更有质感,我在 resources/style.qss
中写了如下片段:
QMainWindow {
background: transparent;
}
QLabel {
color: #ffffff;
text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
}
QPushButton {
border: none;
background: rgba(255,255,255,0.3);
border-radius: 4px;
}
QPushButton:hover {
background: rgba(255,255,255,0.5);
}
并在 main.py
中加载:
# main.py
import sys
from PyQt5.QtWidgets import QApplication
from main_window import MainWindow
import resources_rc
if __name__ == "__main__":
app = QApplication(sys.argv)
with open("resources/style.qss", "r", encoding="utf-8") as f:
app.setStyleSheet(f.read())
win = MainWindow()
win.show()
sys.exit(app.exec_())
十一、异常处理与容错
开发过程中,我遇到过几种意外情况:
- 缓存文件损坏或缺失:undefined
open(txt_path)
时报错,导致应用卡住。解决:在ResourceManager.get_today
加try/except
,失败后删除当日缓存目录重试或使用内置文案。 - 在线接口不可用:undefined
requests.get
超时或返回非 JSON。解决:捕获所有异常并返回 fallback 文案。 - 图片格式不支持:undefined一些远程图片为 WebP,PyQt 默认可能无法加载。解决:可使用 PIL(Pillow)先转换成 JPEG,再传入。
- 窗口位置超屏:undefined如果上次运行时拖到了多屏偏移位置,打开后窗口可能不可见。解决:在启动时判断
win.geometry()
,若超出主屏可视区,则居中显示。
十二、打包发布
完成所有功能后,就用 PyInstaller 打包,命令如下:
代码语言:bash复制pyinstaller --noconfirm --clean --windowed \
--name DailyQuote \
--add-data "resources/;resources/" \
main.py
--windowed
:不弹命令行黑框;--add-data
:把resources/
目录打包进去;- 打包后生成
dist/DailyQuote/
,包含可执行和依赖。
用户只需下载此文件夹,双击运行 DailyQuote.exe
(或无后缀可执行文件)即可体验每天一句的小确幸。
总结
从思考动机,到需求拆解,再到架构图、模块落地、代码实现、细节优化,最后到美化和打包,整个“日签应用”开发过程,既是一场 PyQt 的深度研习,也是一次 桌面小工具 的完整落地实践。
希望这篇 开发纪实,不仅分享了核心思路和代码片段,也能帮助你掌握如何从零构建一个可维护、易扩展的 PyQt 项目。若你正好也想做个“桌面日签”“待办小助手”“天气预报”“文件管理”等工具,不妨借鉴本文的模块划分与工作流程。
最后,祝你编码顺利,每天都有一句好文案,给工作和生活增添一丝小确幸。
本文标签: PyQt 日签应用制作
版权声明:本文标题:PyQt 日签应用制作 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://it.en369.cn/jiaocheng/1747435184a2166332.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论