Tkinter: Putting Code Into Class Breaks Python - python

I have this really simple and short few lines of code:
import Tkinter as tk
master = tk.Tk()
w = tk.Canvas(master, width=800, height=600)
w.pack(side="top", fill="both", expand=True)
master.mainloop()
For readibility, extended functionality, portability and other reasons I want to put the code into a class, like this:
class Example(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.create_window()
def create_window(self):
self.canvas = tk.Canvas(self, width=800, height=600)
self.canvas.pack(side="top", fill="both", expand=True)
if __name__ == "__main__":
app = Example()
app.mainloop()
The code runs fine, exactly like the one above, but when I stop the program I get a "Python has stopped working" message. It doesn't affect the program itself but I want to know what's causing it and why it runs perfectly outside a class.

Tkinter's Tk and other classes have hundreds of methods and attributes. I know that if one read ancient GUI programing books, from the times of Windows 3.x, the recommendation for creating your own Windows would be to subclass the underlying library widget and wrtite yoru code. That is what you are trying to do above, by subclassing Tk.
Well, do not do that. Really, don't. That way you can't create any attribute on your class without triple-checking if it is not colliding with some other attribute.
Use inhretance in OO to create your own class hierarchy - and that is a relevant and ordered thing that will make things easier to you. To inherit from the GUI code' Window, and attach members to it like "name_text_entry" and "name_variable" (and repeat that for each input control in your program) is a thing that is easily perceived as mixing very different things (Window control attributes and methods) with the internal workings of your own program. Here is a more extensive consideration on why this pattern does not work (not in Python, but I just picked it from the first Google results)
So, just create your own program, and leave Tkinter's classes alone - unless you are creating a new custom Widget (to be used in another part of the code).
Your code should be more like:
class Example(object):
def __init__(self, *args, **kw):
self.tk = tk.Tk(*args, **kw)
self.create_window()
def create_window(self):
self.canvas = tk.Canvas(self, width=800, height=600)
self.canvas.pack(side="top", fill="both", expand=True)
And just work.
The error message, as seen in the comments, is not deterministic, and can be yielded by lots of different factors - from tkinter.Tk internally depending on not being subclassed (even if that is a bug), to it having an internal "canvas" attribute, and so on.
Just use composition and be happy.

Related

Tkinter Widget References

I have a question about how to properly address widget attributes from outside the GUI main loop. Here is the gist of my project: I have an HMI, and it runs in its own thread. Outside of the HMI, there will be another thread that is collecting data and writing to hardware. I stripped out a bunch of stuff to create a basic example so you can see what I mean. It consists of a main window, a frame where the operator can select manual or auto, and a notebook that will display temperature probe data. In the real world, there will be varying numbers of probes and additional attributes, which is why I instantiate the probes at the top of the script. In real life, this would come from a SQL database and the GUI would be built dynamically based on database properties.
from tkinter import *
import tkinter as tk
from tkinter import ttk
from threading import Thread
from time import sleep
import random
wProbes= {1: {'Title': 'Indoors','Temperature':0,'MainObject':'','TempLabel':''},2: {'Title': 'Outdoors','Temperature':0,'Object':''}}
controlTemplate={'State':0,'On':0, 'Off':0}
systemControl = {'Auto':controlTemplate,'Manual':controlTemplate}
def gui():
class SystemOperation(tk.LabelFrame):
def __init__(self,parent):
super().__init__(parent)
self.config(text='System Operation',labelanchor='n',bd=2,highlightbackground='black',relief= 'solid',font=("arial", 20))
self.place(width=500, height=900,x=150,y=100)
def autoModeClick():
self.bAutoMode.configure(background='green')
self.bManualMode.configure(background='white')
systemControl['Auto']['State'] = 1
systemControl['Manual']['State'] = 0
def manualModeClick():
self.bManualMode.configure(background='green')
self.bAutoMode.configure(background='white')
systemControl['Auto']['State'] = 0
systemControl['Manual']['State'] = 1
self.bAutoMode = Button(self, text='Auto', background='green', command=autoModeClick)
self.bAutoMode.place(x=100, y=100, width=70)
self.bManualMode = Button(self, text='Manual', command=manualModeClick)
self.bManualMode.place(x=100, y=150, width=70)
class TemperatureProbe(tk.LabelFrame):
def __init__(self,parent,inst):
super().__init__(parent)
self.config(text=wProbes[inst]['Title'],labelanchor='n',bd=2,highlightbackground='black',relief= 'solid',font=("arial", 20))
self.place(width=500, height=300)
self.currentTemp=0
self.actTemp=Label(self,text='Temperature= %d F'%self.currentTemp,width=20)
self.actTemp.place(relx=.5, y=20, anchor='center')
self.pack_propagate(0)
wProbes[inst]['TempLabel']=self.actTemp
class probeNotebook(ttk.Notebook):
def __init__(self,parent):
super().__init__(parent)
self.place(x=900,y=100,width=800,height=700)
for t in sorted(wProbes.keys()):
tabName = wProbes[t]['Title']
self.tabFrame = tk.Frame(self, width=700, height=900, bg='white', borderwidth=2, relief='solid')
self.add(self.tabFrame, text=' %s '%tabName)
self.pFB=TemperatureProbe(self.tabFrame,t)
self.pFB.place(x=100, y=20)
wProbes[t]['MainObject']=self.pFB
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
self.geometry("800x800")
self.title('Test Window')
SystemOperation(self)
probeNotebook(self)
if __name__=="__main__":
app=MainWindow()
app.mainloop()
threadGUI = Thread(target=gui)
threadGUI.start()
while 1:
#A bunch of stuff will be happening here that interfaces with external hardware
if wProbes[1]['MainObject']!='':
randInt=random.randint(10,200)
wProbes[1]['TempLabel'].config(text='Temperature= %d F'%randInt)
wProbes[2]['TempLabel'].config(text='Temperature= %d F' % (randInt+200))
sleep(.5)
So, you can see that a notebook is created, and the tabs are then created based on the number of probes in the probe dictionary. Also in the probe dictionary, I set a reference to the probe container, and then the probe temperature label. If you run the code, you can see the temperatures change correctly based on the probe instances with a test random int.
But what I was really hoping to do is just use the probe MainObject reference and address its child widgets that way, i.e. wProbes[1]['MainObject'].actTemp, because in reality the probe container will have more widgets such as limits and setpoints and warnings. But if creating individual references is the correct way, I'm good with that too. Am I on the right track?
Another thing I wondered about as I go forward is how I can separate the GUI from the process script completely. Imagine the GUI as a standalone app, and a second app that acts like a server and does the heavy lifting and communicates with the GUI. Any basic hints on doing this? I'll do the work in figuring it out, but if you push me in the right direction I'd appreciate it.
Also, if there are criticisms of my project approach in general, I'm all ears. I've done quite a bit of python programming, but this is my first tkinter attempt. Thanks!
(Note: I hope it formatted correctly. I just copied and pasted into my ide, and it was fine. Worst case would be that the indentation is wrong in a couple of places when copied, but give it a try.)

Inheritance from Tkinter Frame in varying implementations

I'm fairly new to python and try to build a simple GUI following an object oriented approach. Therefor I let my widget classes inherit from tk.Frame and create an application controller to build the GUI.
My application contains the following two files:
mainModule.py
# coding: utf8
from testPackage import myGUI
import Tkinter as tk
root = tk.Tk() # Main window
my_gui = myGUI.MainApplication(root)
root.mainloop() # Hold window open until we close it
myGUI.py
# coding: utf8
import Tkinter as tk
# Application initializer
class MainApplication(tk.Frame):
def __init__(self, master):
self.master = master
tk.Frame.__init__(self, master)
self.configure_gui()
self.pack() # <--- CODE IN QUESTION
self.create_widgets()
def configure_gui(self):
self.master.title("Any title")
self.master.geometry('800x600')
self.master.minsize(600, 100)
def create_widgets(self):
self.main_window = MainWindow(self)
self.main_window.pack()
# Main Data Window
class MainWindow(tk.Frame):
def __init__(self, master):
self.master = master
tk.Frame.__init__(self, master)
self.main_window = tk.Label(self, text="This is a test")
self.main_window.pack(side="top", fill="x")
Initially running my code without self.pack() (marked as #Code in question) in the MainApplication class definition gave me only the root basic window without the Label created from MainWindow class.
As simple as the answer may be but why?
I used a few sources to get into the topic inbefore and some of them didn't use any geometry manager on the root window (or i'm to inexpierienced to see it. Source below for examples).
Source:
https://www.begueradj.com/tkinter-best-practices.html
http://python-textbok.readthedocs.io/en/latest/Introduction_to_GUI_Programming.html#putting-it-all-together
Tkinter example code for multiple windows, why won't buttons load correctly?
Only after reading following answers regarding widget creation I came aware of the missing part:
Best way to structure a tkinter application
creating a custom widget in tkinter
Figuring that pack() on initialization would be the same as
MainApplication(root).pack(side="top", fill="both", expand=True)
or
Example(root).place(x=0, y=0, relwidth=1, relheight=1)
for a grid based layout.
I guess it's a very simple or obvious element i don't see relating to inheritance but i'm not entirely sure why have to pack() the MainApplication Frame and furthermore why it seems to work for example without this step.
Because MainWindow inherits from Frame, it is itself a frame. If you never call pack, place, or grid on it, it will be invisible. This is no different than if you created a button or scrollbar or any other widget and then don't call one of those methods on it.
Since all of the other widgets are a children of this frame, they will be invisible since their parent is invisible.
Somewhat unrelated to the question being asked, self.pack() is a code smell. Generally speaking, a class should never call pack, place or grid on itself. This tightly couples itself to the caller (meaning, this widget has to know that the caller is using one of those methods).
In other words, if you decide that MainApplication wants to switch from pack to grid for all of its children, you can't just update MainApplication, you also have to update MainWindow. In this case, the root window has only one child so the problem is fairly small, but by doing it this way you are starting a bad practice that will eventually cause you problems.
The rule of thumb is that the function that creates a widget should be responsible for adding it to the screen. That means that it would be better to do it like this:
root = tk.Tk()
my_gui = myGUI.MainApplication(root)
my_gui.pack(fill="both", expand=True)
You would then need to remove self.pack() from MainApplication.__init__.
You also have some very misleading code that might be contributing to the confusion. Take a look at this code:
# Main Data Window
class MainWindow(tk.Frame):
def __init__(self, master):
...
self.main_window = tk.Label(self, text="This is a test")
self.main_window.pack(side="top", fill="x")
Notice how you have a class named MainWindow. Within that you have a label that is named self.main_window, but self.main_window is not a MainWindow. This is especially confusing since the function that creates MainWindow also creates an instance variable named self.main_window.
You might want to consider renaming this label to be something else (eg: self.label, self.greeting, etc).

Displaying only one frame at a time in tkinter

I've been struggling with this for a while. I think I'm missing some simple piece of information and I hope you guys can help clear this up for me.
I'm trying to get tkinter to display different frames which I will eventually place widgets inside of. Here's what I did:
I've made a class that is supposed to initialize the window and make all the different frames the program will run.
I've made a separate class for each frame(I'm intending to have variables associated with the different classes when the program is done), and assigned a variable that will start that class up and make it run it's init function
I ended the StartUp class by telling it to tkraise() the frame I want displayed, and that's where things stop working correctly.
I set each frame to a different color, so when you run this program you will see that they split the screen space up instead of one being raised to the top. What am I missing?
One last point, I am purposely trying to spell everything out in my program, I learn better that way. I left it so I have to type tkinter.blah-blah-blah in front of each tkinter command so I can recognize them easily, and I decided not to have my classes inherit Frame or Tk or anything. I'm trying to understand what I'm doing.
import tkinter
class StartUp:
def __init__(self):
self.root = tkinter.Tk()
self.root.geometry('300x300')
self.container = tkinter.Frame(master=self.root, bg='blue')
self.container.pack(side='top', fill='both', expand=True)
self.page1 = Page1(self)
self.page2 = Page2(self)
self.page1.main_frame.tkraise()
class Page1():
def __init__(self, parent):
self.main_frame = tkinter.Frame(master=parent.container, bg='green')
self.main_frame.pack(side='top', fill='both', expand=True)
class Page2():
def __init__(self, parent):
self.main_frame = tkinter.Frame(master=parent.container, bg='yellow')
self.main_frame.pack(side='top', fill='both', expand=True)
boot_up = StartUp()
boot_up.root.mainloop()
When you do pack(side='top', ...), top doesn't refer to the top of the containing widget, it refers to the top of any empty space in the containing widget. Page initially takes up all of the space, and then when you pack Page2, it goes below Page1 rather than being layered on top of it.
If you are using the strategy of raising one window above another, you need to either use grid or place to layer the widgets on top of each other. The layering is something pack simply can't do.
Your other choice is to call pack_forget on the current window before calling pack on the new windowl

