I am building a data-analysis program using Python and Tkinter that allows for the entry, plotting and export of data (an Excel clone of sorts). It implements many windows and functions that need to access and modify shared data variables. How should I organize the GUI in order to ensure that each window can transfer data back and forth, while also compartmentalizing the code to be able to modify and debug each function independently?
I initially built a main GUI class with sub-functions (Example 1), where it is easy for all of the functions to modify and access the data initialized, as all variables are shared through self. However, as I increasingly modify the code (which is actually thousands of lines long), it became challenging to debug as the code isn't compartmentalized well.
I saw a suggestion to have each window (ie data table, plot window, etc) be independent classes (Example 2), but the only way I figured out how to pass the data between classes (passing the first window object to the other windows) seems very messy, and the only alternative I could figure out was explicitly passing all necessary variables to each window at the definition/call, which can also be very messy.
Example 1
class GUI:
def __init__(self, toptk):
self.toptk = toptk
""" Code creating main window and toolbar """
list_x = []
list_y = []
""" Code adding data """
def entryTable(self):
tablewin = Toplevel(self.toptk)
""" Code creating frames, buttons, etc """
""" Code doing something to list_x, list_y """
def plotWin(self):
plotwin = Toplevel(self.toptk)
""" Code creating frames, buttons, etc """
""" Code doing something to list_x, list_y """
entryTable()
plotWin()
root = tk.Tk()
main = GUI(root)
root.mainloop()
Example 2
class GUI:
def __init__(self, toptk):
""" Code creating main window and toolbar """
list_x = []
list_y = []
""" Code adding data """
entryTable.__init__(toptk,self)
plotWin.__init__(toptk,self)
class entryTable():
def __init__(self,toptk,topGUI):
tabletop = Toplevel(toptk)
""" Code creating frames, buttons, etc """
""" Code doing something to topGUI.list_x, topGUI.list_y """
class plotWin():
def __init__(self,toptk,topGUI):
plottop = Toplevel(toptk)
""" Code creating frames, buttons, etc """
""" Code doing something to topGUI.list_x, topGUI.list_y """
How can I improve the organization of this multi-window program in a way that retains a simple exchange of variables between classes while also allowing me to isolate and debug each one individually?
Thank you very much.
Question: how to pass the data between classes
This solution don't pass anything beteen classes, it simple uses a global class DATA object. It's also possible to hold this object in a own DATA.py, to do import DATA.
import tkinter as tk
class DATA:
""" Data container """
list_x = []
list_y = []
class EntryTable(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
""" Code creating frames, buttons, etc """
""" Code adding something to DATA.list_x, DATA.list_y """
DATA.list_x = [1, 2, 3]
DATA.list_y = [4, 5, 6]
class PlotWin(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
""" Code creating frames, buttons, etc """
""" Code doing something with DATA.list_x, DATA.list_y """
print('plot:{}'.format((DATA.list_x, DATA.list_y)))
class App(tk.Tk):
def __init__(self):
super().__init__()
""" Code adding main window widgets and toolbar """
""" Code adding Toplevel window's """
entryTable = EntryTable(self)
plotWin = PlotWin(self)
if __name__ == '__main__':
App().mainloop()
Related
I am trying to transform my procedural-programming Python project to object oriented programming.
In my project I am using Tkinter.
The procedural version worked just fine, but in OOP I get the
line 2493, in grid_configure
self.tk.call(
_tkinter.TclError: can't invoke "grid" command: application has been destroyed
error right when I try to grid the first labels.
My code:
from tkinter import *
class Document:
root = Tk()
root.geometry("1000x500")
file_name = Label(text="File Name")
document_title = Label(text="Document Title")
def gridding(self):
self.file_name.grid(row=1,column=2)
self.document_title.grid(row=2,column=2)
root.mainloop()
doc1 = Document()
doc1.gridding()
The error message is not helping at all, so hopefully this post will help others as well.
Many thanks for your help beforehand.
There are a lot of things to consider about your code, and I will address all of them.
You are importing all of tkinter without any alias
Calling your root "doc1" implies that you think you will be making a "doc2", "doc3", etc.. out of that class, and that is not going to work. You can't have many root instances. At best, that should be Toplevel, but only if you intend to open a new window for every new document.
You shouldn't be calling mainloop inside the class, at all. You should be using module conventions. This way, you can import elements of this script into another script without worrying about this script running things automatically. At the very least, creating a wrapper for mainloop is silly. You can already call mainloop, what is the purpose of sticking it inside another method? (doc1.root.mainloop())
Creating a method to do very specific grid placement isn't reusable. I would argue that a mixin that makes certain features more usable, but keeps them dynamic would be a better approach. It isn't necessary to create a custom wrapper for widgets, but if you are going to put a gridding method on everything then why not make a better gridding method and have it automatically "mixed in" to everything that applies? And as long as you go that far you might as well include some other conveniences, as well.
Something like Document (ie, something where you may need many or may change often) likely shouldn't be the root. It should be IN the root. It certainly shouldn't have the root buried in it as a property.
Below gives numerous examples of the things I stated above. Those examples can and should be embellished. For instance, BaseWidget could include properties for x, rootx, y, rooty etc... The way ParentWidget is designed, you can use a range as the first argument of rowcfg and colcfg to do "mass configuring" in one call. They both return self so, they can be used inline.
import tkinter as tk
from typing import Iterable
class BaseWidget:
#property
def width(self) -> int:
self.update_idletasks()
return self.winfo_width()
#property
def height(self) -> int:
self.update_idletasks()
return self.winfo_height()
def grid_(self, r=None, c=None, rs=1, cs=1, s='nswe', **kwargs):
#this allows keyword shorthand, as well as original keywords
#while also allowing you to rearrange the above arguments
#so you know the exact order they appear and don't have to use the keyword, at all
self.grid(**{'row':r, 'column':c, 'rowspan':rs, 'columnspan':cs, 'sticky':s, **kwargs})
#return self so this can be used inline
return self
class ParentWidget:
#property
def descendants(self):
return self.winfo_children()
#inline and ranged grid_rowconfigure
def rowcfg(self, index, **options):
index = index if isinstance(index, Iterable) else [index]
for i in index:
self.grid_rowconfigure(i, **options)
#so this can be used inline
return self
#inline and ranged grid_columnconfigure
def colcfg(self, index, **options):
index = index if isinstance(index, Iterable) else [index]
for i in index:
self.grid_columnconfigure(i, **options)
#so this can be used inline
return self
class Custom_Label(tk.Label, BaseWidget):
#property
def text(self) -> str:
return self['text']
#text.setter
def text(self, value:str):
self['text'] = value
def __init__(self, master, **kwargs):
tk.Label.__init__(self, master, **kwargs)
class Document(tk.Frame, ParentWidget, BaseWidget):
def __init__(self, master, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
#the rest of this class is an example based on what little code you posted
#the results are not meant to be ideal. It's a demo of usage ... a gist
self.file_name = Custom_Label(self, text='File Name').grid_(1,2)
self.doc_title = Custom_Label(self, text='Document Title').grid_(2,2)
#possible
#r = range(len(self.descendants))
#self.colcfg(r, weight=1).rowcfg(r, weight=1)
self.colcfg(2, weight=1).rowcfg([1,2], weight=1)
class Root(tk.Tk, ParentWidget):
def __init__(self, title, width, height, x, y, **kwargs):
tk.Tk.__init__(self)
self.configure(**kwargs)
self.title(title)
self.geometry(f'{width}x{height}+{x}+{y}')
self.rowcfg(0, weight=1).colcfg(0, weight=1)
doc1 = Document(self).grid_()
if __name__ == '__main__':
Root('MyApplication', 800, 600, 200, 200, bg='#000000').mainloop()
You are almost there: you should build the class in the __init__ method, and only call mainloop at the end of the setup:
from tkinter import Tk
from tkinter import Label
class Document:
def __init__(self):
self.root = Tk()
self.root.geometry("1000x500")
self.file_name = Label(text="File Name")
self.document_title = Label(text="Document Title")
self.gridding()
def gridding(self):
self.file_name.grid(row=1, column=2)
self.document_title.grid(row=2, column=2)
def start(self):
self.root.mainloop()
doc1 = Document()
doc1.start()
This creates a window with two labels, as expected.
Cheers!
The most obvious approach for me is to declare each window (window, dialog or widget) in the constructor and call the show() method when needed. Something like this:
class MultiWindowApp():
def __init__(self):
self.window_1 = self.init_window_1()
self.window_2 = self.init_window_2()
def init_window_1(self):
gui = uic.loadUi(...)
# other settings
return gui
def init_window_2(self):
gui = uic.loadUi(...)
# other settings
return gui
def show_window_1(self):
self.window_1.show()
def show_window_2(self):
self.window_2.show()
Nevertheless, it does not seem to be memory efficient, because I store the windows in the memory all the time, even when I am not showing it.
Alternative solution that comes to my mind is to create a separate class for each window (or other widget) and have one placeholder for all in the main class. Assign an instance of the respective class and delete on closing the window. A minimal example below:
class Window_1(QWidget):
def __init__(self):
QWidget.__init__(self)
uic.loadUi(...)
# other settings
self.show()
class Window_2(QWidget):
def __init__(self):
QWidget.__init__(self)
uic.loadUi(...)
# other settings
self.show()
class MultiWindowApp():
def __init__(self):
self.widget_placeholder = None
def show_window_1(self):
self.widget_placeholder = Window_1()
def show_window_2(self):
self.widget_placeholder = Window_1()
This would be a bit slower, but I would avoid keeping in memory unnecessary stuff. Nevertheless, I still have a feeling that there is a better way. What is the proper way of designing such an application?
I didn't run the examples above, so there can be some errors, but I think that the concepts behind them are clear.
I am writing a class that I want to include multiple widgets that can be displayed in a Jupyter notebook. These widgets should calls class methods that update class parameters. A function that I connect to an ipywidget's events need access to the class instance, I think through self, but I can't figure out how to get this communication to work.
Here's a minimal example:
import numpy as np
import ipywidgets as widgets
class Test(object):
def __init__(self):
self.val = np.random.rand()
display(self._random_button)
_random_button = widgets.Button(
description='randomize self.val'
)
def update_random(self):
self.val = np.random.rand()
print(self.val)
def button_pressed(self):
self.update_random()
_random_button.on_click(button_pressed)
I see how the button_pressed() function sees the Button instance as self, giving "AttributeError: 'Button' object has no attribute 'update_random'".
Is there a way that I can access methods of the class Test through a button that belongs to the class, or is there a better way that I should be structuring this code to ease communication between these components?
The button widget and the on_click should be created (or initialised) in the init method.
The on_click method generates an argument that is sent to the function, but it is not needed in this case so I have just put a *args in the button_pressed function.
The display call is not needed.
When calling a function in a class, you must use self.functionName. That includes the function calls in on_click or observe
In this case, you didn't need the random number generation in the init function.
There are a few examples of Jupyter widgets within classes here: https://github.com/bloomberg/bqplot/tree/master/examples/Applications
import numpy as np
import ipywidgets as widgets
class Test(object):
def __init__(self):
self.random_button = widgets.Button(
description='randomize self.val')
self.random_button.on_click(self.button_pressed)
def update_random(self):
self.val = np.random.rand()
print(self.val)
def button_pressed(self,*args):
self.update_random()
buttonObject = Test()
# display(buttonObject.random_button) # display works but is not required if on the last line in Jupyter cell.
buttonObject.random_button # Widget to be dispalyed - must last last line in cell
When using JupyterLab, if you want the output to show in the notebook cell, rather than in the notebook log, a minor tweak is needed to #DougR's excellent answer:
import numpy as np
import ipywidgets as widgets
# create an output widget
rand_num_output = widgets.Output()
class Test(object):
def __init__(self):
self.random_button = widgets.Button(
description='randomize self.val')
self.random_button.on_click(self.button_pressed)
def update_random(self):
# clear the output on every click of randomize self.val
rand_num_output.clear_output()
# execute function so it gets captured in output widget view
with rand_num_output:
self.val = np.random.rand()
print(self.val)
def button_pressed(self,*args):
self.update_random()
buttonObject = Test()
# display(buttonObject.random_button) # display works but is not required if on the last line in Jupyter cell.
widgets.HBox([buttonObject.random_button, rand_num_output]) # Widget to be dispalyed - must last last line in cell, add output widget
I have two separate columns of checkbuttons in tkinter in python with identical labels.
My goal is to disable those in one of the columns when the same item is selected in another column.
I can do this outside of classes and functions, but am having trouble calling the variables within the functions.
When I plug in static values for the dynamic parameters and remove the loop on the command function creation to disable the buttons, it works perfectly. Whenever I use the loop, however, I receive an "AttributeError: 'tool' object has no attribute 'dsblr0'" response.
How can I access the loop-created functions, such that I can disable the boxes in the second column whenever the same items are checked in the first column?
Any help is greatly appreciated!
My current code:
from tkinter import *
from tkinter import messagebox
buttonnames = []
class tool(Frame):
def get_button_names(self):
self.buttonnames=['b1','b2','b3','b4','b5']
global buttonnames
for item in self.buttonnames:
buttonnames.append(item)
def __init__(self, parent):
'''
Constructor
'''
Frame.__init__(self, parent)
self.parent=parent
self.get_button_names()
self.display_new_window()
for i in range(len(buttonnames)):
exec('''def dsblr{0}(self):
self.bcb{0}.config(state=DISABLED if self.var{0}.get() else NORMAL)'''.format(i,))
def display_new_window(self):
""" Transpose tools
"""
self.parent.title("Tool")
self.parent.grid_rowconfigure(0, weight=1)
self.parent.grid_columnconfigure(0, weight=1)
for i,column in enumerate(self.buttonnames):
exec('self.var{0}=IntVar()'.format(i,))
exec('self.bvar{0}=IntVar()'.format(i,))
exec('self.cb{0} = Checkbutton(self.parent, text=column, variable=self.var{0},command=self.dsblr{0})'.format(i,))
exec("self.cb{0}.grid(row=i+1,column=0,sticky='w')".format(i,))
exec('self.bcb{0} = Checkbutton(self.parent, text=column, variable=self.bvar{0})'.format(i,))
exec("self.bcb{0}.grid(row=i+1,column=1,sticky='w')".format(i,))
def main():
root=Tk()
d=tool(root)
root.mainloop()
if __name__=="__main__":
main()
Goal of the script:
(3) different windows, each in its own class, with its own widgets and layout, are created via Toplevel and callbacks.
When a new (Toplevel) window is created, the previous one is destroyed. Thus, only one window is visible and active at a time.
Problem?
Basically, I've tried many things and failed, so I must understand too little of ["parent", "master", "root", "app", "..."] :(
Note on raising windows:
I have implemented a successful example of loading all frames on top of each other, and controlling their visibility via the .raise method.
For this problem, however, I don't want to load all the frames at once.
This is an abstracted version of a quiz program that will require quite a lot of frames with images, which makes me reluctant to load everything at once.
Script (not working; bugged):
#!/usr/bin/env python
from Tkinter import *
import tkMessageBox, tkFont, random, ttk
class First_Window(Frame):
"""The option menu which is shown at startup"""
def __init__(self, master):
Frame.__init__(self, master)
self.gotosecond = Button(text = "Start", command = self.goto_Second)
self.gotosecond.grid(row = 2, column = 3, sticky = W+E)
def goto_Second(self):
self.master.withdraw()
self.master.update_idletasks()
Second_Window = Toplevel(self)
class Second_Window(Toplevel):
"""The gamewindow with questions, timer and entrywidget"""
def __init__(self, *args):
Toplevel.__init__(self)
self.focus_set()
self.gotothird = Button(text = "gameover", command = self.goto_Third)
self.gotothird.grid(row = 2, column = 3, sticky = W+E)
def goto_Third(self):
Third_Window = Toplevel(self)
self.destroy()
class Third_Window(Toplevel):
"""Highscores are shown with buttons to Startmenu"""
def __init__(self, *args):
Toplevel.__init__(self)
self.focus_set()
self.master = First_Window
self.gotofirst = Button(text = "startover", command = self.goto_First)
self.gotofirst.grid(row = 2, column = 3, sticky = W+E)
def goto_First(self):
self.master.update()
self.master.deiconify()
self.destroy()
def main():
root = Tk()
root.title("Algebra game by PJK")
app = First_Window(root)
root.resizable(FALSE,FALSE)
app.mainloop()
main()
The problem is not really a Tkinter problem, but a basic problem with classes vs. instances. Actually, two similar but separate problems. You probably need to read through a tutorial on classes, like the one in the official Python tutorial.
First:
self.master = First_Window
First_Window is a class. You have an instance of that class (in the global variable named app), which represents the first window on the screen. You can call update and deiconify and so forth on that instance, because it represents that window. But First_Window itself isn't representing any particular window, it's just a class, a factory for creating instances that represent particular windows. So you can't call update or deiconify on the class.
What you probably want to do is pass the first window down through the chain of windows. (You could, alternatively, access the global, or do various other things, but this seems cleanest.) You're already trying to pass it to Second_Window, you just need to stash it and pass it again in the Second_Window (instead of passing self instance, which is useless—it's just a destroyed window object), and then stash it and use it in the Third_Window.
Second:
Second_Window = Toplevel(self)
Instead of creating an instance of the Second_Window class, you're just creating an instance of the generic Toplevel class, and giving it the local name Second_Window (which temporarily hides the class name… but since you never use that class, that doesn't really matter).
And you have the same problem when you try to create the third window.
So:
class First_Window(Frame):
# ...
def goto_Second(self):
# ...
second = Second_Window(self)
class Second_Window(Toplevel):
def __init__(self, first, *args):
Toplevel.__init__(self)
self.first = first
# ...
def goto_Third(self):
third = Third_Window(self.first)
self.destroy()
class Third_Window(Toplevel):
"""Highscores are shown with buttons to Startmenu"""
def __init__(self, first, *args):
Toplevel.__init__(self)
self.first = first
# ...
def goto_First(self):
self.first.update()
self.first.deiconify()
self.destroy()