放弃了豆包,我使用Python做了一个桌面宠物

故事的开始

故事的开始,在抖音上刷到有人使用豆包生成桌面宠物

比如把自己的猫猫图片给豆包,之后再把下面这段话给他

就可以生成桌宠了

bash 复制代码
项目目标
开发一个基于 Windows 平台的桌面宠物应用程序,单文件 EXE 运行,无需安装依赖。
核心功能要求
窗口特性
透明与无边框:窗口背景完全透明,仅显示角色图像,无系统默认边框和标题栏。
置顶逻辑:默认"始终置顶",但在右键菜单中可切换开关。
鼠标穿透:除角色主体外,透明区域应支持鼠标穿透(即点击透明处不会阻挡下方窗口的操作)。
图像处理
自动抠图:程序启动时自动去除图片白色背景(建议使用色度键控或边缘检测算法),确保角色边缘平滑。
资源加载:支持加载用户提供的 PNG/JPG 格式角色图。
交互逻辑
拖拽移动:按住鼠标左键在角色身上拖动,可自由移动桌宠位置。
滚轮缩放:滚动鼠标滚轮可实时调整桌宠大小(需设置最小/最大缩放限制,防止过小或溢出屏幕)。
点击互动:
左键单击角色,随机触发以下动画之一(需通过代码变换实现,无需额外素材):
跳跃:Y轴快速位移后回落。
压扁回弹:X轴拉伸、Y轴压缩后恢复。
左右抖动:X轴快速左右微移。
对话气泡:触发互动时,在角色头顶或侧方随机显示一条中文趣味台词。气泡需有独立背景色(不透明),且位置需动态计算,严禁遮挡角色面部。
系统菜单(右键)
点击鼠标右键呼出上下文菜单,包含:
调整大小:输入具体数值或滑块调整。
置顶开关:切换 AlwaysOnTop 状态。
退出程序:彻底关闭进程。
交付形式
最终交付物为一个独立的 .exe 文件,双击即可运行,无需用户配置 Python 环境或安装库。

结果就是把我的豆包累坏了,中道休息,提示我7月26日才能为我继续服务

正文

好在执行过程中我看到了他的思考过程,大致了解了实现的思路,于是便有了下面这段代码。

我用夸克网盘给你分享了「桌面宠物」打开「夸克APP」即可获取。

链接:https://pan.quark.cn/s/815d05e72fe6

功能介绍

单机人物,会有对话提示,以及触发跳一跳,摇一摇动作:

右键退出程序

🐱 桌面宠物功能清单

加载 pet.png 作为宠物图标,自动去除白色背景(逐像素处理)

无边框透明悬浮窗,始终显示在桌面上

1. 鼠标交互
操作 效果
左键单击 触发互动(随机动画 + 对话气泡)
左键拖拽 拖动宠物移动位置
右键 弹出功能菜单
滚轮 放大/缩小宠物(范围 30% ~ 300%)
2. 随机互动(单击触发)
  • 对话 :宠物头顶弹出对话,3 秒后自动消失
    • 气泡文字从 6 条短语中随机选取(如 "今天也要加油鸭!")
  • 动画 :随机执行以下一种
    • 跳跃 🐇 --- 上跳 再落回
    • 摇晃 🤪 --- 左右快速摆动
3. 右键菜单
菜单项 功能
切换置顶 切换窗口是否始终在最前
退出程序 直接退出

代码

python 复制代码
import sys
import random
from PyQt5.QtWidgets import QApplication, QLabel, QMenu, QAction
from PyQt5.QtCore import Qt, QTimer, QPoint, QPropertyAnimation, QEasingCurve
from PyQt5.QtGui import QPixmap, QImage, QColor
from PyQt5 import sip

# --- 配置区 ---
PET_IMAGE_PATH = "pet.png"
DIALOGS = [
    "今天也要加油鸭!", "摸摸头~", "你在看什么呢?",
    "好饿呀...", "动一动更健康!", "哇!被发现了!"
]


