I'm working on a GUI Python program using Tkinter.
I have a function that is called when a button is pressed (and when the program is loaded). The program is currently unfinished and only checks data validation at this current point. As the default entry is current invalid, it throws an error.
However, after this point, the entry box is disabled and will not let me enter any data. I cannot figure out why this is happening and I was wondering if someone could tell me the reason so I can work on a solution.
Thanks
import sys
import random
from tkinter import *
from tkinter import ttk
from tkinter import messagebox
root = Tk()
root.title("COSC110 - Guessing Game")
hint = StringVar()
guesses = []
guess_input = ''
def loadWordList(filename): #Load the words from a file into a list given a filename.
file = open(filename, 'r')
line = file.read().lower()
wordlist = line.split()
return wordlist
word = random.choice(loadWordList('words.txt'))
def getHint(word, guesses): #Get hint function, calculates and returns the current hint.
hint = ' '
for letter in word:
if letter not in guesses:
hint += '_ '
else:
hint += letter
return hint
def guessButton(guess, word, guesses):
guess = str(guess_input)
guess = guess.lower()
if not guess.isalpha():
is_valid = False
elif len(guess) !=1:
is_valid = False
else:
is_valid = True
while is_valid == False:
messagebox.showinfo("Error:","Invalid input. Please enter a letter from a-z.")
break
hint.set(getHint(word, guesses))
return hint
label_instruct = Label(root, text="Please enter your guess: ")
label_instruct.grid(row=1,column=1,padx=5,pady=10)
guess_input = Entry(root,textvariable=guess_input)
guess_input.grid(row=1, column=2)
guess_button = Button(root, text="Guess", width=15, command=guessButton(guess_input,word,guesses))
guess_button.grid(row=1, column=3,padx=15)
current_hint = Label(root, textvariable=hint)
current_hint.grid(column=2,row=2)
label_hint = Label(root, text="Current hint:")
label_hint.grid(column=1,row=2)
label_remaining = Label(root, text="Remaining guesses: ")
label_remaining.grid(column=1,row=3)
root.mainloop() # the window is now displayed
Any tips are appreciated.
There are two apparent problems.
Firstly, you shouldn't use
guess_button = Button(root, text="Guess", width=15, command=guessButton(guess_input,word,guesses))
because you can't call a function with arguments on the command config.
My suggestion would be to take a look here and use one of the proposed methods, I particularly like the one using functools and partial:
from functools import partial
#(...)
button = Tk.Button(master=frame, text='press', command=partial(action, arg))
with action being the function you want to call and arg the parameters you want to call separated by a comma.
Secondly, you are using
guess = str(guess_input)
which doesn't return the Entry typed text, use instead
guess = guess_input.get()
PS: Albeit not directly related to your question, you should use
if var is False:
instead of
if var == False:
Related
I am building a rudimentary python program for tracking bullet fire in my local Rogue Trader campaign. I hate writing - erasing - rewriting on my sheet leaving it smudged and gross. This gives me an excuse to practice my coding skills. Eventually going to have it save values to a file and then read them upon start up, but that's in the future.
I am letting it ask for what guns I have, setting a clipSize for said gun, and then create a button that references each gun. Upon pressing the button, fireGun is supposed to take the value of the gun shot corresponding to which button is pressed. However, the way it runs currently, all guns fire from the same ammo amount which is the last 'clipSize' entered.
I need each button to track its own variable to update the correct dictionary reference upon fireGun.
from tkinter import *
addGuns = 'true'
gunList = {}
while (addGuns == 'true'):
newGun = input("What is the name of your gun? ")
clipSize = int(input("What is its clip size? "))
gunList[newGun] = clipSize
gunCheck = input("Done adding guns? ")
if (gunCheck == 'yes'):
addGuns = 'false'
root = Tk()
root.title("Pew Pew")
def fireGun(x):
startingAmmo = gunList[x]
endingAmmo = startingAmmo - 1
gunList[x] = endingAmmo
print(gunList[x])
return
for gun in gunList:
button = Button(root, text = gun, command = lambda name = gun:fireGun(gun))
button.pack()
root.mainloop()
Use partial to send parameters to a function with a command= call. You could also use an Entry to get the gun info, and one label for each gun with the name and updated ammo amount on each label. A very sloppy example (I have to go to work).
from tkinter import *
from functools import partial
gunCheck="no"
gunList = {}
while gunCheck != 'yes':
newGun = input("What is the name of your gun? ")
clipSize = int(input("What is its clip size? "))
gunList[newGun] = int(clipSize)
gunCheck = input("----->Done adding guns? ")
## if (gunCheck == 'yes'):
## addGuns = 'false'
root = Tk()
root.title("Pew Pew")
def fireGun(x):
startingAmmo = gunList[x]
endingAmmo = startingAmmo - 1
gunList[x] = endingAmmo
print(gunList[x])
label_list[x].config(text=x + "-->" + str(endingAmmo))
return
label_list={}
fr=Frame(root)
fr.pack(side="top")
for gun in gunList:
lab=Label(fr, text="%s --> %d" %(gun, gunList[gun]))
lab.pack(side=TOP)
label_list[gun]=lab
button = Button(root, text = gun, command = partial(fireGun, gun))
button.pack()
root.mainloop()
I have a script that continually takes in text and outputs text (its a text based game)
I would like to run it through a tkinter GUI as opposed to the console
Python : Converting CLI to GUI
This question perfectly answers how to convert "print" into a GUI insert.
The problem is that my game obviously runs through a ton of loops, and that screws up the "app.mainloop()" because it either never runs (and then the GUI never shows up) or you run it first, and it doesn't let anything else run.
I suppose I could try and and stagger these loops somehow, but that seems very hackish. I could also try to modify my entire codebase to run inside the app.mainloop(), but what I really think I need is multiple threads. Problem is, I have no idea how to make that work.
There are a few other questions, but they either don't work or don't make much sense:
Tkinter with multiple threads
Run process with realtime output to a Tkinter GUI
Thanks.
Edit: extremely simplified code:
def moveNorth():
print('You have moved north')
def interpreter(command):
if command == 'go north':
moveNorth()
else:
print('invalid command')
def listener():
playerchoice = sys.stdin.readline().strip()
return playerchoice
if __name__ == '__main__':
print('Welcome')
while playing:
interpreter(listener())
I think you might be making it more complicated than it needs to be.
For Tkinter at least it is very simple change console interactions into a GUI interaction instead.
The simplest example I can give is to use an Entry field for user input and a Text widget for the output.
Here is a simple example of a console based game being moved to a GUI using Tkinter.
Console number guessing game:
import random
print("simple game")
print("-----------")
random_num = random.randint(1, 5)
print(random_num)
x = True
while x == True:
#Input for user guesses.
guess = input("Guess a number between 1 and 5: ")
if guess == str(random_num):
#Print answer to console.
print("You win!")
x = False
else:
print("Try again!")
Here is the Tkinter GUI example of the same game:
import tkinter as tk
import random
root = tk.Tk()
entry_label = tk.Label(root, text = "Guess a number between 1 and 5: ")
entry_label.grid(row = 0, column = 0)
#Entry field for user guesses.
user_entry = tk.Entry(root)
user_entry.grid(row = 0, column = 1)
text_box = tk.Text(root, width = 25, height = 2)
text_box.grid(row = 1, column = 0, columnspan = 2)
text_box.insert("end-1c", "simple guessing game!")
random_num = random.randint(1, 5)
def guess_number(event = None):
#Get the string of the user_entry widget
guess = user_entry.get()
if guess == str(random_num):
text_box.delete(1.0, "end-1c") # Clears the text box of data
text_box.insert("end-1c", "You win!") # adds text to text box
else:
text_box.delete(1.0, "end-1c")
text_box.insert("end-1c", "Try again!")
user_entry.delete(0, "end")
# binds the enter widget to the guess_number function
# while the focus/cursor is on the user_entry widget
user_entry.bind("<Return>", guess_number)
root.mainloop()
As you can see there is a bit more code for the GUI but most of that is the GUI design.
The main part that you need to change is the use of entry vs input for your answers and the use of insert vs print for your response. The rest is really just design stuff.
If you want to keep the questions on a continuous nature you can update the label with a new question or you could even use tkinters askstring function for each new question. There are many options.
the main thing is getting the value of the user answer, using that answer to test with the question, then printing the results to the text box.
This is a simple math game. The function of interest is checkAnswer(). I am trying to update label1 so that the label updates with the new str instead of continuously printing multiple labels.
Error:
AttributeError: 'str' object has no attribute 'set'
from tkinter import *
from random import randint
num1 = 0
num2 = 0
userAnswer = 0
answer = 0
score = 0
labeltext = ""
#PROGRAM FUNCTIONS
def question():
global num1, num2
global answer
num1 = randint(1,10)
num2 = randint(1,10)
question = Label(text = "What is " + str(num1)+ " + " + str(num2) + "?").pack()
answer = num1+num2
print(answer) #testing purposes
def userAnswer():
global userAnswer
userAnswer = IntVar()
entry = Entry(root, textvariable = userAnswer).pack()
submit = Button(root, text = "submit", command = checkAnswer).pack()
def checkAnswer():
global labeltext
print(userAnswer.get())
if userAnswer == answer:
labeltext.set("good job")
score += 1
elif userAnswer != answer:
labeltext.set("oh no")
labeltext = StringVar()
label1 = Label(root, textvariable = labeltext).pack()
#INTERFACE CODE
root = Tk()
question()
userAnswer()
root.mainloop()
You are getting that AttributeError because you initially bind a string "" to the global name labeltext, and a Python string doesn't have a .set method. (That wouldn't make sense because Python strings are immutable). You eventually bind a Tkinter StringVar to labeltext, and StringVars do have a .set method, but your code does that after it's already tried to call .set on the plain Python string.
A similar problem will occur with the IntVar you named userAnswer. That has an additional problem: its name clashes with one of your functions. You can't do that!
Here's a repaired version of your code, with a few other minor changes. There's no need to use the global directive on those StringVars or the IntVar since you are simply calling methods of those objects. You only need global if you need to perform an assignment on a global object, merely accessing the existing value of a global or calling one of its methods doesn't need the global directive.
from tkinter import *
from random import randint
#PROGRAM FUNCTIONS
def question():
global true_answer
num1 = randint(1,10)
num2 = randint(1,10)
Label(text="What is " + str(num1)+ " + " + str(num2) + "?").pack()
true_answer = num1 + num2
print(true_answer) #testing purposes
def answer():
Entry(root, textvariable=userAnswer).pack()
Button(root, text="submit", command=checkAnswer).pack()
def checkAnswer():
global score
print(userAnswer.get()) #testing purposes
if userAnswer.get() == true_answer:
labeltext.set("good job")
score += 1
else:
labeltext.set("oh no")
label1 = Label(root, textvariable=labeltext).pack()
#INTERFACE CODE
root = Tk()
true_answer = 0
score = 0
userAnswer = IntVar()
labeltext = StringVar()
question()
answer()
root.mainloop()
However, that code still has several problems. It can only ask a single question. And each time you hit the "submit" button it adds a new Label widget, which I don't think you really want.
It's not a good idea to use global variables. They break modularity, which makes the code harder to understand, and harder to modify and re-use.
Here's an enhanced version of your program which puts everything into a class, so we can use instance attributes instead of globals.
This version asks multiple questions. It doesn't have a "submit" button, instead the question is automatically submitted when the user hits the Enter / Return key, either on the main keyboard or the numeric keypad.
import tkinter as tk
from random import randint
class Quiz(object):
def __init__(self):
root = tk.Tk()
# The question
self.question_var = tk.StringVar()
tk.Label(root, textvariable=self.question_var).pack()
# The answer
self.user_answer_var = tk.StringVar()
entry = tk.Entry(root, textvariable=self.user_answer_var)
entry.pack()
# Check the answer when the user hits the Enter key,
# either on the main keyboard or the numeric KeyPad
entry.bind("<Return>", self.check_answer)
entry.bind("<KP_Enter>", self.check_answer)
self.true_answer = None
# The response
self.response_var = tk.StringVar()
self.score = 0
tk.Label(root, textvariable=self.response_var).pack()
# Ask the first question
self.ask_question()
root.mainloop()
def ask_question(self):
num1 = randint(1, 10)
num2 = randint(1, 10)
self.question_var.set("What is {} + {}?".format(num1, num2))
self.true_answer = num1 + num2
#print(self.true_answer) #testing purposes
def check_answer(self, event):
user_answer = self.user_answer_var.get()
#print(user_answer) #testing purposes
if int(user_answer) == self.true_answer:
text = "Good job"
self.score += 1
else:
text = "Oh no"
self.response_var.set('{} Score={}'.format(text, self.score))
# Clear the old answer and ask the next question
self.user_answer_var.set('')
self.ask_question()
Quiz()
Please note the import tkinter as tk statement. It's much better to use this form than from tkinter import * since that "star" import dumps 130 names into your namespace, which is messy, and can lead to name collisions, especially if you do star imports with other modules. The import tkinter as tk form requires you to do a little more typing, but it also makes the code much easier to read, since it's obvious which names are coming from Tkinter.
I've also changed the names of the variables and the class methods (functions) so they conform to the Python PEP-0008 style guide.
There are various further enhancements that could be made. In particular, this code doesn't gracefully handle user input that isn't a valid integer.
This is my first app ever. It is working well but I would like to separate the UI concerns like getting input and creating labels, from the translation logic. I would then like to remove the output from the previous translation, i.e., only showing one translation on the screen at a time.
How can I separate the translation logic from my Tkinter GUI?
from Tkinter import *
import tkMessageBox
def start():
inputg = input.get()
if len(inputg) >= 2 and inputg.isalpha():
new_word_out = Label(text=(inputg[1:] + (inputg[0] + "ay")).lower().title()).pack()
out_message = Label(text="Cool! Try another!").pack()
# restart()
elif len(inputg) <= 1 and inputg.isalpha():
show_error(message="Whoops! I need 2 or more characters to translate! Try again!")
return
elif len(inputg) >= 1 and not inputg.isalpha():
show_error(message="Whoops! No numbers or symbols please! Try again!")
return
elif len(inputg) == 0:
show_error(message="It seems you haven't given me anything to translate!")
return
def show_error(message):
tkMessageBox.showerror(title="Error", message=message)
return
def quit():
ask_exit = tkMessageBox.askyesno(title="Quit", message="Are you sure you want to quit?")
if ask_exit > 0:
root.destroy()
return
root = Tk()
input = StringVar() # stores user input into this variable as a string.
root.title("The Pig Translator")
root.protocol("WM_DELETE_WINDOW", quit)
labeltitle1 = Label(text="Hello there! This is my Pig Latin Translator!").pack()
labeltitle2 = Label(text="Please enter a word to continue!", fg='darkgreen', bg='grey').pack()
original_entry = Entry(textvariable=input, bd=5, fg='darkgreen').pack()
translate_button = Button(text="Translate", command=start).pack()
root.bind('<Return>', lambda event: start()) # essentially binds 'Return' keyboard event to translate_button
root.mainloop()
There are many ways you can separate logic from GUI. generally I would recommend using classes and callback functions. Thus, I made a class that generates the gui. However, the translation is performed by external function called do_translation.
MyFrame does not know much about how do_translation. It only knows it returns translated_str, message and takes string as argument. do_translation does not relay on any gui as well. The do_translation takes only an input string, does what it wants, and returns translated string and message. The MyFrame take this function as a callback. You can make any other translation function, and as long as the input and output are same, it will work.
I rely here on a "Cool" in a massage which indicates that translation was ok. Its poor idea to make it relay on 'Cool' word, but did not want to change your code too much. Probably better to raise some error, or use message codes, etc.
from Tkinter import *
import tkMessageBox
class MyFrame(Frame):
def __init__(self, master, input_callback=None, **kwargs):
Frame.__init__(self, master)
self.set_input_callback(input_callback)
self.create_widgets()
self.pack()
def create_widgets(self):
self.input = StringVar() # stores user input into this variable as a string.
self.labeltitle1 = Label(text="Hello there! This is my Pig Latin Translator!")
self.labeltitle1.pack()
self.labeltitle2 = Label(text="Please enter a word to continue!", fg='darkgreen', bg='grey')
self.labeltitle2.pack()
self.original_entry = Entry(textvariable=self.input, bd=5, fg='darkgreen')
self.original_entry.pack()
self.translate_button = Button(text="Translate", command=self.start)
self.translate_button.pack()
self.new_word_out = Label(text='')
self.out_message = Label(text='')
def set_input_callback(self, some_fun):
self.input_callback = some_fun
def show_error(self, message):
tkMessageBox.showerror(title="Error", message=message)
return
def start(self):
inputg = self.input.get()
if self.input_callback:
translated_str, message = self.input_callback(inputg)
if 'Cool' in message:
self.new_word_out['text'] = translated_str
self.new_word_out.pack()
self.out_message['text'] = message
self.out_message.pack()
else:
self.show_error(message)
def do_translation(inputg):
translated_str = message = ''
if len(inputg) >= 2 and inputg.isalpha():
translated_str = (inputg[1:] + (inputg[0] + "ay")).lower()
message = "Cool! Try another!"
elif len(inputg) <= 1 and inputg.isalpha():
message = "Whoops! I need 2 or more characters to translate! Try again!"
elif len(inputg) >= 1 and not inputg.isalpha():
message = "Whoops! No numbers or symbols please! Try again!"
elif len(inputg) == 0:
message = "It seems you haven't given me anything to translate!"
return translated_str, message
def quit():
ask_exit = tkMessageBox.askyesno(title="Quit", message="Are you sure you want to quit?")
if ask_exit > 0:
root.destroy()
return
root = Tk()
root.title("The Pig Translator")
root.protocol("WM_DELETE_WINDOW", quit)
mf = MyFrame(root)
mf.set_input_callback(do_translation)
root.bind('<Return>', lambda event: start()) # essentially binds 'Return' keyboard event to translate_button
root.mainloop()
Hopefully this will be useful. I know, that there is not too much explanation what is happening here, but, don't have much time to write it. Your problem is very general.
Trying to get used to the tkinter gui but I'm running into a problem with setting up an input box. I wanted to make a simple number guessing program using Entry to input an integer and a button to submit the guess. I'm getting an str to int conversion error when I use int(GuessBox.get()) and I'm not sure what to do.
ValueError: invalid literal for int() with base 10: ''
from tkinter import *
import random
def makeAGuess():
guess = int(GuessBox.get())
print(guess)
if guess == Answer:
print("you got it!")
return False
elif guess > Answer:
print("Too High, try again")
return True
else :
print("Too low, try again")
return True
Answer = random.randint(1, 100)
main = Tk()
label = Label(main, text = "Guess a number")
label.pack()
GuessBox = Entry(master = main)
GuessBox.pack()
submitGuess = Button(master = main, text = "Submit Guess", command = makeAGuess())
submitGuess.pack()
main.mainloop()
You need to pass the function as an object, don't call it.
submitGuess = Button( master = main, text = "Submit Guess", command = makeAGuess )
Otherwise makeAGuess is called when the Button is created, but not passed any arguments.
With this change your code works perfectly for me.