How do I keep a tkinter canvas within a window? - python

When I run my code, my grid of entry fields does not fit within the window, meaning I have to expand the window in order to access the lower entry fields and the buttons. I want to be able to get to them by scrolling instead.
I have tried various combinations of frames and canvasses, including putting the entry fields directly on the canvas, but at no point have I been able to create a canvas the size of the window(and therefore smaller than the grid of entries contained within it).
def __init__(self, window):
# parameter entry fields below
column_headers = ["Duration (ns)", "SPDT state", "SP4T state", "RF relative wave"]
row_number=50
self.entries = []
canvas = tk.Canvas(window, width=700, height=600)
frame=tk.Frame(canvas)
frame.grid(row=0, column=0)
canvas.configure(scrollregion=frame.bbox("all"))
for col_num, col_name in enumerate(column_headers):
tk.Label(frame, text = col_name).grid(row = 0, column = col_num)
for row_num in range(row_number): # Creates grid of entry fields and stores locations in a list of lists.
self.entries.append([]) # Entry field contents accessed by self.entries[row_num][col_num].get() (both starting at 0)
for col_num, col_name in enumerate(column_headers):
self.entries[row_num].append(tk.StringVar())
self.entries[row_num][col_num] = tk.Entry(frame)
self.entries[row_num][col_num].grid(row = row_num+1, column = col_num)
tk.Button(frame, text = "Update Parameters", command=self.get_params).grid(row = row_number+4)
tk.Button(frame, text = "Run Sweep", command= run_expt).grid(row = row_number+4, column = 1)
tk.Button(frame, text = "Abort", command = abort).grid(row = row_number+4, column = 2)
# data storage stuff below
tk.Label(frame, text="File Name").grid(sticky='W', row=row_number+3, column=0)
self.fileNameEntry = tk.StringVar()
self.fileNameEntry = tk.Entry(frame)
self.fileNameEntry.grid(row=row_number+3, column=1)
vbar = tk.Scrollbar(window, orient=tk.VERTICAL, command=canvas.yview)
canvas.configure(yscrollcommand=vbar.set)
vbar.pack(side=tk.RIGHT, fill=tk.Y)
canvas.pack(fill=tk.BOTH)
window=tk.Tk()
window.geometry("700x600")
EPR=EPRGUI(window)
window.mainloop()
Just so no one suggests this, I'd like to point out that I do have functions for all the buttons in my code, but have omitted them from this question to make it a bit quicker to read.

Related

tkinter: cleanly destroy auto-wrapping widgets created in tk.Text with window_create

Based on this answer, I successfully created my own widget where labels are wrapped automatically in a Text element using window_create.
I'm also able to destroy these labels using nametowidget. However, the window element of the deleted widgets remains and is returned by dump.
Here is my minimal reproducible example:
import tkinter as tk
root = tk.Tk()
btn_frame1 = tk.Frame(root)
btn_frame1.pack(side='top', fill='x')
create_labels_btn = tk.Button(btn_frame1, text='Create Labels')
create_labels_btn.pack(side='left')
delete_labels_btn = tk.Button(btn_frame1, text='Delete Labels')
delete_labels_btn.pack(side='right')
label_text_field = tk.Text(root, height=7, width=40)
label_text_field.pack(side='top')
dump_text_field_btn = tk.Button(root, text='Dump text field')
dump_text_field_btn.pack(side='top')
dumping_text = tk.Text(root, height=7, width=40)
dumping_text.pack(side='top')
def create_labels():
for i in range(1, 7):
lbl = tk.Label(label_text_field, width=12, text=f'label {i}')
label_text_field.window_create("insert", window=lbl, padx=8, pady=8)
create_labels_btn['command'] = create_labels
def delete_labels():
for lbl in label_text_field.dump("1.0", "end"):
if lbl[0] =='window' and lbl[1]:
label_text_field.nametowidget(lbl[1]).destroy()
delete_labels_btn['command'] = delete_labels
def dump_text_field():
dumping_text.delete("1.0", "end")
for obj in label_text_field.dump("1.0", "end"):
dumping_text.insert('end', str(obj) + '\n')
dump_text_field_btn['command'] = dump_text_field
root.mainloop()
After creating and deleting my labels. The deleted elements are still visible in the dump output (the widget was succesfully deleted though and the remaining name is only an empty string):
How can I cleanly remove the associated window so that they don't add up over time?
You need to remove the items from the Text widget using the index returned by .dump(). But you need to remove them in reverse order, otherwise the index will be wrong after removing the first item.
def delete_labels():
for lbl in label_text_field.dump("1.0", "end")[::-1]: # get the items in reverse order
if lbl[0] =='window' and lbl[1]:
label_text_field.nametowidget(lbl[1]).destroy()
label_text_field.delete(lbl[2]) # remove item from text box as well
Actually you can pass window=1 to dump() to return window items only:
def delete_labels():
for lbl in label_text_field.dump("1.0", "end", window=1)[::-1]:
if lbl[1]:
label_text_field.nametowidget(lbl[1]).destroy()
label_text_field.delete(lbl[2])

