1

From what I've read, ImageTk should convert a Pillow image into a format that Tkinter can display. (Indeed, when I don't, Tkinter fails with an error.) However, when I run this program on my Windows computer, the Tkinter box says it's not responding and does not display the images.

from PIL import Image, ImageDraw

class Block:
    def __init__(self, x=30, y=10, speed=None, size=2.5):
        self.x = x
        self.y = y
        self.size = size # radius

        if speed is None:
            speed = self.size * 2

        self.speed = speed

    def draw(self, image):
        width, height = image.size
        drawer = ImageDraw.Draw(image)
        drawer.rectangle(((self.x - self.size, height - (self.y - self.size)), (self.x + self.size, height - (self.y + self.size))), fill="white")


class BallDodge():
    def __init__(self):
        self.width = 400
        self.height = 300
        self.main_player = Block(self.width // 2)
        self.balls = []
        self.frame = 0
        self.frames_between_balls = 20
        self.frames_since_last_ball = self.frames_between_balls
        self.input_function = self.get_human_input
        self.states = []


    @property
    def state(self):
        ans = Image.new('RGBA', (self.width, self.height), "black")
        self.main_player.draw(ans)

        return ans

    def get_human_input(self, *args, **kwargs):
        try:
            ans = int(input("1, 2, 3, or 4:"))
            if ans > 4:
                raise ValueError("Can't be greater than 4")
            elif ans < 1:
                raise ValueError("Can't be less than 1")
            if ans == 1:
                return [1, 0]
            elif ans == 2:
                return [0, 0]
            elif ans == 3:
                return [0, 1]
            elif ans == 4:
                return [1, 1]
            else:
                raise ValueError("Somehow it's not one of the specified numbers. You are magical.")
        except Exception as e:
            print("Invalid input")
            print(e)
            return self.get_human_input()

    def step(self):
        inpt = self.input_function(self.state)
        directions = [-1, 1]
        for i in range(2):
            self.main_player.x += directions[i] * inpt[i]

        self.states.append(self.state)

if __name__ == "__main__":
    game = BallDodge()
    import tkinter as tk
    from PIL import ImageTk, Image
    root = tk.Tk()
    my_image = ImageTk.PhotoImage(game.state)
    game.state.show()
    while True:
        panel = tk.Label(image = ImageTk.PhotoImage(game.state), master = root)
        panel.pack()
        # panel = Label(root, image = ImageTk.PhotoImage(game.state))
        # panel.pack(side = "bottom", fill = "both", expand = "yes")
        root.update_idletasks()
        root.update()
        game.step()

Things I've noticed: If I do game.state.show() it will open up the image just fine, so there's definitely an image there. If I do hit one of the number keys to advance to the next frame, the Tkinter window gets twice as tall, but still displays nothing.

How can I get a loop such that I can display a Pillow image in a Tkinter window, get user input, and then display a new image?

Pro Q
  • 4,391
  • 4
  • 43
  • 92
  • 2
    The window gets taller and taller because you keep `pack`ing_new_ `tk.Labels` into it. You already know how to get Pillow images into tkinter dynamically—i.e. using `ImageTk.PhotoImage()`. What you don't know is how to display them and then later update them. To change the image used on the label, you could use something along the lines of `panel.config(image=new_imagetk)`. – martineau Jun 21 '18 at 03:32
  • But the Tkinter window isn't even showing the first image I put into it. It's good to know about the config function to change the image, but even if I move the panel lines outside of the loop, my Tkinter window is just blank. (Which makes me think I don't really know how to get Pillow images into tkinter, much less dynamically.) – Pro Q Jun 21 '18 at 14:20
  • Another big issue is you're not programming `tkinter` correctly. Something like your `while True:` loop is **not** how they work. In a nutshell, you need always call `.mainloop()` somewhere to get your app running. From that point on, everything must occur inside that loop. Basically what you want to do is animation, so that's what you need to understand how to do (there are many questions related to that here on stackoverflow). This is often accomplished using a callback function with the universal widget method [`after()`](http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html). – martineau Jun 21 '18 at 15:15
  • I worry about having all of my logic inside a tkinter function because I just want to use tkinter to display a game, not to run it. I see how it's possible to refactor it though so it does use the mainloop without coupling the game to tkinter. Thank you for your responses! The loop I'm using is from [this question](https://stackoverflow.com/a/29158947/5049813), and it seems to work for me for the time being, so that's what I'll use for now. – Pro Q Jun 21 '18 at 15:24

1 Answers1

0

Using the following loop worked for me. (It's messy and could be improved, but it works.)

if __name__ == "__main__":
    import tkinter as tk
    from PIL import ImageTk, Image

    game = BallDodge()
    root = tk.Tk()
    pilImage = game.state
    tkimage = ImageTk.PhotoImage(pilImage)
    width, height = pilImage.size
    root.geometry('{}x{}'.format(width+1, height+1))
    canvas = tk.Canvas(root, width=width, height=height, bg="blue")
    canvas.pack()
    imagesprite = canvas.create_image(0, 0, anchor=tk.NW, image=tkimage)
    while True:
        tkimage =  ImageTk.PhotoImage(game.state)
        imagesprite = canvas.create_image(0, 0, anchor=tk.NW, image=tkimage)
        root.update_idletasks()
        root.update()
        game.step()
Pro Q
  • 4,391
  • 4
  • 43
  • 92
  • 1
    I suggest you read the entire [linked answer](https://stackoverflow.com/questions/29158220/tkinter-understanding-mainloop/29158947#29158947).Where one of the comments by @Bryan Oakley—a tkinter expert—says "There is never a reason to call both `update_idletasks` and `update` at the same time". Another thing is that further down, it does talk about using `after()` as I suggested, which shouldn't require putting "all of my logic inside a tkinter function" because the `after()` callback function can itself do whatever it wants, including making calls all the non-tkinter functions it needs to. – martineau Jun 21 '18 at 16:11