Posts: 61
Threads: 29
Joined: Oct 2023
Jan-31-2024, 03:45 PM
(This post was last modified: Jan-31-2024, 03:48 PM by jacksfrustration.)
I am trying to write a program using tkinter. I created a Class and in the init i create a few lists. The window consists of entrys, optionmenus, labels, picture and buttons. all of the widgets except for the two optionmenu widgets are in a window. these two optionmenu widgets are located in their own window. image is attached
picture of problem
please request me to provide the relevant code if need be. anyone knows why this is happening?
Posts: 6,798
Threads: 20
Joined: Feb 2020
Jan-31-2024, 11:28 PM
(This post was last modified: Jan-31-2024, 11:28 PM by deanhystad.)
Is the small window with the 4 option menus the root window (window returned by tk.Tk())? When you create a widget and you don't provide a parent argument, the widget is added to the root window. You can see that here:
import tkinter as tk
root = tk.Tk()
root.title("root")
font = (None, 100)
window = tk.Toplevel(root)
window.title("window")
tk.Label(window, text="In Window", font=font).pack()
tk.Label(text="No Parent", font=font).pack() # Parent not specified
root.mainloop()
Posts: 61
Threads: 29
Joined: Oct 2023
(Jan-31-2024, 11:28 PM)deanhystad Wrote: Is the small window with the 4 option menus the root window (window returned by tk.Tk())? When you create a widget and you don't provide a parent argument, the widget is added to the root window. You can see that here:
import tkinter as tk
root = tk.Tk()
root.title("root")
font = (None, 100)
window = tk.Toplevel(root)
window.title("window")
tk.Label(window, text="In Window", font=font).pack()
tk.Label(text="No Parent", font=font).pack() # Parent not specified
root.mainloop() im not trying to have two windows. i want one with all of the widgets present in it. should i post my code? its like 200 something lines
Posts: 6,798
Threads: 20
Joined: Feb 2020
Feb-03-2024, 06:43 PM
(This post was last modified: Feb-03-2024, 06:43 PM by deanhystad.)
You must be making two windows. tk.Tk() creates a window and tk.TopLevel() creates a window. Are you calling both in your program? If you only call tk.Tk() you will only have 1 window.
Posting a 200 line program is fine.
Posts: 61
Threads: 29
Joined: Oct 2023
Feb-05-2024, 05:16 PM
(This post was last modified: Feb-05-2024, 05:16 PM by jacksfrustration.)
(Feb-03-2024, 06:43 PM)deanhystad Wrote: You must be making two windows. tk.Tk() creates a window and tk.TopLevel() creates a window. Are you calling both in your program? If you only call tk.Tk() you will only have 1 window.
Posting a 200 line program is fine.
im not using top level. i know that creates a second window but like i said, i want only one window. my code follows
from tkinter import *
from tkinter import messagebox
import json
from datetime import datetime,timedelta
from tkcalendar import DateEntry
import calendar as cal
class WorkoutTracker:
def __init__(self, window,canvas):
self.window = window
self.day_value_inside= StringVar(self.window, value="Choose a date")
self.act_value_inside = StringVar(self.window, value="Choose a workout")
self.canvas=canvas
self.cur_row = 0
self.count = 0
self.updated_list,self.day_lbl,self.act_lbl, \
self.day_opt,self.act_opt,self.unit_lbl, \
self.unit_ent,self.min_lbl=[], [],[],[],[],[],[],[]
self.dates_list = self.all_dates_current_month()
self.pic_file = PhotoImage(file="./workout.png")
self.setup_gui()
self.add_column()
def all_dates_current_month(self,days=60,format="%a %d %B %Y"):
'''generates a list of all days inside the current month and returns it.
The dates are presented in a spcific format ("day name","date","month" and "year")'''
dates=[]
start=datetime.now() + timedelta(days=-30)
for x in range(-30,days+1):
dates.append((start + timedelta(days=x)).strftime(format))
print(dates)
return dates
def setup_gui(self):
# make a list of the 4 past and 4 upcoming days
for index, dat in enumerate(self.dates_list):
if dat == datetime.today().strftime('%a %d %B %Y'):
self.updated_list.extend(self.dates_list[index - 5:index+6])
self.window = Tk()
self.canvas.grid(row=0, column=1, columnspan=8)
pic = self.canvas.create_image(240, 135, image=self.pic_file)
remove_button=Button(text="Remove Last Entry",command=self.remove_button)
remove_button.grid(row=1,column=7)
gen_row_but = Button(text="Add entry", command=self.add_column)
gen_row_but.grid(row=1, column=8)
reg_but = Button(text="Register activities", command=self.reg_act)
reg_but.grid(row=1, column=9)
display_but = Button(text="Display Saved Activities", command=self.display_act)
display_but.grid(row=1, column=10)
def reg_act(self):
'''saves dates of workout, activities and duration in a dictionary setting and later saves these in a json file'''
try:
with open("saved_data.json", "r") as file:
saved_data = json.load(file)
except FileNotFoundError:
saved_data = {}
with open("saved_data.json", "w") as file:
json.dump(saved_data, file)
except json.decoder.JSONDecodeError:
saved_data = {}
finally:
for i in range(0, self.count):
day = self.day_value_inside.get()
activity = self.day_value_inside.title()
try:
unit = float(self.unit_ent[i].get())
except ValueError:
messagebox.showerror(title="Oooops", message="You have not entered duration information")
else:
if messagebox.askokcancel(title=f"{day} Workout Information",
message=f"Activity: {activity} for {unit} minutes.\nSave this activity?"):
if day in saved_data:
saved_data[day]["activity"].append(activity)
saved_data[day]["unit"].append(unit)
else:
new_data = {day: {"activity": [activity], "unit": [unit]}}
saved_data.update(new_data)
with open("saved_data.json", "w") as file:
json.dump(saved_data, file, indent=4)
def display_act(self):
'''makes a string of all activities within a specific date. uses messagebox to display each day as it is iterated through'''
try:
with open("saved_data.json", "r") as file:
data = json.load(file)
except json.decoder.JSONDecodeError:
data = {}
messagebox.showerror(title="Ooooops", message=f"There is no saved data to display")
finally:
data_str = ""
for key in data:
for i, act in enumerate(data[key]['activity']):
data_str += f"On {key} you did {data[key]['activity'][i].lower()} for {data[key]['unit'][i]} minutes.\n"
messagebox.showinfo(title=f"{key} Workout Information", message=f"{data_str}")
data_str = ""
def add_column(self):
'''generates a column of entries and labels
names of each entry and label are created dynamically
and each widget has a location added. Tkinter "rows" are incremented so each time this function is called
the next column of widgets gets generated below the previous'''
self.cur_row += 1
self.day_lbl.append(Label(text="On"))
self.day_lbl[-1].grid(row=self.cur_row, column=0)
self.day_opt.append(OptionMenu(self.window,self.day_value_inside, self.updated_list[0], self.updated_list[1],
self.updated_list[2], self.updated_list[3], self.updated_list[4],
self.updated_list[5], self.updated_list[6], self.updated_list[7],
self.updated_list[8], self.updated_list[9]))
self.day_opt[-1].grid(row=self.cur_row, column=1)
self.act_lbl.append(Label(text="i did"))
self.act_lbl[-1].grid(row=self.cur_row,column=2)
self.act_opt.append(OptionMenu(self.window,self.act_value_inside,"Aerobics", "Cycling","Running", "Swimming", "Walking"))
self.act_opt[-1].grid(row=self.cur_row,column=3)
self.unit_lbl.append(Label(text="for: "))
self.unit_lbl[-1].grid(row=self.cur_row,column=4)
self.unit_ent.append(Entry())
self.unit_ent[-1].grid(row=self.cur_row,column=5)
self.min_lbl.append(Label(text="minutes"))
self.min_lbl[-1].grid(row=self.cur_row,column=6)
self.count += 1
def remove_button(self):
self.day_lbl[-1].forget()
self.act_lbl[-1].forget()
self.day_opt[-1].forget()
self.act_opt[-1].forget()
self.unit_lbl[-1].forget()
self.unit_ent[-1].forget()
self.min_lbl[-1].forget()
self.count-=1
def display_act(self):
'''makes a string of all activities within a specific date. uses messagebox to display each day as it is iterated through'''
try:
with open("saved_data.json", "r") as file:
data = json.load(file)
except json.decoder.JSONDecodeError:
data = {}
messagebox.showerror(title="Ooooops", message=f"There is no saved data to display")
finally:
data_str = ""
for key in data:
for i, act in enumerate(data[key]['activity']):
data_str += f"On {key} you did {data[key]['activity'][i].lower()} for {data[key]['unit'][i]} minutes.\n"
messagebox.showinfo(title=f"{key} Workout Information", message=f"{data_str}")
data_str = ""
if __name__ == "__main__":
window = Tk()
window.title("Workout Tracker")
canvas=Canvas(width=400,height=227)
tracker = WorkoutTracker(window,canvas)
window.mainloop()
Posts: 6,798
Threads: 20
Joined: Feb 2020
Feb-05-2024, 09:13 PM
(This post was last modified: Feb-07-2024, 12:14 PM by deanhystad.)
You do this on line 172:
if __name__ == "__main__":
window = Tk() And this on line 41
def setup_gui(self):
# make a list of the 4 past and 4 upcoming days
for index, dat in enumerate(self.dates_list):
if dat == datetime.today().strftime('%a %d %B %Y'):
self.updated_list.extend(self.dates_list[index - 5:index+6])
self.window = Tk() That is 2 windows. Calling Tk() twice in a program is also an error.
And I don't see where you ever use the correct arguments for making buttons or labels. Instead of this:
gen_row_but = Button(text="Add entry", command=self.add_column) You really need to do this:
gen_row_but = Button(window, text="Add entry", command=self.add_column) You only specify the parent window when you make option menus. That is why option menus show in the other window.
The first argument should always be the parent window for the widget. If you leave this out, the default is to add widgets to the window created by Tk(). This lazy behavior will start causing more problems as your windows become more complicated.
Additional comments.
Learn about list pack/unpack. Instead of this:
self.day_opt.append(OptionMenu(self.window,self.day_value_inside, self.updated_list[0], self.updated_list[1],
self.updated_list[2], self.updated_list[3], self.updated_list[4],
self.updated_list[5], self.updated_list[6], self.updated_list[7],
self.updated_list[8], self.updated_list[9])) do this:
self.day_opt.append(OptionMenu(self.window, self.day_value_inside, *self.updated_list)) This is an error
self.act_opt.append(OptionMenu(self.window,self.act_value_inside,"Aerobics", "Cycling","Running", "Swimming", "Walking")) You cannot reuse the self.act_value_inside. You need to create a new variable for each OptionMenu instance.
As mentioned in your other thread, this will not work:
def remove_button(self):
self.day_lbl[-1].forget()
self.act_lbl[-1].forget()
self.day_opt[-1].forget()
self.act_opt[-1].forget()
self.unit_lbl[-1].forget()
self.unit_ent[-1].forget()
self.min_lbl[-1].forget()
self.count-=1 You are using the wrong type of forget (need to use grid_forget()), and your logic is wrong. Your forgotten widgets remain in the lists. When you call remove_button() a second time it forgets the already forgotten widgets.
Posts: 6,798
Threads: 20
Joined: Feb 2020
Feb-06-2024, 05:21 PM
(This post was last modified: Feb-06-2024, 05:21 PM by deanhystad.)
If you have a table of workout activities, your program should have something like a table, and something that represents workout activities.
import tkinter as tk
from tkinter import ttk
from datetime import datetime, timedelta
class DatePicker(ttk.Combobox):
"""A widget for entering a date. Can type in or select from list."""
format = "%b %d, %Y"
def __init__(self, *args, date=None, days=10, **kwargs):
"""date sets current value. Defaults to today.
days is number of days in list surrounding current selection.
"""
super().__init__(*args, postcommand=self.postcommand, **kwargs)
date = datetime.today() if date is None else date
self.set(date.strftime(self.format))
self.days = days
def postcommand(self):
"""Update list to self.days before and after current date."""
date = datetime.strptime(self.get(), self.format)
self["values"] = [
(date + timedelta(days=d)).strftime(self.format)
for d in range(-self.days, self.days+1)
]
class TableRow(tk.Frame):
"""A row in a table. Get set values using properites."""
activities = ("Aerobics", "Cycling", "Running", "Swimming", "Walking")
def __init__(self, parent, *args, **kwargs):
"""Create widgets that appear in a row"""
super().__init__(parent, *args, **kwargs)
self._date = DatePicker(self, width=12)
self._activity = ttk.Combobox(self, values=self.activities, width=10)
self.activity = self.activities[0]
self._duration = tk.DoubleVar(parent, 30)
for x in (
self._date,
self._activity,
tk.Entry(self, textvariable=self._duration, width=8, justify=tk.RIGHT),
tk.Button(self, text="X", command=lambda: parent.remove_row(self))
): x.pack(padx=2, side=tk.LEFT)
def __str__(self):
return f"{self.activity} on {self.date} for {self.duration} minutes"
def __repr__(self):
return str(self)
@property
def activity(self):
return self._activity.get()
@activity.setter
def activity(self, value):
self._activity.set(value)
@property
def duration(self):
return self._duration.get()
@duration.setter
def duration(self, value):
self._duration.set(value)
@property
def date(self):
return self._date.get()
@date.setter
def date(self, value):
self._date.set(value)
class Table(tk.Frame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
header = tk.Frame(self)
header.pack(side=tk.TOP, expand=1, fill="x")
tk.Label(header, text="Date", width=13).pack(side=tk.LEFT)
tk.Label(header, text="Activity", width=13).pack(side=tk.LEFT)
tk.Label(header, text="Minutes").pack(side=tk.LEFT)
self.rows = []
def append_row(self):
self.rows.append(TableRow(self))
self.rows[-1].pack(side=tk.TOP)
def remove_row(self, row):
if row in self.rows:
self.rows.remove(row)
row.destroy()
def __str__(self):
return "\n".join(str(row) for row in self.rows)
class WorkoutTracker(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.table = Table(self)
self.table.pack(side=tk.TOP)
buttons = tk.Frame(self)
buttons.pack(side=tk.TOP)
self.append_btn = tk.Button(buttons, text="Add Activity", command=self.table.append_row)
self.append_btn.pack(side=tk.LEFT)
self.print_btn = tk.Button(buttons, text="Print Activities", command=lambda: print(self.table))
self.print_btn.pack(side=tk.LEFT)
for _ in range(3):
self.table.append_row()
window = WorkoutTracker()
window.mainloop() One thing I don't like about this solution is that the rows prevent using a grid layout
Posts: 6,798
Threads: 20
Joined: Feb 2020
This treats the table row like a dictionary and uses a json file to save/restore the table.
import tkinter as tk
from tkinter import ttk
from datetime import datetime, timedelta
import json
class DatePicker(ttk.Combobox):
"""A widget for entering a date. Can type in or select from list."""
format = "%b %d, %Y"
def __init__(self, *args, date=None, days=10, **kwargs):
"""date sets current value. Defaults to today.
days is number of days in list surrounding current selection.
"""
super().__init__(*args, postcommand=self.postcommand, **kwargs)
date = datetime.today() if date is None else date
self.set(date.strftime(self.format))
self.days = days
def postcommand(self):
"""Update list to self.days before and after current date."""
date = datetime.strptime(self.get(), self.format)
self["values"] = [
(date + timedelta(days=d)).strftime(self.format)
for d in range(-self.days, self.days+1)
]
class TableRow(tk.Frame):
"""A row in a table. Get set values using properites."""
activities = ("Aerobics", "Cycling", "Running", "Swimming", "Walking")
def __init__(self, parent, *args, date=None, activity=None, minutes=None, **kwargs):
"""Create widgets that appear in a row"""
super().__init__(parent, *args, **kwargs)
self._values = {
"date": DatePicker(self, date=date, width=12),
"activity": ttk.Combobox(self, values=self.activities, width=10),
"minutes": tk.DoubleVar(parent, minutes if minutes is not None else 30.0)
}
self["activity"] = activity if activity is not None else self.activities[0]
for x in (
self._values["date"],
self._values["activity"],
tk.Entry(self, textvariable=self._values["minutes"], width=8, justify=tk.RIGHT),
tk.Button(self, text="X", command=lambda: parent.remove_row(self))
): x.pack(padx=2, side=tk.LEFT)
def __str__(self):
return str(self.values)
def __repr__(self):
return str(self)
def __iter__(self):
"""Key iterator."""
return iter(self._values)
def __getitem__(self, name):
"""Return field value."""
return self._values[name].get()
def __setitem__(self, name, value):
"""Set field value."""
self._values[name].set(value)
@property
def values(self):
"""Return field values as a dictionary."""
return {key: self[key] for key in self._values}
@values.setter
def values(self, new_values):
"""Set field values from new_values dictionary."""
for key, value in new_values.items():
self[key] = value
class Table(tk.Frame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
header = tk.Frame(self)
header.pack(side=tk.TOP, expand=1, fill="x")
tk.Label(header, text="Date", width=13).pack(side=tk.LEFT)
tk.Label(header, text="Activity", width=13).pack(side=tk.LEFT)
tk.Label(header, text="Minutes").pack(side=tk.LEFT)
self.rows = []
def append(self, values=None):
"""Add a row to the table. Values is a dictionary of values for the row."""
row = TableRow(self)
if values is not None:
row.values = values
print("<", ", ".join([f"{key} = {row[key]}" for key in row]), ">")
row.pack(side=tk.TOP)
self.rows.append(row)
return row
def remove(self, row):
"""Remove row with matching ID."""
if row in self.rows:
self.rows.remove(row)
row.destroy()
def clear(self):
"""Delete all rows"""
for row in self.rows:
row.destory()
self.rows = []
@property
def values(self):
"""Return list of row values. Row values are dictionaries."""
return [row.values for row in self.rows]
@values.setter
def values(self, new_values):
"""Reset table to show new_values. new_values is list of row values."""
self.clear()
for values in new_values:
self.append(values)
def __str__(self):
return "\n".join(str(row) for row in self.rows)
class WorkoutTracker(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filename = None
self.table = Table(self)
self.table.pack(side=tk.TOP)
buttons = tk.Frame(self)
buttons.pack(side=tk.TOP, expand=1, fill=tk.X)
self.append_btn = tk.Button(buttons, text="Add Activity", command=self.table.append)
self.append_btn.pack(side=tk.LEFT, padx=5, pady=5, expand=1, fill=tk.X)
self.print_btn = tk.Button(buttons, text="Print Activities", command=lambda: print(self.table))
self.print_btn.pack(side=tk.LEFT, padx=(0, 5), pady=5, expand=1, fill=tk.X)
self.protocol("WM_DELETE_WINDOW", self.on_close)
def on_close(self):
"""Dump values to json file."""
if self.filename:
with open(self.filename, "w") as file:
json.dump(self.table.values, file)
self.destroy()
def load(self, filename):
"""Load values from json file."""
self.filename = filename
try:
with open(self.filename, "r") as file:
self.table.values = json.load(file)
except:
pass
window = WorkoutTracker()
window.load("table.json")
window.mainloop()
|