Python: why must Tkinter class instantiation use a Frame?

If I want to create a Tkinter GUI simply with statements, I can do this:
from Tkinter import *
root = Tk()
root.title("Test Window")
tkFrame = Frame(root)
tkButton = Button(tkFrame)
[...]
The documentation, however, advises that Tkinter be used with a class definition, subclassing a Frame widget:
class App(Frame):
[...]
I would like to understand why that is so. Why can't we subclass the Frame's container, the window? It appears that is what is done with statements in the first example, so why not in a class definition?
EDIT (following Bryan Oakley's answer):
I would like to instantiate at the highest level of Tkinter, which I assume to be Tk() (though I have come across references stating Frame is the top level, but never mind). Indeed, the following will create a window:
from Tkinter import *
class Application(Tk):
pass
app = Application()
app.mainloop()
...but as soon as I try to add widgets I either get errors or two windows, with the widgets in a new window, depending on how I structure the code. Here's a basic example that will produce a second window with the button:
from Tkinter import *
class Application(Tk):
tkBtn = Button()
tkBtn.pack()
app = Application()
app.mainloop()
Anything more, using self, __init__, etc., produces errors. Could someone point me to working code that instantiates Tkinter at the highest level? Just like all the Frame subclasses I'm seeing, but at the highest level?
There is nothing that says a tkinter class must inherit from a frame. You can inherit from any of the tkinter widgets, or any other classs. If you have found documentation that states otherwise, that documentation is wrong. Using Frame is a logical choice since it is designed to be a container of other widgets, but it is not the only choice.
Personally I inherit from a frame because I find it convenient. Some of my GUIs need the ability to open more than one identical window. By having my main code in a Frame I am able to create multiple windows simply by creating multiple instances of the frame, and packing them in Toplevel widgets.
When you inherit from Tk, you can only have a single instance. In the real world that's usually enough, and there's absolutely nothing wrong with doing it that way. Since I personally write a fair number of tkinter programs, having them all start out exactly the same is convenient for me.
Another good choice is a Canvas since you can easily add a background image, which is not something you can do with a Frame.
Bottom line: you are absolutely not required to inherit from Frame. Inherit from whatever you want.
(the following was written in response to an edit of the original question)
In reference to this code:
from Tkinter import *
class Application(Tk):
tkBtn = Button()
tkBtn.pack()
app = Application()
app.mainloop()
The reason you see two windows is that you're not creating the class properly. You need to call the __init__ method of the superclass before creating widgets, because that's what actually creates the root window. Because you don't, you end up with two windows. You get one that is created implicitly when you add a button to a not-yet-constructed root window, and you get another when your subclass finishes initializing.
The solution is to not take shortcuts, and instead initialize the class properly:
from Tkinter import *
class Application(Tk):
def __init__(self):
Tk.__init__(self)
tkBtn = Button()
tkBtn.pack()
app = Application()
app.mainloop()
Note that this isn't a tkinter-specific problem. When subclassing, unless you have explicit reasons to do otherwise, you always should call the __init__ method of the superclass.
You asked for working examples, here are a couple:
https://stackoverflow.com/a/22424245/7432
https://stackoverflow.com/a/11405393/7432
You might also want to read the responses in the question Inheriting from Frame or not in a Tkinter application

tkinter and GUI programming methods

Hopefully this doesn't fall under "general discussion topic", since I'd like it to be more about resolving these issues in an efficient manner than a giant debate about which general approach to GUI programming is the absolute best.
So I've started some GUI programming with tkinter and long story short my code is getting pretty ugly pretty quickly. I'm trying to create a tile-based map editor for a video game. My main issues seem to be:
the inability of callbacks to return values.
the inability to transfer data between windows easily.
I assume that the reason I see these as issues is because I'm using functions a lot more than I'm using classes. For instance, my "load tileset" window is handled entirely functionally: Clicking the menu option in the main window calls the function that loads the new window. From within that window, I create an open file dialog when looking for the image, and modify the canvas displaying the image when I press the enter key (so that it draws the appropriate grid over the image). function function function.
What looks like really bad practice to me is the inclusion of extra arguments to compensate. For example, when I create a tileset, the instance of the TileSet class created should be sent back to the main window where the appropriate information can be displayed. I have a list of loaded tilesets as a global variable (even more bad practice: Everything dealing with my root window is in the global scope! yay!), and because callback functions don't return values, I pass that list as an argument to my "load tileset window" function, which then passes the argument to the create tileset function (called when you click the appropriate button in the window), where it's actually needed so that I can add my newly created tileset to the list. Passing arguments through a function 'hierarchy' like that seems like a horrible idea. It gets confusing, it's horrible for writing modular code, and just generally seems unnecessary.
My attempt at fixing the problem would be to write a class representing the whole GUI, and custom made window classes (that the GUI class can create and reference) that can actually store relevant data. That should take care of issues with transferring data between windows. Hopefully it would cut down on my gratuitous use of lambda functions in callbacks as well.
But I'm wondering: is this the best way? Or at least close? I'd rather not start rewriting and then end up with another system that's just sloppy and confusing in a different way. I know my methods are bad, but I don't really know what the best approach would be. I'm getting a lot of advice on how to do specific things, but none on how to structure the program as a whole. Any help would be greatly appreciated.
It sounds like you're trying to create a GUI that acts procedurally, which won't work. GUIs aren't procedural, their code doesn't run linearly where functions call callbacks which return values. What you're asking isn't unique to tkinter. This is the nature of event based GUI programming -- callbacks can't return anything because the caller is an event rather than a function.
Roughly speaking, you must use a global object of some sort to store your data. Typically this is called the "Model". It can be a global variable, or it might be a database, or it can be an object of some sort. In any case, it must exist "globally"; that is, it must be accessible to the whole GUI.
Often, this access is provided by a third component called a "Controller". It is the interface between the GUI (the "View") and the data (the "Model"). These three components make up what is called the model-view-controller pattern, or MVC.
The model, view and controller don't have to be three different objects. Often, the GUI and the controller are the same object. For small programs this works quite well -- the GUI components talk directly to your data model.
For example, you could have a class that represents a window which inherits from Tkinter.Toplevel. It can have an attribute that represents the data being edited. When the user selects "New" from a main window, it does something like self.tileset = TileSet(filename). That is, it sets the attribute named tileset of the GUI object named self to be an instance of the TileSet class specific to the given filename. Later functions that manipulate the data use self.tileset to access the object. For functions that live outside the main window object (for example, a "save all" function from the main window) you can either pass this object as an argument, or use the window object as the controller, asking it to do something to its tileset.
Here's a brief example:
import Tkinter as tk
import tkFileDialog
import datetime
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.windows = []
menubar = tk.Menu(self)
self.configure(menu=menubar)
fileMenu = tk.Menu(self)
fileMenu.add_command(label="New...", command=self.new_window)
fileMenu.add_command(label="Save All", command=self.save_all)
menubar.add_cascade(label="Window", menu=fileMenu)
label = tk.Label(self, text="Select 'New' from the window menu")
label.pack(padx=20, pady=40)
def save_all(self):
# ask each window object, which is acting both as
# the view and controller, to save it's data
for window in self.windows:
window.save()
def new_window(self):
filename = tkFileDialog.askopenfilename()
if filename is not None:
self.windows.append(TileWindow(self, filename))
class TileWindow(tk.Toplevel):
def __init__(self, master, filename):
tk.Toplevel.__init__(self, master)
self.title("%s - Tile Editor" % filename)
self.filename = filename
# create an instance of a TileSet; all other
# methods in this class can reference this
# tile set
self.tileset = TileSet(filename)
label = tk.Label(self, text="My filename is %s" % filename)
label.pack(padx=20, pady=40)
self.status = tk.Label(self, text="", anchor="w")
self.status.pack(side="bottom", fill="x")
def save(self):
# this method acts as a controller for the data,
# allowing other objects to request that the
# data be saved
now = datetime.datetime.now()
self.status.configure(text="saved %s" % str(now))
class TileSet(object):
def __init__(self, filename):
self.data = "..."
if __name__ == "__main__":
app = SampleApp()
app.mainloop()

Categories