class DesktopPet(QLabel):
    def __init__(self):
        super().__init__()
        self._animations = []  # 持有动画引用,防止被GC回收导致崩溃
        self.init_ui()
        self.load_and_process_image()
        self.setup_timers()

    def init_ui(self):
        # 【修复】使用 Qt.Window + CustomizeWindowHint 替代 SubWindow/Tool
        # 这是 Windows 下透明悬浮窗最稳定的标志组合
        self.setWindowFlags(
            Qt.Window |
            Qt.FramelessWindowHint |
            Qt.WindowStaysOnTopHint |
            Qt.CustomizeWindowHint
        )
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setAttribute(Qt.WA_NoSystemBackground)
        # 关键:禁用窗口自动重置,防止动画/点击时系统隐藏窗口
        self.setAttribute(Qt.WA_ShowWithoutActivating)
        self.setCursor(Qt.OpenHandCursor)

        self.base_size = 200
        self.current_scale = 1.0
        self.resize(self.base_size, self.base_size)

        self.dragging = False
        self.offset = QPoint()
        self.drag_start_pos = QPoint()
        self.animating = False
        self._click_cooling = False  # 点击冷却标志,防连点

        # 确保窗口不会被任务栏捕获
        self.setWindowModality(Qt.NonModal)

    def load_and_process_image(self):
        """【修复】使用纯Qt原生方式加载并去白底,彻底移除PIL"""
        try:
            pixmap = QPixmap(PET_IMAGE_PATH)
            if pixmap.isNull():
                raise FileNotFoundError(f"无法加载图片: {PET_IMAGE_PATH}")

            image = pixmap.toImage().convertToFormat(QImage.Format_ARGB32)
            w, h = image.width(), image.height()

            # 原生Qt逐像素去白底(安全,无跨库内存问题)
            for y in range(h):
                for x in range(w):
                    color = image.pixelColor(x, y)
                    if color.red() > 240 and color.green() > 240 and color.blue() > 240 and color.alpha() > 0:
                        image.setPixelColor(x, y, QColor(255, 255, 255, 0))

            self.original_pixmap = QPixmap.fromImage(image)
            self.update_display()
        except Exception as e:
            print(f"图片加载失败: {e}")
            self.setText("图片加载失败\n请检查 pet.png")
            self.setStyleSheet("color: white; background: black;")

    def update_display(self):
        if hasattr(self, 'original_pixmap'):
            new_w = int(self.base_size * self.current_scale)
            new_h = int(self.base_size * self.current_scale)
            scaled = self.original_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
            self.setPixmap(scaled)
            self.resize(scaled.size())

    def setup_timers(self):
        self.bubble_timer = QTimer(self)
        self.bubble_timer.timeout.connect(self.hide_bubble)

    # --- 交互逻辑 ---

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.dragging = True
            self.drag_start_pos = event.globalPos()
            self.offset = event.globalPos() - self.frameGeometry().topLeft()
            self.setCursor(Qt.ClosedHandCursor)
        elif event.button() == Qt.RightButton:
            self.show_context_menu(event.globalPos())

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

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.dragging = False
            self.setCursor(Qt.OpenHandCursor)
            # 【防连点】冷却中直接忽略点击
            if not self.animating and not self._click_cooling:
                dist = (event.globalPos() - self.drag_start_pos).manhattanLength()
                if dist < 10:
                    self.trigger_interaction()

    def wheelEvent(self, event):
        delta = event.angleDelta().y()
        if delta > 0:
            self.current_scale *= 1.1
        else:
            self.current_scale /= 1.1
        self.current_scale = max(0.3, min(self.current_scale, 3.0))
        self.update_display()

    def trigger_interaction(self):
        if self.animating or self._click_cooling:
            return
        self.animating = True
        self._click_cooling = True  # 进入冷却,防连点
        # 800ms 后解除冷却(比任何动画都长,确保安全)
        QTimer.singleShot(800, lambda: self._end_cooldown())

        action = random.choice(["jump", "squash", "shake"])
        self.show_dialog()

        if action == "jump":
            self.animate_jump()
        elif action == "squash":
            self.animate_squash()
        else:
            self.animate_shake()

    def _end_cooldown(self):
        """安全解除点击冷却"""
        if sip.isdeleted(self):
            return
        self._click_cooling = False

    def show_dialog(self):
        text = random.choice(DIALOGS)
        if hasattr(self, 'bubble_label'):
            self.bubble_label.close()

        # 使用独立顶层窗口,避免父窗口裁剪子控件
        self.bubble_label = QLabel(text)
        self.bubble_label.setWindowFlags(
            Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
        )
        self.bubble_label.setAttribute(Qt.WA_TranslucentBackground)
        self.bubble_label.setStyleSheet("""
            background-color: white; border: 2px solid #333; border-radius: 10px;
            padding: 5px 10px; color: black; font-size: 14px; font-weight: bold;
        """)
        self.bubble_label.adjustSize()
        # 气泡显示在宠物正上方(屏幕坐标)
        x = self.x() + (self.width() - self.bubble_label.width()) // 2
        y = self.y() - self.bubble_label.height() - 10
        self.bubble_label.move(x, y)
        self.bubble_label.show()
        self.bubble_timer.start(3000)

    def hide_bubble(self):
        if hasattr(self, 'bubble_label'):
            self.bubble_label.close()

    # --- 【修复】安全动画实现 ---

    def _safe_anim(self, target_prop, end_value, duration, easing=QEasingCurve.Linear):
        """创建并持有动画引用,防止GC回收导致崩溃"""
        anim = QPropertyAnimation(self, target_prop)
        anim.setDuration(duration)
        # 用 property() 获取当前值作为起始值,安全且兼容
        anim.setStartValue(self.property(target_prop))
        anim.setEndValue(end_value)
        anim.setEasingCurve(easing)
        self._animations.append(anim)
        anim.finished.connect(lambda a=anim: self._animations.remove(a) if a in self._animations else None)
        return anim

    def _run_anim_chain(self, anims, final_callback):
        """链式启动动画序列,最后一个动画结束后回调 final_callback"""
        if not anims:
            if final_callback:
                final_callback()
            return
        for i in range(len(anims) - 1):
            anims[i].finished.connect(anims[i + 1].start)
        anims[-1].finished.connect(lambda: self.reset_anim())
        anims[0].start()

    def animate_jump(self):
        self.setUpdatesEnabled(False)
        orig_y = self.y()
        up = self._safe_anim(b"pos", QPoint(self.x(), orig_y - 50), 150, QEasingCurve.OutQuad)
        down = self._safe_anim(b"pos", QPoint(self.x(), orig_y), 150, QEasingCurve.InQuad)
        self._run_anim_chain([up, down], self.reset_anim)

    def animate_squash(self):
        self.setUpdatesEnabled(False)
        orig_y = self.y()
        down = self._safe_anim(b"pos", QPoint(self.x(), orig_y + 20), 100)
        up = self._safe_anim(b"pos", QPoint(self.x(), orig_y - 10), 100)
        reset = self._safe_anim(b"pos", QPoint(self.x(), orig_y), 100)
        self._run_anim_chain([down, up, reset], self.reset_anim)

    def animate_shake(self):
        self.setUpdatesEnabled(False)
        orig_x = self.x()
        right = self._safe_anim(b"pos", QPoint(orig_x + 10, self.y()), 50)
        left = self._safe_anim(b"pos", QPoint(orig_x - 10, self.y()), 50)
        back = self._safe_anim(b"pos", QPoint(orig_x, self.y()), 50)
        self._run_anim_chain([right, left, back], self.reset_anim)

    def mouseDoubleClickEvent(self, event):
        """【修复】安全的双击退出 --- 不碰动画,避免 C++ 回调冲突"""
        if event.button() == Qt.LeftButton and not self._click_cooling:
            # 标记状态,阻止新交互
            self.animating = True
            # 显示告别语(独立窗口,不影响宠物窗口)
            self._show_goodbye()
            # 延迟退出,让事件队列自然处理
            QTimer.singleShot(500, QApplication.quit)

    def _show_goodbye(self):
        """用独立窗口显示告别文字,不修改宠物窗口本身的显示"""
        label = QLabel("👋 拜拜~")
        label.setWindowFlags(
            Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
        )
        label.setAttribute(Qt.WA_TranslucentBackground)
        label.setStyleSheet("""
            QLabel {
                background-color: rgba(0, 0, 0, 200);
                color: white;
                font-size: 28px;
                font-weight: bold;
                padding: 15px 20px;
                border-radius: 12px;
            }
        """)
        label.adjustSize()
        # 居中显示在宠物窗口位置
        label.move(
            self.x() + (self.width() - label.width()) // 2,
            self.y() - label.height() - 20
        )
        label.show()
        # 500ms 后随程序退出自动销毁

    def reset_anim(self):
        """【修复】增加防御性检查,防止动画结束后对象已失效"""
        # 如果窗口正在关闭或已被销毁,直接返回
        if not self.isVisible() or sip.isdeleted(self):
            return
        self.setUpdatesEnabled(True)
        self.animating = False

    # --- 右键菜单 ---

    def show_context_menu(self, pos):
        menu = QMenu(self)
        act_top = QAction("切换置顶", self)
        act_top.triggered.connect(self.toggle_topmost)
        act_quit = QAction("退出程序", self)
        act_quit.triggered.connect(QApplication.quit)
        menu.addAction(act_top)
        menu.addSeparator()
        menu.addAction(act_quit)
        menu.exec_(pos)

    def toggle_topmost(self):
        flags = self.windowFlags()
        if flags & Qt.WindowStaysOnTopHint:
            self.setWindowFlags(flags & ~Qt.WindowStaysOnTopHint)
        else:
            self.setWindowFlags(flags | Qt.WindowStaysOnTopHint)
        self.show()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    pet = DesktopPet()
    pet.show()
    sys.exit(app.exec_())