Refresh tkinter GUI after scanning three barcodes and compare strings

I am new to python and very new to GUI tkinter. I am working on a project for work that is a simple task, but I wanted to try to make it work using a GUI. The task is to scan three barcodes, parse the last nine digits of the barcodes, and compare. If they all match, then display "pass"; if they don't display, "fail". I have written the code using the basic command line and I was able to get everything to work. Now I am trying to implement that code with a graphical user interface, which is where I am struggling.
The issue I am running into is knowing how or what to update/refresh. My logic works as long as I have a string entered in the ENTRY text box before the code is executed. If the strings are different, it fails; if they are the same or empty, it passes.
I wanted to be able to scan three serial numbers and have the bottom frame update pass or fail and then scan a different set of serial numbers and get another result. See my code below. Any help will be greatly appreciated. Thanks everyone!
from tkinter import *
# FUNCTION THAT ALLOWS THE CURSOR TO MOVE TO THE NEXT ENTRY TEXT BOX AUTOMATICALLY.
def go_to_next_entry(event, entry_list, this_index):
next_index = (this_index + 1) % len(entry_list)
entry_list[next_index].focus_set()
# FUNCTION THAT DETERMINES IF THE THREE SERIAL NUMBERS ARE THE SAME OR DIFFERENT.
# DISPLAYS PASS OR FAIL BASED ON RESULT OF CONDITION.
def get_results():
# DECLARING AND INITIALISING VARIABLES THAT ARE EQUAL TO INPUT FROM ENTRY TEXT BOXES.
scan_one = scan_one_entry.get()
scan_two = scan_two_entry.get()
scan_three = scan_three_entry.get()
# PARSING THE LAST NINE DIGITS OF THE ENTERED STRING.
parsed_scan_one = scan_one[-9:]
parsed_scan_two = scan_two[-9:]
parsed_scan_three = scan_three[-9:]
# IF-ELSE CONDITION THAT DISPLAYS PASS IF THREE SERIAL NUMBERS ARE THE SAME AND FAIL IF THEY
ARE NOT THE SAME.
if parsed_scan_one == parsed_scan_two and parsed_scan_two == parsed_scan_three and
parsed_scan_three == parsed_scan_one:
# DELETING DATA THAT IS STORED IN ENTRY TEXT BOXES.
scan_one_entry.delete(0, END)
scan_two_entry.delete(0, END)
scan_three_entry.delete(0, END)
# PLACING THE PASS BOTTOM FRAME IF CONDITION IS MET.
pass_bottom_frame.grid(row=1, sticky="ns, ew")
# CREATING PASS BOTTOM FRAME WIDGETS OF IF CONDITION IS MET.
pass_label = Label(pass_bottom_frame,
text='PASSED SCAN CHECK!',
font=('Helvetica', 100),
justify="center")
# PICKING BACKGROUND COLOR AND FONT COLOR OF LABEL WIDGET
pass_label.config(bg="#63d464", fg="#000000")
# PLACING THE PASS BOTTOM FRAME WIDGET IF CONDITION IS MET
pass_label.place(relx=.5, rely=.5, anchor="center")
else:
# DELETING DATA THAT IS STORED IN ENTRY TEXT BOXES.
scan_one_entry.delete(0, END)
scan_two_entry.delete(0, END)
scan_three_entry.delete(0, END)
# PLACING THE FAILED BOTTOM FRAME.
fail_bottom_frame.grid(row=1, sticky="ns, ew")
# CREATING PASS BOTTOM FRAME WIDGETS.
fail_label = Label(fail_bottom_frame,
text='FAILED SCAN CHECK!',
font=('Helvetica', 100),
justify="center")
# PICKING BACKGROUND COLOR AND FONT COLOR OF LABEL WIDGET.
fail_label.config(bg="#f51023", fg="#000000")
# PLACING THE FAILED BOTTOM FRAME WIDGET.
fail_label.place(relx=.5, rely=.5, anchor="center")
# CREATING MAIN WINDOW
main_window = Tk()
main_window.title('Serial Number Barcode Scan Check')
main_window.state("zoomed")
# CREATING THE MAIN FRAMES THAT WILL BE PLACED IN MAIN WINDOW
top_frame = Frame(main_window, width=1800, height=500)
pass_bottom_frame = Frame(main_window, bg="#63d464", width=1800, height=500)
fail_bottom_frame = Frame(main_window, bg="#f51023", width=1800, height=500)
# LAYOUT MAIN TOP FRAME
main_window.grid_rowconfigure(1, weight=1)
main_window .grid_columnconfigure(0, weight=1)
# PLACING TOP FRAME
top_frame.grid(row=0, sticky="ns, ew")
# CREATING TOP FRAME WIDGETS
# TOP THREE ARE LABELS AND THE LAST THREE ARE ENTRY BOXES
scan_one_label = Label(top_frame,
text='ENTER SCAN ONE: ',
font=('Helvetica', 40))
scan_two_label = Label(top_frame,
text='ENTER SCAN TWO: ',
font=('Helvetica', 40))
scan_three_label = Label(top_frame,
text='ENTER SCAN THREE: ',
font=('Helvetica', 40))
scan_one_entry = Entry(top_frame, font="Helvetica 20", justify="center", border=5)
scan_two_entry = Entry(top_frame, font="Helvetica 20", justify="center", border=5)
scan_three_entry = Entry(top_frame, font="Helvetica 20", justify="center", border=5)
# PLACING THE TOP FRAME WIDGETS INTO THE TOP FRAME
scan_one_label.grid(row=0, column=0, sticky='w')
scan_two_label.grid(row=1, column=0, sticky='w')
scan_three_label.grid(row=2, column=0, sticky='w')
scan_one_entry.grid(row=0, column=1, ipadx=100, ipady=8)
scan_two_entry.grid(row=1, column=1, ipadx=100, ipady=8)
scan_three_entry.grid(row=2, column=1, ipadx=100, ipady=8)
# CODE USED TO MOVE TO EACH ENTRY TEXT BOX AUTOMATICALLY
entries = [child for child in top_frame.winfo_children() if isinstance(child, Entry)]
for index, entry in enumerate(entries):
entry.bind('<Return>', lambda e, index=index: go_to_next_entry(e, entries, index))
# CALLING THE FUNCTION GET RESULTS
get_results()
# MAIN LOOP
main_window.mainloop()

