PyQt 实现自己的桌面萌宠源码分享
- 一、效果展示
- 二、源码分享
- 三、实现原理
-
- [1、 QMovie 是什么?](#1、 QMovie 是什么?)
- 2、核心用法详解
-
- [2.1、 创建与设置](#2.1、 创建与设置)
- [2.2 、播放控制](#2.2 、播放控制)
- [2.3 、重要信号](#2.3 、重要信号)
- [3、 在宠物项目中的应用](#3、 在宠物项目中的应用)
- [4、 常见问题与解决方案](#4、 常见问题与解决方案)
-
- [问题1:GIF 播放卡顿/闪烁](#问题1:GIF 播放卡顿/闪烁)
- 问题2:透明背景失效
- 问题3:内存泄漏
- [问题4:多 GIF 同时播放性能差](#问题4:多 GIF 同时播放性能差)
- [5、 进阶用法](#5、 进阶用法)
-
- [5.1 、自定义帧渲染](#5.1 、自定义帧渲染)
- [5.2 、动态切换动画](#5.2 、动态切换动画)
- [5.3 、与 QTimer 结合实现帧控制](#5.3 、与 QTimer 结合实现帧控制)
- [6、 总结](#6、 总结)
一、效果展示


二、源码分享
1、main.py
python
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QMenu)
from PyQt6.QtGui import QMovie, QCursor, QFont, QAction
from PyQt6.QtCore import Qt, QPoint, QTimer, QEvent
class PetDialogWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(220, 80)
self.timer_close = QTimer(self)
self.init_ui()
def init_ui(self):
self.setWindowFlags(Qt.WindowType.FramelessWindowHint
| Qt.WindowType.WindowStaysOnTopHint
| Qt.WindowType.Tool)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.label_content = QLabel("", self)
self.label_content.setWordWrap(True)
self.label_content.setStyleSheet("""
QLabel {
border-image: url(image/background.svg) 0 0 0 0 stretch stretch;
border-radius: 8px;
padding: 12px 10px;
font-size: 12px;
color: #333333;
}
""")
self.label_content.setGeometry(0, 0, self.width(), self.height())
self.timer_close.timeout.connect(self._on_timeout_close)
def _on_timeout_close(self):
self.close()
self.timer_close.stop()
def show_content(self, x: int, y: int, content: str, timeout_ms: int):
self.label_content.setText(content)
self.timer_close.setInterval(timeout_ms)
self.timer_close.start()
self.move(x, y)
self.show()
class PetWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.pet_dialog = None
self.movie = None
self.pressed_point = QPoint()
self.left_btn_pressed = False
# ========== 新增菜单锁定标志 ==========
self.is_menu_opening = False
self.init_control()
def init_control(self):
self.setWindowFlags(Qt.WindowType.FramelessWindowHint
| Qt.WindowType.WindowStaysOnTopHint
| Qt.WindowType.Tool)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setFixedSize(100, 150)
self.pet_dialog = PetDialogWidget()
self.lbl_gif = QLabel("", self)
self.lbl_gif.setGeometry(0, 0, self.width(), self.height())
# 加载GIF
self.movie = QMovie("image/pet.gif")
self.movie.setScaledSize(self.size())
self.lbl_gif.setMovie(self.movie)
def on_frame_changed(frame_num):
if frame_num == self.movie.frameCount() - 1:
self.movie.stop()
self.movie.frameChanged.connect(on_frame_changed)
self.movie.start()
self.installEventFilter(self)
self.lbl_gif.installEventFilter(self)
self.lbl_gif.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.popup_menu)
def show_dialog(self, content: str, timeout: int):
dlg_w = self.pet_dialog.width()
dlg_h = self.pet_dialog.height()
x = self.x() + (self.width() - dlg_w) // 2
y = self.y() - dlg_h
self.pet_dialog.show_content(x, y, content, timeout)
def popup_menu(self, local_pos: QPoint):
# 互斥锁,防止连续右键弹出多个菜单
if self.is_menu_opening:
return
self.is_menu_opening = True
# ===== 核心修复:局部坐标转屏幕全局坐标 =====
global_pos = self.mapToGlobal(local_pos)
# 偏移10像素,避免菜单被宠物GIF本体遮挡
pop_pos = global_pos + QPoint(10, 10)
menu = QMenu()
menu.setStyleSheet("""
QMenu {
background-color: #FDA330;
border: 1px solid white;
font-size: 13px;
border-radius:10px;
}
QMenu::item {
padding: 8px 24px;
border: 1px solid white;
}
QMenu::item:selected {
background: #00ffff;
color: #0066cc;
}
""")
# 菜单强制置顶,不会被置顶主窗口盖住
menu.setWindowFlags(
Qt.WindowType.FramelessWindowHint
| Qt.WindowType.NoDropShadowWindowHint
| Qt.WindowType.WindowStaysOnTopHint
)
menu.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
menu.addAction("菜单项1")
menu.addAction("菜单项2")
menu.addAction("菜单项3")
menu.addAction("菜单项4")
menu.addAction("菜单项5")
act_exit = menu.addAction("退出程序")
def slot_click(action):
if action == act_exit:
QApplication.quit()
else:
menu.close()
menu.triggered.connect(slot_click)
menu.exec(pop_pos)
# 菜单关闭后解锁
self.is_menu_opening = False
def eventFilter(self, obj, event):
# 优先捕获右键
if event.type() == QEvent.Type.MouseButtonPress:
me = event
if me.button() == Qt.MouseButton.RightButton:
# self.popup_menu(me.globalPosition().toPoint())
return True
# 左键拖拽逻辑
if obj == self:
if event.type() == QEvent.Type.MouseButtonPress:
me = event
if me.button() == Qt.MouseButton.LeftButton:
self.pressed_point = me.globalPosition().toPoint() - self.pos()
self.left_btn_pressed = True
self.lbl_gif.setCursor(QCursor(Qt.CursorShape.ClosedHandCursor))
elif event.type() == QEvent.Type.MouseMove:
me = event
if self.left_btn_pressed:
new_pos = me.globalPosition().toPoint() - self.pressed_point
self.move(new_pos)
dlg_w = self.pet_dialog.width()
dlg_h = self.pet_dialog.height()
dlg_x = self.x() + (self.width() - dlg_w) // 2
dlg_y = self.y() - dlg_h
self.pet_dialog.move(dlg_x, dlg_y)
elif event.type() == QEvent.Type.MouseButtonRelease:
me = event
if me.button() == Qt.MouseButton.LeftButton:
self.left_btn_pressed = False
self.lbl_gif.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
elif obj == self.lbl_gif:
if event.type() == QEvent.Type.Enter:
self.movie.start()
return False
def closeEvent(self, event):
if self.pet_dialog:
self.pet_dialog.close()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
pet = PetWidget()
pet.show()
test_timer = QTimer()
test_timer.singleShot(3000, lambda: pet.show_dialog("主人你好呀~", 4000))
sys.exit(app.exec())
2、库安装

3、资源下载
三、实现原理
1、 QMovie 是什么?
QMovie 是 PyQt6 中用于播放动画(如 GIF、APNG、MNG 等格式)的核心类。它本身不是一个可视部件,而是一个动画数据源 ,需要与 QLabel(或 QGraphicsPixmapItem 等)配合才能显示动画。
核心特点:
- 轻量级:专门为序列帧动画设计,不依赖多媒体框架(如 Qt Multimedia)。
- 格式支持:默认支持 GIF、APNG、MNG;可通过插件扩展支持更多格式。
- 控制灵活:可控制播放、暂停、跳帧、速度等。
- 信号驱动 :提供
frameChanged、finished等信号,便于响应动画状态。
2、核心用法详解
2.1、 创建与设置
python
from PyQt6.QtGui import QMovie
# 1. 创建 QMovie 对象
movie = QMovie("path/to/animation.gif")
# 2. 设置显示控件(通常是 QLabel)
label = QLabel()
label.setMovie(movie)
# 3. 可选:设置缩放尺寸
movie.setScaledSize(QSize(200, 200))
# 4. 开始播放
movie.start()
2.2 、播放控制
| 方法 | 说明 | 示例 |
|---|---|---|
start() |
开始播放 | movie.start() |
stop() |
停止播放(重置到第一帧) | movie.stop() |
setPaused(bool) |
暂停/恢复 | movie.setPaused(True) |
jumpToFrame(int) |
跳转到指定帧 | movie.jumpToFrame(10) |
setSpeed(int) |
设置播放速度(百分比) | movie.setSpeed(200) # 2倍速 |
2.3 、重要信号
python
# 帧变化时触发
movie.frameChanged.connect(lambda frame: print(f"当前帧: {frame}"))
# 动画播放完成时触发
movie.finished.connect(lambda: print("动画播放完毕"))
# 状态变化时触发(QMovie.MovieState)
movie.stateChanged.connect(
lambda state: print(f"状态: {QMovie.movieStateToString(state)}")
)
3、 在宠物项目中的应用
本项目(桌面宠物)中,QMovie 用于显示宠物的 GIF 动画:
python
# 初始化
self.movie = QMovie("image/pet.gif")
self.movie.setScaledSize(self.size()) # 缩放到窗口大小
self.lbl_gif.setMovie(self.movie)
# 播放控制
self.movie.start() # 开始播放
self.movie.stop() # 停止播放
# 帧控制:播放到最后一帧时停止
def on_frame_changed(frame_num):
if frame_num == self.movie.frameCount() - 1:
self.movie.stop()
self.movie.frameChanged.connect(on_frame_changed)
关键技巧:
setScaledSize():确保 GIF 适应窗口尺寸,避免拉伸变形。frameChanged信号:用于检测动画播放进度,实现"播放一次后停止"的效果。QLabel.setMovie():将QMovie与显示控件绑定。
4、 常见问题与解决方案
问题1:GIF 播放卡顿/闪烁
原因 :QMovie 默认使用 QImageReader 逐帧解码,大尺寸 GIF 可能解码慢。
解决:
- 使用
movie.setCacheMode(QMovie.CacheMode.CacheAll)预缓存所有帧。 - 减小 GIF 尺寸或帧率。
问题2:透明背景失效
原因 :某些 GIF 的透明通道未被正确识别。
解决:
python
# 设置背景透明
label.setStyleSheet("background: transparent;")
# 或
label.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
问题3:内存泄漏
原因 :QMovie 未及时删除,或 GIF 文件过大。
解决:
python
# 不再使用时主动删除
movie.deleteLater()
# 或使用局部变量让垃圾回收自动处理
问题4:多 GIF 同时播放性能差
原因 :每个 QMovie 都在主线程解码。
解决:
- 使用
QThread将解码移到子线程(复杂)。 - 改用
QGraphicsView+QGraphicsPixmapItem组合,性能更好。
5、 进阶用法
5.1 、自定义帧渲染
python
# 获取当前帧的 QPixmap
current_pixmap = movie.currentPixmap()
# 手动绘制到 QLabel
label.setPixmap(current_pixmap.scaled(100, 100))
5.2 、动态切换动画
python
def switch_animation(new_gif_path):
movie.stop()
movie.setFileName(new_gif_path)
movie.start()
5.3 、与 QTimer 结合实现帧控制
python
timer = QTimer()
timer.timeout.connect(lambda: movie.jumpToNextFrame())
timer.start(100) # 每100ms跳一帧
6、 总结
QMovie 是 PyQt6 中处理简单动画的最高效工具,尤其适合:
- ✅ 桌面宠物、表情动画等轻量级应用
- ✅ GIF/APNG 格式的序列帧动画
- ✅ 需要简单播放控制的场景
局限性:
- ❌ 不支持视频格式(MP4、AVI 等)
- ❌ 复杂动画建议使用
QGraphicsView或Qt Quick
在本项目中,我们利用 QMovie 的轻量特性,实现了宠物 GIF 的流畅播放与帧控制,是桌面宠物动画的核心技术支撑。
