tkinter two windows instead of one - Printable Version +- Python Forum (https://python-forum.io) +-- Forum: Python Coding (https://python-forum.io/forum-7.html) +--- Forum: GUI (https://python-forum.io/forum-10.html) +--- Thread: tkinter two windows instead of one (/thread-41528.html) |
tkinter two windows instead of one - jacksfrustration - Jan-31-2024 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? RE: tkinter two windows instead of one - deanhystad - Jan-31-2024 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() RE: tkinter two windows instead of one - jacksfrustration - Feb-03-2024 (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: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 RE: tkinter two windows instead of one - deanhystad - Feb-03-2024 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. RE: tkinter two windows instead of one - jacksfrustration - Feb-05-2024 (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. 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() RE: tkinter two windows instead of one - deanhystad - Feb-05-2024 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-=1You 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. RE: tkinter two windows instead of one - deanhystad - Feb-06-2024 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 RE: tkinter two windows instead of one - deanhystad - Feb-08-2024 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() |