ttk combobox selection synchronize the state of entry

I would like to realize a special case, when i select the value in combobox, the entry state changes correspondingly, e.g. when i select 1 in levels, the entry of level-1 is active, while the entry of level-2 is disabled, can anyone give me some suggestion. when i select 2 in levels, both of the state of entry are enabled.
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk
class MainGui(tk.Tk):
def __init__(self):
super().__init__()
self.create_widgets()
def create_widgets(self):
label_1 = tk.Label(self,
text = "Levels:",
anchor = 'w')
label_1.grid(row=0, column=0)
ComboBox1 = ttk.Combobox(self, width = 10, state = 'readonly')
ComboBox1["values"] = ("1", "2")
ComboBox1.current(0)
ComboBox1.grid(row=0, column=1)
label1 = tk.Label(self,
text = "Level-1:",
anchor = 'w')
label1.grid(row=1, column=0)
entry1 = tk.Entry(self,
width = 10,
)
entry1.grid(row=1, column=1)
label2 = tk.Label(self,
text = "Level-2:",
anchor = 'w')
label2.grid(row=2, column=0)
entry2 = tk.Entry(self,
width = 10,
)
entry2.grid(row=2, column=1)
def main():
# gui
root = MainGui()
root.mainloop()
if __name__ == '__main__':
main()
You can bind the virtual event <<ComboboxSelected>> on ComboBox1 and update the state of Entry2 in the event callback which will be executed whenever the selection of ComboBox1 is changed. But you need to change ComboBox1 and entry2 to instance variables in order to access them inside the event callback:
def create_widgets(self):
...
# changed ComboBox1 to instance variable self.ComboBox1
self.ComboBox1 = ttk.Combobox(self, width = 10, state = 'readonly')
self.ComboBox1["values"] = ("1", "2")
self.ComboBox1.current(0)
self.ComboBox1.grid(row=0, column=1)
self.ComboBox1.bind("<<ComboboxSelected>>", self.on_select)
...
# changed entry2 to instance variable self.entry2
self.entry2 = tk.Entry(self,
width = 10,
state = "disabled" # initially disabled
)
self.entry2.grid(row=2, column=1)
def on_select(self, event=None):
# set state of entry2 based on selected value
self.entry2.config(state="normal" if self.ComboBox1.get()=="2" else "disabled")
Here is a solution (sample):
from tkinter import Tk, Label, Entry, Frame
from tkinter.ttk import Combobox
entry_list = []
def activate_entries(event=None):
to = int(combobox.get())
for entry in entry_list[:to]:
entry.config(state='normal')
for entry in entry_list[to:]:
entry.config(state='disabled')
root = Tk()
values = []
for i in range(5):
(frame := Frame(root)).grid(row=i + 1, column=0)
Label(frame, text=f'Level-{i + 1}').pack()
(e := Entry(frame, state='disabled' if i != 0 else 'normal',
disabledbackground='grey80')).pack()
entry_list.append(e)
values.append(i + 1)
combobox = Combobox(root, values=values, state='readonly')
combobox.current(0)
combobox.grid(row=0, column=0)
combobox.bind('<<ComboboxSelected>>', activate_entries)
root.mainloop()
The main part here is the activate_entries() function, it simply gets the value from combobox and uses a list of the entries to set their state based on their index in the list which is given by the combobox. (Also some parts require a Python version of 3.8 or higher, can be adjusted for older versions too), also note the <<ComboboxSelected>> event that calls the activate_entries() function when user selects something from the combobox.
EDIT: added disabledbackground='grey80' to visually indicate that the entry is disabled
EDIT2: you can obviously set the range to two which will create two entries, their reference is saved in the entry_list so you can access them through that list to get their value for example

