Home > Software engineering >  How to have button complete function on first press and revert on second press, and so on? [Tkinter]
How to have button complete function on first press and revert on second press, and so on? [Tkinter]

Time:08-06

I'm making a flashcard app and I want the button that turns the card be able to go back and fourth with every press. I have two canvases that represent the front and back side of the card.

This is the function that turns the card:

def turn_card():
    back_card = Canvas(width=800, height=526, bg=BACKGROUND_COLOR, highlightthickness=0)
    back_image = PhotoImage(file='images/card_back.png')
    back_card.create_image(400, 263, image=back_image)
    back_card.create_text(400, 263, text="French", fill='white', font=('Helvetica', 60, 'bold'))
    back_card.grid(column=0, row=0, columnspan=3)
    front_card.itemconfig(back_card)

I know that as the code is currently it will only turn to the back once, this is as far as I've gotten without breaking the code entirely. Even as it is now, I'm sure there is a better way to go about turning the card, especially since I get the error: _tkinter.TclError: invalid boolean operator in tag search expression It still effectively works, but if there is a better approach I would love to know.

Here is the rest of the code for context:

from tkinter import *

BACKGROUND_COLOR = '#B1DDC6'
TEXT_COLOR = '#34444D'

root = Tk()
root.title('tkFlash')
root.config(background=BACKGROUND_COLOR, padx=50, pady=50)


def turn_card():
    back_card = Canvas(width=800, height=526, bg=BACKGROUND_COLOR, highlightthickness=0)
    back_image = PhotoImage(file='images/card_back.png')
    back_card.create_image(400, 263, image=back_image)
    back_card.create_text(400, 263, text="French", fill='white', font=('Helvetica', 60, 'bold'))
    back_card.grid(column=0, row=0, columnspan=3)
    front_card.itemconfig(back_card)


def correct_answer():
    pass


def wrong_answer():
    pass


front_card = Canvas(width=800, height=526, bg=BACKGROUND_COLOR, highlightthickness=0)
card_image = PhotoImage(file='images/card_front.png')
front_card.create_image(400, 263, image=card_image)
front_card.create_text(400, 150, text="French", fill=TEXT_COLOR, font=('Helvetica', 40, 'italic'))
front_card.create_text(400, 263, text="Word", fill=TEXT_COLOR, font=('Helvetica', 60, 'bold'))
front_card.create_text(400, 430, text="Word", fill=BACKGROUND_COLOR, font=('Helvetica', 20, 'bold'))
front_card.grid(column=0, row=0, columnspan=3)

rbutton_image = PhotoImage(file='images/right.png')
wbutton_image = PhotoImage(file='images/wrong.png')
tbutton_image = PhotoImage(file='images/turn.png')

right_button = Button(image=rbutton_image, highlightbackground=BACKGROUND_COLOR)
right_button.grid(column=0, row=1)

wrong_button = Button(image=wbutton_image, highlightbackground=BACKGROUND_COLOR)
wrong_button.grid(column=2, row=1)

turn_button = Button(image=tbutton_image, bg=BACKGROUND_COLOR, highlightbackground=BACKGROUND_COLOR, command=turn_card)
turn_button.grid(column=1, row=1)


root.mainloop()

I've found a couple similar SO questions regarding this, but none of them are solving my particular issue. I've tried setting up an if-statement within the function to switch between the canvases based on a boolean value, but this did nothing. Any ideas on how to achieve this?

CodePudding user response:

First: you could create Canvas only once and later remove all items card.delete('all') and put new elements. And this would need to create PhotoImages only once (in main code).

Second: there is bug in PhotoImage which removes image when it is assigned to local variable in function - and you may see empty place instead of image. It would need to assign it to global variable or assign to other object - like `card.image = front_card. But this problem will not exist if you create images at start (in main code)

Third: it can be good to put root (parent/master) as first value in widgets because allow to nest widgets in other widgets (i.e in Frame to group elements and easy remove it) or put widgets in different windows.


You would use global boolean variable to control which side to display. ie. show_front = True. And using if show_front: ... else: ... and changing state show_front = not show_front you could run different code on every press button.


Minimal working code.

I removed most images and colors to show only important elements.

import tkinter as tk     # PEP8: `import *` is not preferred
#from PIL import ImageTk  # to load `jpg`

# --- constants ---   # PEP8: constants before classes

BACKGROUND_COLOR = '#B1DDC6'
TEXT_COLOR = '#34444D'

# --- classes ---     # PEP8: classes before functions

# empty

# --- functions ---   # PEP8: functions before main code 

def turn_card():
    global show_front  # inform function to assign new value to external/global variable instead of creating local variable
    
    card.delete('all')  # remove all items from canvas
    
    if show_front:
        card.create_image(400, 263, image=front_image)
        card.create_text(400, 150, text="French", fill=TEXT_COLOR, font=('Helvetica', 40, 'italic'))
        card.create_text(400, 263, text="Word", fill=TEXT_COLOR, font=('Helvetica', 60, 'bold'))
        card.create_text(400, 430, text="Word", fill=BACKGROUND_COLOR, font=('Helvetica', 20, 'bold'))
    else:
        card.create_image(400, 263, image=back_image)
        card.create_text(400, 263, text="French", fill='white', font=('Helvetica', 60, 'bold'))

    show_front = not show_front
    
# --- main ---

show_front = True  # default value at start

root = tk.Tk()

card = tk.Canvas(root, width=800, height=526, bg=BACKGROUND_COLOR, highlightthickness=0)
card.grid(column=0, row=0, columnspan=3)

front_image = PhotoImage(file='images/card_front.png')
back_image  = PhotoImage(file='images/card_back.png')
#front_image = ImageTk.PhotoImage(file='test/lenna/lenna.jpg')  # need `ImageTk` to load `jpg`
#back_image  = ImageTk.PhotoImage(file='test/lenna/lenna_flip.png')  # `ImageTk` can load also `png` and other formats

turn_card()  # show first time

turn_button = tk.Button(root, text='Turn', command=turn_card)
turn_button.grid(column=1, row=1)

root.mainloop()

enter image description here


PEP 8 -- Style Guide for Python Code

Image Lenna from Wikipedia

CodePudding user response:

Here is a slightly different approach to the problem. This creates both canvases ONCE then uses grid_forget() and grid(column = 0, row = 0, columnspan = 3) to flip Canvas and contents into view.

import os
import tkinter as tk

root = tk.Tk()
path = os.path.dirname(__file__)
images = [os.path.join(path, "images", "card_front.png"),
          os.path.join(path, "images", "card_back.png")]

def make_canvas(i, text):
    A = tk.PhotoImage(file = images[i])
    B = tk.Canvas(root, width = 800, height = 526, highlightthickness = 0)
    B.create_image(400, 263, image = A)
    B.create_text(
        400, 263, text = text, fill = 'white', font = 'Helvetica 60 bold')
    B.grid(column = 0, row = 0, columnspan = 3)
    return A, B

front_image, front_card = make_canvas(0, "Dressing")
back_image, back_card = make_canvas(1, "French")
C = 1

def turn_card():
    global C
    if C == 1:
        front_card.grid(column = 0, row = 0, columnspan = 3)
        back_card.grid_forget()
    else:
        back_card.grid(column = 0, row = 0, columnspan = 3)
        front_card.grid_forget()
    C = [2, 1][C==2]

turn_button = tk.Button(root, text = "Flip Canvas Image", command = turn_card)
turn_button.grid(column = 1, row = 1)
root.mainloop()
  • Related