I am trying to figure out a way to prompt a user for input inside of a frame I created on a tkinter window.
This is the code I'm currently working with:
--------- Main.py ----------
from tkinter import *
import customtkinter
import QuizFunctions
import DatabaseFunctions
customtkinter.set_appearance_mode("dark")
customtkinter.set_default_color_theme("dark-blue")
# Globally tracked variables for user progress.
question_tally = 0
correct_answer_tally = 0
# Root window setup
root = customtkinter.CTk()
root.geometry(f"{800}x{800}")
root.title("Roadmap.sh Study Utility")
# Create two frames. One center frame for user to interface with, the other for buttons / options to run quizzes.
center_frame= Frame(master=root, width=300, height=300, bg='grey')
center_frame.place(relx = 0.5, rely = 0.5, anchor= CENTER)
button_frame = Frame(master=root, width=300, height=100, bg='grey')
button_frame.place(x= 315, y= 10)
# Menu bar that will be used to manage test-banks
menu_bar = Menu(root, background='blue', foreground='black', activebackground='black', activeforeground='blue')
testbank_menu = Menu(menu_bar, tearoff=0, background='blue', foreground='black', activebackground='black', activeforeground='blue')
testbank_menu.add_command(label="New Test Bank", command=DatabaseFunctions.create_table)
testbank_menu.add_command(label="Delete Test Bank", command=DatabaseFunctions.delete_table)
testbank_menu.add_command(label="Add to test bank", command=DatabaseFunctions.new_quizbank_entry)
testbank_menu.add_command(label="Remove from test bank", command=DatabaseFunctions.delete_quizbank_entry)
menu_bar.add_cascade(label="Test Bank",menu=testbank_menu)
# New Quiz button
question_button = customtkinter.CTkButton(master=button_frame, text="Ask a Random Question!", command=lambda: QuizFunctions.generate_question(button_frame))
question_button.grid(row=0, column=0)
root.config(menu=menu_bar)
root.mainloop()
---------- QuizFunctions.py ------------
# Functions pertaining to running users quizzes.
import DatabaseFunctions
import customtkinter
from tkinter import messagebox
def submit(chosen_answer, correct_answer, frame):
if chosen_answer == correct_answer:
messagebox.showinfo("CORRECT")
else:
messagebox.showinfo("WRONG")
# Destroy currently created radiobuttons in frame.
for widget in frame.winfo_children():
widget_type = str(type(widget))
print(widget_type)
if widget_type == "<class 'customtkinter.windows.widgets.ctk_radiobutton.CTkRadioButton'>":
widget.destroy()
elif widget_type == "<class 'customtkinter.windows.widgets.ctk_textbox.CTkTextbox'>":
widget.destroy()
def generate_question(frame):
question_data = DatabaseFunctions.retrieve_question_data()
question = question_data[0]
correct_answer = question_data[1]
answer_bank = question_data[2]
question_box = customtkinter.CTkTextbox(frame)
question_box.grid(row=1, column=0)
question_box.insert("0.0", question)
question_box.configure(state="disabled")
choice_1 = customtkinter.CTkRadioButton(master=frame, text=answer_bank[0], command=lambda: submit(answer_bank[0], correct_answer, frame))
choice_2 = customtkinter.CTkRadioButton(master=frame, text=answer_bank[1], command=lambda: submit(answer_bank[1], correct_answer, frame))
choice_3 = customtkinter.CTkRadioButton(master=frame, text=answer_bank[2], command=lambda: submit(answer_bank[2], correct_answer, frame))
choice_4 = customtkinter.CTkRadioButton(master=frame, text=answer_bank[3], command=lambda: submit(answer_bank[3], correct_answer, frame))
choice_1.grid()
choice_2.grid()
choice_3.grid()
choice_4.grid()
------------ DatabaseFunctions.py ----------------
#Functions involved with database manipulation. This program utilizes SQLite3 for database management.
import sqlite3
import tkinter
from tkinter import messagebox
from tkinter import simpledialog
import random
# Create database if one is not available, then populate Question Bank table.
def create_table():
try:
conn = sqlite3.connect('QuestionBank.db')
cursor = conn.cursor()
cursor.execute('CREATE TABLE QUESTION_BANK (''EntryID INTEGER PRIMARY KEY,''Question varchar(255),''Answer varchar(255)'');')
conn.close()
except Exception as e:
messagebox.showerror(title="An Error Occurred", message=str(e))
# Delete current Question Bank table
def delete_table():
try:
conn = sqlite3.connect('QuestionBank.db')
cursor = conn.cursor()
cursor.execute('DROP TABLE QUESTION_BANK')
conn.close()
except Exception as e:
messagebox.showerror(title="An Error Occurred", message=str(e))
# Generate new entry into the Question Bank table.
def new_quizbank_entry():
try:
user_question = simpledialog.askstring(title='Add Question', prompt='Please enter the question you wish to store in your quiz bank.')
user_answer = simpledialog.askstring(title='Add Answer', prompt='Please enter the correct answer to store in your quiz bank.')
user_entry = [user_question, user_answer]
conn = sqlite3.connect('QuestionBank.db')
cursor = conn.cursor()
cursor.execute("INSERT INTO QUESTION_BANK (Question, Answer) VALUES (?,?);", user_entry)
conn.commit()
conn.close()
except Exception as e:
messagebox.showerror(title="An Error Occurred", message=str(e))
# Delete entry from Question Bank.
def delete_quizbank_entry():
try:
entry_id = simpledialog.askstring(title='Entry Number', prompt='What entry number would you like to delete?')
conn = sqlite3.connect('QuestionBank.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM QUESTION_BANK WHERE EntryID = ?;", entry_id)
conn.commit()
conn.close()
except Exception as e:
messagebox.showerror(title="An Error Occurred", message=str(e))
# Pull new question data.
def retrieve_question_data():
conn = sqlite3.connect('QuestionBank.db')
cursor = conn.cursor()
random_question_pool = cursor.execute("SELECT Question, Answer FROM QUESTION_BANK ORDER BY RANDOM() LIMIT 4;").fetchall()
cursor.close
chosen_question = random_question_pool[0][0]
correct_answer = random_question_pool[0][1]
answer_2 = random_question_pool[1][1]
answer_3 = random_question_pool[2][1]
answer_4 = random_question_pool[3][1]
answer_bank = [correct_answer, answer_2, answer_3, answer_4]
random.shuffle(answer_bank)
return chosen_question, correct_answer, answer_bank
What I am trying to do is instead of opening a simpledialog box. I would like the prompt to be placed in a frame inside of an already established window.
So far I have searched around and every resource I am using opens a new window instead of placing the prompt within a frame.
Is this possible? I'm sure it is, I may just not be using the right keywords in my search.
CodePudding user response:
Generally speaking, the technique is to just put an entry widget wherever you want using whatever method you want. You will need to create either a binding or button that the user can use to trigger that they are done doing input (eg: a binding on the return key, or a "submit" button).
Tkinter has methods that will let you effectively pause your program and wait for the user to do something. You can either wait for a window to be destroyed, or wait for a tkinter variable to be set. So, your binding or your button can do one of those actions.
Here's a contrived example that uses a "submit" button, and a frame that we pop up on top of the current window.
import tkinter as tk
def get_input(widget):
var = tk.StringVar()
frame = tk.Frame(widget, bd=2, relief="groove")
entry = tk.Entry(frame, textvariable=var)
button = tk.Button(frame, text="submit", command=frame.destroy)
button.pack(side="right", padx=(0, 20))
entry.pack(side="left", fill="both", expand=True, padx=(20, 0), pady=20)
# center the frame on the passed-in window, then
# wait for it to be destroyed
frame.place(relx=.5, rely=.5, anchor="c")
entry.focus_set()
frame.wait_window()
print(f"you entered: '{var.get()}'")
root = tk.Tk()
root.geometry("400x400")
button = tk.Button(root, text="Get Input", command=lambda: get_input(root))
button.pack(expand=True)
root.mainloop()
When you run this, you'll get a window with a button in the center.
If you click the button, the function will be called causing the frame and other widgets to appear. The function won't return until you've clicked the submit button.
This example includes a print statement to illustrate that it won't be executed until after the frame has been destroyed. You can see that the output of the print statement won't appear until you've clicked the submit button.
Don't get hung up on the fact that it overlays a frame using place
. The important thing to focus on is the call to frame.wait_window()
, which pauses the execution of the code while still being able to process events. When you click the button, the frame is destroyed and frame.wait_window()
will return, allowing the code to continue.
Also important to notice is that I added a textvariable
to the widget. That is because we are triggering on the frame being destroyed which won't happen until the entry widget has been destroyed. That means we can't call the get
method of the entry since it will no longer exist. We can, however, call the get
method of the variable since it won't be destroyed when the frame is destroyed.
If you want the frame to always be visible but still wait until the user clicks the submit button, you can use tkinter's wait_variable
method instead. In that case, you should not associate the variable with the entry. Instead, have the command
call a function that sets the variable to the return of entry.get()
before destroying the frame.