Set Entry widget values dynamically in Tkinter

I have a window to browse a folder containing necessary files. I am using tkFileDialog for the same. I want to set the value of Entry widget equal to this selected folder. Initially when no folder is selected it will be null. As soon as I select the folder, the path of the selected folder should appear in the Entry widget. The user should be able to modify.Below mentioned is the code for the same.
from Tkinter import *
from tkFileDialog import *
class Checkit:
root = Tk()
#default string to be displayed in the entry of path
path_to_file = StringVar(root, value="abc")
def __init__(self):
self.inputDetail()
def inputDetail(self):
#copy the root window
master = self.root
#create frame for details in the root window
details_frame = Frame(master)
details_frame.grid(row=0, column=0)
#Create the Labels
papercode_label = Label(details_frame, text="Paper code:")
subject_label = Label(details_frame, text="Subject:")
chapter_label = Label(details_frame, text="Chapter:")
batch_label = Label(details_frame, text="Batch:")
ansFolder_label = Label(details_frame, text="Folder containing answer-keys:")
#create entry for the labels
papercode_entry = Entry(details_frame)
subject_entry = Entry(details_frame)
chapter_entry = Entry(details_frame)
batch_entry = Entry(details_frame)
ansFolder_entry = Entry(details_frame)
#create button to add path
path = Button(details_frame, text="Browse", command= lambda: self.addpath(details_frame))
#button to enter the next window
next = Button(details_frame, text="Next", command= lambda: self.checkOmr(details_frame, master))
#Use grid layout to place labels and entry
papercode_label.grid(row=0, sticky=W)
papercode_entry.grid(row=1, sticky=W)
subject_label.grid(row=2, sticky=W)
subject_entry.grid(row=3, column=0, sticky=W)
chapter_label.grid(row=4, sticky=W)
chapter_entry.grid(row=5, column=0, sticky=W)
batch_label.grid(row=6, sticky=W)
batch_entry.grid(row=7, column=0, sticky=W)
ansFolder_label.grid(row=8, sticky=W)
path.grid(row=9, sticky=W, columnspan=2)
next.grid(row=10, sticky=E, columnspan=2)
master.mainloop()
def checkOmr(self, old_frame, master):
#destoy the old frame
old_frame.destroy()
#create frame for details in the root window
inputPath_frame = Frame(master)
inputPath_frame.grid(row=0, column=0)
#create label to input folder containing
omrFolder_label = Label(inputPath_frame, text="Folder containing OMR sheet to be checked:")
#create button to add path
path = Button(inputPath_frame, text="Browse", command= lambda: self.addpath(inputPath_frame))
selected_path = Entry(inputPath_frame, textvariable=self.path_to_file)
#place the label and button on the grid
omrFolder_label.grid(row=0, sticky=W)
path.grid(row=1, column=0, sticky=W)
selected_path.grid(row=1, column=1, sticky=W)
#master.mainloop()
def addpath(self, details_frame):
self.path_to_file = askdirectory(parent=details_frame,initialdir="/",title='Please select a directory')
if __name__=='__main__':
handle = Checkit()
Here I am trying to change the modifying the self. path_to_file value on the click of the button. I tried to print the value of self.path_to_file value in addpath(). It gives correct result there but the value in the Entry selected_path in checkOMR() does not modify.
Can somebody suggest what changes should I make to make that thing possible.
Look at this line:
self.path_to_file = askdirectory(...)
Before this line of code runs, self.path_to_file is an instance of a StringVar. After this line of code has run, self.path_to_file is reset to be just a string.
Assuming you want self.path_to_file to remain an instance of StringVar, you need to change that line to this:
path = askdirectory(...)
self.path_to_file.set(path)

