prerequisites

This code is windows-specific. It will not run on linux (I’ve tried).

Make sure you have python and tkinter installed. Test if tkinter is installed with python -m tkinter. It should open a demo window. If not, you may have to run the python installer again and make sure you have the “install tkinter/IDLE” box checked.

imports

import tkinter as tk
import time

We will only need 2 imports. tkinter is python’s standard GUI toolkit, which we’ll use for drawing the window. This is not an in-depth tkinter tutorial, so refer to the tkinter documentation if you really want to understand what the code does.

desktop pet

we’ll create a class for our pet

class pet():
    def __init__(self):

In it’s constructor (__init__), we’ll set up the window and use a placeholder image.

        # create a window
        self.window = tk.Tk()

        # placeholder image
        img = tk.PhotoImage(file='placeholder.png')

        # set focushighlight to black when the window does not have focus
        self.window.config(highlightbackground='black')

        # make window frameless
        self.window.overrideredirect(True)

        # make window draw over all others
        self.window.attributes('-topmost', True)

        # turn black into transparency
        self.window.wm_attributes('-transparentcolor', 'black')

        # create a label as a container for our image
        self.label = tk.Label(self.window, bd=0, bg='black')

        # create a window of size 128x128 pixels, at coordinates 0,0
        self.window.geometry('128x128+0+0')

        # add the image to our label
        self.label.configure(image=img)

        # give window to geometry manager (so it will appear)
        self.label.pack()

        # run self.update() after 0ms when mainloop starts
        self.window.after(0, self.update)
        self.window.mainloop()

Then we’ll write a method that will be called continuously once the mainloop starts (it doesn’t have to be called update). Calling .after() on our window does not by itself cause the function to be called in a loop. To achieve this, we must have the method call itself the same way.

        def update(self):
            # add code here
            self.window.after(10, self.update)

Finally, we’ll call the constructor

pet()

If you run the code now, and put an image named “placeholder.png” in your folder (such as the stickman here), you should have your character appear in the top left of your screen.

img

Next let’s make the character move.

making the character move

We’ll want to keep track of the character’s x position and draw him there. Replace the call to self.window.geometry() with this:

        # create a window of size 128x128 pixels, at coordinates 0,0
        self.x = 0
        self.window.geometry('64x64+{x}+0'.format(x=str(self.x)))

…and fill in the update() method:

    def update(self):
        # move right by one pixel
        self.x += 1

        # create the window
        self.window.geometry('64x64+{x}+0'.format(x=str(self.x)))
        # add the image to our label
        self.label.configure(image=self.img)
        # give window to geometry manager (so it will appear)
        self.label.pack()

        # call update after 10ms
        self.window.after(10, self.update)

To move the character, we call self.window.geometry() and fill in our new x position. We will again have to call self.label.pack().

If you run the code now, the character should move to the right.

He’s not animated yet, so let’s do that next.

animating the character

You can use this terrible walking animation I made if you don’t have one yet.

img

Replace the call to tk.PhotoImage() in the constructor with the gif:

        # placeholder image
        self.walking_right = [tk.PhotoImage(file='walking_right.gif', format='gif -index %i' % (i)) for i in range(4)]

This line uses a list comprehension to create a list, where every element is the result of calling tk.PhotoImage(), with a different index to select each frame of the gif. If your gif has a different amount of frames, you’ll need change the range() call.

We’ll want to keep track of the index of the current frame and assign the frame to a variable. We’ll also want to keep track of how much time has passed since the frame has advanced.

        self.frame_index = 0
        self.img = self.walking_right[self.frame_index]

        # timestamp to check whether to advance frame
        self.timestamp = time.time()

Lastly, replace the whole update method with this one:

    def update(self):
        # move right by one pixel
        self.x += 1

        # advance frame if 50ms have passed
        if time.time() > self.timestamp + 0.05:
            self.timestamp = time.time()
            # advance the frame by one, wrap back to 0 at the end
            self.frame_index = (self.frame_index + 1) % len(self.walking_right)
            self.img = self.walking_right[self.frame_index]

        # create the window
        self.window.geometry('64x64+{x}+0'.format(x=str(self.x)))
        # add the image to our label
        self.label.configure(image=self.img)
        # give window to geometry manager (so it will appear)
        self.label.pack()

        # call update again after 10ms
        self.window.after(10, self.update)

Here we check if 50ms have passed since we last advanced the frame and only advance it then. To keep our frame index within the bounds of the amount of frames we have, we use the modulo (%) operator to get the remainder of an integer division by the number of frames (the length of our array that contains the frames).

Our character should now move right and be animated! From here, it’s up to you to add more logic. Here are some ideas:

  • also moving left
  • states, such as idling, jumping, etc.
  • switching between states at random
  • interaction with the mouse cursor

the final code

import tkinter as tk
import time
import random

class pet():
    def __init__(self):
        # create a window
        self.window = tk.Tk()

        # placeholder image
        self.walking_right = [tk.PhotoImage(file='walking_right.gif', format='gif -index %i' % (i)) for i in range(4)]
        self.frame_index = 0
        self.img = self.walking_right[self.frame_index]

        # timestamp to check whether to advance frame
        self.timestamp = time.time()

        # set focushighlight to black when the window does not have focus
        self.window.config(highlightbackground='black')

        # make window frameless
        self.window.overrideredirect(True)

        # make window draw over all others
        self.window.attributes('-topmost', True)

        # turn black into transparency
        self.window.wm_attributes('-transparentcolor', 'black')

        # create a label as a container for our image
        self.label = tk.Label(self.window, bd=0, bg='black')

        # create a window of size 128x128 pixels, at coordinates 0,0
        self.x = 0
        self.window.geometry('64x64+{x}+0'.format(x=str(self.x)))

        # add the image to our label
        self.label.configure(image=self.img)

        # give window to geometry manager (so it will appear)
        self.label.pack()

        # run self.update() after 0ms when mainloop starts
        self.window.after(0, self.update)
        self.window.mainloop()

    def update(self):
        # move right by one pixel
        self.x += 1

        # advance frame if 50ms have passed
        if time.time() > self.timestamp + 0.05:
            self.timestamp = time.time()
            # advance the frame by one, wrap back to 0 at the end
            self.frame_index = (self.frame_index + 1) % 4
            self.img = self.walking_right[self.frame_index]

        # create the window
        self.window.geometry('64x64+{x}+0'.format(x=str(self.x)))
        # add the image to our label
        self.label.configure(image=self.img)
        # give window to geometry manager (so it will appear)
        self.label.pack()

        # call update after 10ms
        self.window.after(10, self.update)

pet()