首页 > 编程开发 > Python    日期:2026-08-19 / 浏览

效果展示

源码分享

main.py

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())

库安装

实现原理

QMovie 是什么?

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

核心特点

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

核心用法详解

创建与设置

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()

播放控制

方法 说明 示例
start() 开始播放 movie.start()
stop() 停止播放(重置到第一帧) movie.stop()
setPaused(bool) 暂停/恢复 movie.setPaused(True)
jumpToFrame(int) 跳转到指定帧 movie.jumpToFrame(10)
setSpeed(int) 设置播放速度(百分比) movie.setSpeed(200) # 2倍速

重要信号

# 帧变化时触发
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)}")
)

在宠物项目中的应用

本项目(桌面宠物)中,QMovie 用于显示宠物的 GIF 动画:

# 初始化
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 与显示控件绑定。

常见问题与解决方案

问题1:GIF 播放卡顿/闪烁

原因QMovie 默认使用 QImageReader 逐帧解码,大尺寸 GIF 可能解码慢。

解决

  • 使用 movie.setCacheMode(QMovie.CacheMode.CacheAll) 预缓存所有帧。
  • 减小 GIF 尺寸或帧率。

问题2:透明背景失效

原因:某些 GIF 的透明通道未被正确识别。

解决

# 设置背景透明
label.setStyleSheet("background: transparent;")
# 或
label.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)

问题3:内存泄漏

原因QMovie 未及时删除,或 GIF 文件过大。

解决

# 不再使用时主动删除
movie.deleteLater()
# 或使用局部变量让垃圾回收自动处理

问题4:多 GIF 同时播放性能差

原因:每个 QMovie 都在主线程解码。

解决

  • 使用 QThread 将解码移到子线程(复杂)。
  • 改用 QGraphicsView + QGraphicsPixmapItem 组合,性能更好。

进阶用法

自定义帧渲染

# 获取当前帧的 QPixmap
current_pixmap = movie.currentPixmap()

# 手动绘制到 QLabel
label.setPixmap(current_pixmap.scaled(100, 100))

动态切换动画

def switch_animation(new_gif_path):
    movie.stop()
    movie.setFileName(new_gif_path)
    movie.start()

与 QTimer 结合实现帧控制

timer = QTimer()
timer.timeout.connect(lambda: movie.jumpToNextFrame())
timer.start(100)  # 每100ms跳一帧

总结

QMovie 是 PyQt6 中处理简单动画的最高效工具,尤其适合:

  • 桌面宠物、表情动画等轻量级应用
  • GIF/APNG 格式的序列帧动画
  • 需要简单播放控制的场景

局限性

  • 不支持视频格式(MP4、AVI 等)
  • 复杂动画建议使用 QGraphicsViewQt Quick

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

 
 

觉得上面的内容有用吗?快来点个赞吧!

点赞() 我要打赏

温馨提示 : 本站内容来自会员投稿以及互联网,所有源码及教程均为作者总结编辑,请大家在使用过程中提前做好备份,以免发生无法预知的错误,源码类教程请勿直接用于生产环境!

 可能感兴趣的文章

1 2 3 4 5