Text widget bulging column widths in the master frame

I have a master frame with five columns, which I want to stay at same width all the time, regardless of how window is resized. There are three rows, the bottom being buttons.
The problem is when I added a Text widget to the larger Frame 3, it stretches out the column width in colums 2, 3, 4 to wider than columns 0 & 1.
I've tried to deal with this by making sure I set all the column weights equal, but this only helps keep the columns maintain their relative size while resizing the master window. It doesn't force the columns to be the same with when the master is instantiated.
I tried using a self.columnconfigure(c,minsize=100) and this makes the columns look even width when I instantiate the object. But if I resize the window bigger, the three righthand colums expand faster than the left two columns, despite all being assigned the same weight! And if I make the window two narrow, in order to maintain the 'minsize' of width, columns aren't visible on the far right. So this isn't a solution.
screenshot of my issue
def __init__(self, master=None):
Frame.__init__(self, master)
self.master.rowconfigure(0, weight=1)
self.master.columnconfigure(0, weight=1)
self.grid(sticky=W+E+N+S)
""" Establish grids, columns for the master Frame widget """
for c in range(5):
# 0,1,2,3,4
self.columnconfigure(c, weight=1)
self.rowconfigure(0, weight = 1)
self.rowconfigure(1, weight = 1)
self.rowconfigure(2, weight = 0)
""" Generate Buttons """
self.button = dict()
for r in range(5):
self.button[r]=Button(self)
self.button[r].grid(column=r, row=2, sticky=N+S+E+W)
self.button[0].config(command=self.b0, text="red")
self.button[1].config(command=self.b1, text="blue")
self.button[2].config(command=self.b2, text="green")
self.button[3].config(command=self.b3, text="black")
self.button[4].config(command=self.b4, text = "open")
continued
""" Frame 1 """
self.f1 = Frame(self, bg="red")
self.f1.grid(row=0, column=0, columnspan=2,rowspan=1, sticky=N+S+W+E)
self.f1.bind("<Button-1>", self.f1_button)
"""didn't help"""
#self.f1.columnconfigure(0, weight=1)
#self.f1.columnconfigure(1, weight=1)
self.label_1_var = StringVar()
self.label_1_var.set("frame 1")
self.label_1 = Label(self.f1, textvariable=self.label_1_var)
self.label_1.grid(row=1, column=1)
""" Frame 2 """
self.f2 = Frame(self, bg="blue")
self.f2.grid(row=1, column=0, columnspan=2,rowspan=1, sticky=N+S+E+W)
self.f2.bind("<Button-1>", self.f2_button)
"""didn't help"""
#self.f2.columnconfigure(0, weight=1)
#self.f2.columnconfigure(1, weight=1)
self.label_2_var = StringVar()
self.label_2_var.set("frame 2")
self.label_2 = Label(self.f2, textvariable = self.label_2_var)
self.label_2.grid(row=1, column=1)
continued
""" frame 3 """
self.f3 = Frame(self, bg="green")
self.f3.grid(row=0, column=2, columnspan=3, rowspan = 2, sticky=N+S+E+W)
self.f3.rowconfigure(0,weight=1)
self.f3.rowconfigure(1,weight=0)
self.f3.columnconfigure(0, weight=1)
"""list some files to try"""
files = glob.glob("*")
default_display =""
for fn in files:
default_display += fn + "\n"
""" Text widget """
self.f3_text = Text(self.f3)
self.f3_text.insert(END, default_display)
self.f3_text.grid(column=0, row=0,sticky=N+S+E+W)
""" Text scrollbar """
self.sb = Scrollbar(self.f3)
self.sb.grid(column=1, row=0, sticky=N+S+E+W)
self.f3_text.config(yscrollcommand=self.sb.set)
self.sb.config(command=self.f3_text.yview)
""" Entry Window """
self.f3_entry = Entry(self.f3)
self.f3_entry.grid(column=0, row=1, columnspan=2, sticky=N+S+E+W)
A simple solution is to set the width and height of the widget to 1, and then rely on the grid options to stretch it to fill its container. Since it's natural size is smaller than the cell it is in, it won't cause the cell to grow.
You want to set the width of the text widget:
text = Text(frame, width=20)
By default, the width is set to 80 characters.
Note that this value isn't constant, so the widget is resized correctly.
Hint: "... regardless of the resize":
While it would be great to rely on behaviour / services of the most common public methods exposed from automated Tk-GUI-MVC-Visual Part, the real-world layout complexities may get us to a dead-end.
Try a hard-coded programmatic way to automatically ad-hoc re-calculate / re-construct the Tk-GUI-MVC-Visual Part from new, changed, internal values from the Tk-GUI-MVC-Controller Part:
master.bind( '<Configure>', aResizeCallbackHANDLER( ) ) # MVC-Controller Part TRIGGER
def aResizeCallbackHANDLER( self, anEvent ): # Reactor:
# inspect <anEvent>.{} # .bind()-bound actor
# and <anEvent>.widget.{} # details from
# incl. <anEvent>.widget.<Tree-sub-element>.{} # Tk-GUI-MVC-Model Part
#
# <anEvent>.widget <-- system-assigned <widget>-instance
# .height on-{ <Configure> }
# .width on-{ <Configure> }
# .serial <-- system-assigned Integer
# .time <-- system-assigned Integer ( .inc each msec )
# .
# ..
# ...
# Act upon changes and enforce new, re-calculated values
# into sub-ordinated geometry
# ...
#
This principle is robust and does not rely on any "hidden" inter-relations of Tk-GUI-MVC-elements.

Categories