如何运行

确保安装了这几个库:

bash 复制代码
pip install PyQt5 pyinstaller

pyinstaller不安装也可以,如果需要打包成exe文件,再安装也不迟

确保有一个pet.png文件在代码的同级目录上

在PyCharm中这里的绿色三角号,点击即可运行;

如何打包成exe

我是执行了下述命令:

bash 复制代码
pyinstaller --onefile --noconsole  zhuochong.py

这里双击就可以运行了,可能需要等待几秒后,会有响应。

最后

毕竟只是临时起意做的demo,难免不够成熟,希望大家多多指正!

相关推荐
测功机之家1 小时前
国内口碑好的测功机公司找哪家
python·物联网
xlrqx1 小时前
2026年平顶山家电清洗培训设备是否齐全依据多因素来判断
大数据·python
summer_du2 小时前
Qdrant
人工智能·python
logomister设计公司阿燕2 小时前
华为商标“减法哲学”:极简主义如何成就全球品牌?
python·华为
萧青山2 小时前
2026国标合规:Agent Loop合规设计模式的6种循环架构(GB/Z 185.6+Python)
python·设计模式·agent loop·gb/z 185·国标合规
zzzzzz3102 小时前
我用 AI 编程半年,这 5 个 Prompt 技巧让我效率翻倍
python·机器学习·编程语言
Scott9999HH10 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
AI云海11 小时前
python 列表、元组、集合和字典
开发语言·python
二十雨辰12 小时前
[爬虫]-Urllib
爬虫·python