PAYATHON 2026

为什么 tkinter 中 State 变量已更新,宠物图像却没有切换?

排障赵师傅

State 更新后,PetFrames 中仍是初始化时加载的 "Sad" 图片。状态变化时,需要加载对应的图片帧,再通过 canvas.itemconfigure(..., image=...) 更新画布中的图像。

为什么输出是 Idle,画面却还是 Sad?

初始化时:

State = 2

创建 PetFrames 时,PetStates[State] 的值是 "Sad",因此加载的是这两个文件:

PetAssets/Pet1Sad1.png
PetAssets/Pet1Sad2.png

这些路径表达式只在创建列表时计算一次。之后执行:

MyFrame.State = 0

只会改变 State,不会重新加载图片,也不会替换 PetFrames 中的 PhotoImage 对象。

下面两处代码读取的是不同的数据:

# 根据当前 State 输出状态名称
print(MyFrame.PetStates[MyFrame.State])

# 从原来的图片列表中取出图片
canvas.itemconfig(image_id, image=self.PetFrames[self.frame])

前者读取当前状态,可以输出 "Idle";后者使用原来的图片列表,仍在两张 "Sad" 图片之间循环。

应该如何修改?

将宠物状态和图片帧保存为实例属性,每次动画回调按以下顺序执行:

  1. 根据当前属性计算状态。
  2. 如果状态发生变化,重新加载对应图片,并重置帧索引。
  3. 更新画布,再用 after() 安排下一次动画。

示例保留了状态判断和图片切换,省略了按钮、属性标签、随机变脏和属性随时间衰减的逻辑。每个用到的状态都需要有编号为 1、2 的两张 PNG 图片。

import tkinter as tk
from tkinter import ttk


class MyFrame(ttk.Frame):
    PetStates = [
        "Idle", "Happy", "Sad", "Dirty",
        "Sleep", "Eat", "Tired", "Deny",
    ]

    def __init__(self, parent):
        super().__init__(parent, padding=10)
        self.pack()

        self.State = 2
        self.Pet = 1
        self.frame = 0
        self.fps = 750

        self.Happiness = 21
        self.Fullness = 21
        self.Energy = 21

        self.PetFrames = []
        self.loaded_state = None

        self.canvas = tk.Canvas(self, width=300, height=160)
        self.canvas.pack()
        self.image_id = self.canvas.create_image(150, 90)

        self.animation()

    def check_state(self):
        if self.Happiness < 5 or self.Fullness < 5:
            self.State = 2
            self.fps = 1000
        elif self.Energy < 5:
            self.State = 6
            self.fps = 1000
        elif self.State not in (3, 4):
            self.State = 0
            self.fps = 750

    def load_frames(self):
        state_name = self.PetStates[self.State]

        # 保留 PhotoImage 引用,供后续动画使用。
        self.PetFrames = [
            tk.PhotoImage(
                master=self,
                file=f"PetAssets/Pet{self.Pet}{state_name}{number}.png",
            ).zoom(4, 4)
            for number in (1, 2)
        ]

        self.loaded_state = self.State
        self.frame = 0

    def animation(self):
        self.check_state()

        if self.State != self.loaded_state:
            self.load_frames()

        self.canvas.itemconfigure(
            self.image_id,
            image=self.PetFrames[self.frame],
        )
        self.frame = (self.frame + 1) % len(self.PetFrames)

        print(self.PetStates[self.State], self.Happiness)
        self.after(self.fps, self.animation)


if __name__ == "__main__":
    root = tk.Tk()
    root.title("Tamagotchi Pet Game")
    MyFrame(root)
    root.mainloop()

初始属性都是 21,所以第一次调用 check_state() 时,self.State 就会变为 0,随后加载 "Idle" 对应的两张图片。

图片只在状态切换时读取。如果切换频繁,可以按状态缓存图片,避免反复读取相同文件。

原代码中还需要修正哪些地方?

将比较运算改为赋值

原来的这一行:

self.State == 3

执行的是比较,不会改变状态。应改为:

self.State = 3

统一使用实例属性

原代码混用了这两种写法:

MyFrame.State = 0
self.State = 3

MyFrame.State 是类属性。执行 self.State = 3 后,实例会有自己的同名属性。此后再修改类属性,读取 self.State 时仍可能得到实例保存的值。

当前宠物的状态、帧索引和属性值适合统一保存在 self.State、self.frame 等实例属性中,打印时也读取同一处数据:

print(self.PetStates[self.State])

确保方法签名和回调一致

如果 animation、check_state 和 run_all 都是定义在类中的普通实例方法,就需要接收 self,并通过实例调用:

def animation(self):
    ...
    self.after(self.fps, self.animation)

传给 after() 的应是回调 self.animation。写成 self.animation() 会立即调用方法,无法按预期安排下一次执行。

原片段缺少缩进,也省略了部分代码,无法确认这些函数是否嵌套在 __init__() 内。如果是内部函数,可以直接使用外层的 self;示例采用普通实例方法,作用域更清楚。

图片切换时的注意事项

  • PhotoImage 需要保留 Python 引用。示例用 self.PetFrames 保存当前图片,避免图片对象被回收后画布显示为空。
  • PetAssets/... 是相对于程序当前工作目录的路径。启动目录不同,可能导致图片找不到。
  • 使用 after() 更新时,外部修改的状态会在下一次动画回调中显示。修改 fps 也不会改变已经安排好的那一次回调。
  • check_state() 必须实际执行。原片段中的 run_all() 没有直接调用它;如果省略的代码也没有调用,状态判断就不会运行。示例在每次绘制前调用它,按钮和其他逻辑设置状态时,也需要考虑这些判断规则。

备注:内容仅供参考。