PyQt 实现自己的桌面萌宠源码分享

PyQt 实现自己的桌面萌宠源码分享

  • 一、效果展示
  • 二、源码分享
  • 三、实现原理
    • [1、 QMovie 是什么?](#1、 QMovie 是什么?)
    • 2、核心用法详解
      • [2.1、 创建与设置](#2.1、 创建与设置)
      • [2.2 、播放控制](#2.2 、播放控制)
      • [2.3 、重要信号](#2.3 、重要信号)
    • [3、 在宠物项目中的应用](#3、 在宠物项目中的应用)
    • [4、 常见问题与解决方案](#4、 常见问题与解决方案)
    • [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、资源下载

Qt QWidget实现自己的桌面萌宠图片资源分享

三、实现原理

1、 QMovie 是什么?

QMovie 是 PyQt6 中用于播放动画(如 GIF、APNG、MNG 等格式)的核心类。它本身不是一个可视部件,而是一个动画数据源 ,需要与 QLabel(或 QGraphicsPixmapItem 等)配合才能显示动画。

核心特点

  • 轻量级:专门为序列帧动画设计,不依赖多媒体框架(如 Qt Multimedia)。
  • 格式支持:默认支持 GIF、APNG、MNG;可通过插件扩展支持更多格式。
  • 控制灵活:可控制播放、暂停、跳帧、速度等。
  • 信号驱动 :提供 frameChangedfinished 等信号,便于响应动画状态。

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)

关键技巧

  1. setScaledSize():确保 GIF 适应窗口尺寸,避免拉伸变形。
  2. frameChanged 信号:用于检测动画播放进度,实现"播放一次后停止"的效果。
  3. 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 等)
  • ❌ 复杂动画建议使用 QGraphicsViewQt Quick

在本项目中,我们利用 QMovie 的轻量特性,实现了宠物 GIF 的流畅播放与帧控制,是桌面宠物动画的核心技术支撑。

相关推荐
小灰灰搞电子3 天前
PyQt6 QCommandLineParser 类详解:命令行参数解析实战指南
pyqt·参数解析
微小冷5 天前
pyQT中的文本部件及其区别
pyqt·gui·富文本编辑器·文本输入框·文本浏览器
房开民9 天前
PyQt 翻译(国际化)极简讲解
pyqt
房开民13 天前
PyQt5 常用模块(对应Qt五大模块)
数据库·pyqt
懷淰メ17 天前
【AI赋能】基于PyQt+YOLO+DeepSeek水上漂浮物检测系统(详细介绍)
人工智能·yolo·目标检测·计算机视觉·pyqt·漂浮物·水上漂浮物
龙腾AI白云1 个月前
【多Agent系统的倒U型曲线与前瞻治理】
人工智能·plotly·pyqt·知识图谱
江畔柳前堤2 个月前
github实战指南01-账号配置与 SSH 密钥
运维·人工智能·深度学习·ssh·github·pyqt·信号处理
DrMaker2 个月前
【无标题】
软件测试·python·测试工具·pyqt
懷淰メ3 个月前
【AI赋能】基于PyQt+YOLO+DeepSeek的淋巴细胞检测系统(详细介绍)
yolo·计算机视觉·pyqt·课程设计·医疗·淋巴细胞·淋巴