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!
Related
For a couple of days I've been attempting to pass an instance of a variable from one of my classes to another, and until recently I have not been able to get it to work. I've read this document regarding classes and instances, I've searched online and stackoverflow and I couldn't get an answer until I tried a solution from a previous asked question and it solved my problem. However, I'm not exactly sure on the why and the how it works.
My code goes as follows, :
from tkinter import *
def main():
main_window = Tk()
app = First(main_window)
main_window.mainloop()
class First:
def __init__(self, root):
self.root = root
self.root.title('First Window')
self.entry1 = Entry(self.root)
self.entry1.pack()
btn = Button(self.root, text="click", command=self.clicked)
btn.pack()
def clicked(self):
writeDoc(First=self)
class writeDoc:
def __init__(self, First):
self.First = First
txt = self.First.entry1.get()
print(txt)
if __name__ == '__main__'
main()
Under the class writeDoc whenever I hover over the parameter First it is not a reference to the Class First, so I am unsure how it has access to entry1. The same goes for the First within writeDoc(First=self). Addtionally, why does writeDoc(First=self) work but not writeDoc(self.First)?
I assumed when I want to reference to another class and want to pass its instances over I would need to do
class First:
....
def clicked(self):
writeDoc(First)
....
class writeDoc(First):
def __init__(self, First):
super().__init__(First):
...
But, this didn't work for me at all.
I hope this isn't a loaded/jumbled question. I want to understand how this works to create better code and avoid misunderstanding how python classes works.
Inside the clicked() method, self refers to an instance of the First class. It then does
writeDoc(First=self)
which passes that instance to the writeDoc class's __init__() method, as the value of its First parameter. This is no different from passing any other object as a method parameter.
writeDoc(self.First) doesn't work because there's no First attribute in the self instance. First is the name of the class, not an attribute.
Well, here turning to the forum for help. I have a routine in which I import module dos.py, and I need module dos.py (toplevel) to always be in front of my main screen uno.py. I cannot get transient to work, previously I had no problems because I did not need the screen that is always present, but now I need my screen dos.py to be always ahead of one.py.
Here I have the problem, I do not apply transient when it comes to importing modules
I leave the routine very short to understand how transient is handled, thank you.
# uno.py
import tkinter as tk
class PRUEBA:
def __init__(self):
ventana_principal = tk.Tk()
ventana_principal.geometry ("600x600")
ventana_principal.config (bg="blue")
ventana_principal.title ("PANTALLA PRINCIPAL")
def importar():
from dos import toplevel
toplevel()
boton = tk.Button (ventana_principal , text = "boton" , command = importar)
boton.pack ( )
ventana_principal.mainloop()
PRUEBAS = PRUEBA ()
**module dos.py
#dos.py
import tkinter as tk
class toplevel:
def __init__(self):
secundario = tk.Toplevel ()
secundario.geometry ("150x40+190+100")
secundario.resizable(0,0)
secundario.transient(self.master) """ Here I have the problem not to apply transient
when it comes to importing modules """
sorry
Your code will raise exception:
AttributeError: 'toplevel' object has no attribute 'master'
The line secundario.transient(self.master) should be secundario.transient(secundario.master) instead.
But the instance of toplevel is useless because you cannot access secundario later as it is a local variable.
Suggest to make toplevel inherit from tk.Toplevel:
class toplevel(tk.Toplevel):
def __init__(self, parent, *args, **kw):
super().__init__(parent, *args, **kw)
self.geometry("150x40+190+100")
self.resizable(0, 0)
self.transient(parent)
And then create it like:
top = toplevel(ventana_principal)
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.
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()
Okay, i've been trying to create a small text editor in Tkinter. I've stubble across a problem and I can't seem to find the answer. If anyone could just help me, I'd be very happy.
First of all, here is my code :
import tkinter as tk
import tkinter.filedialog as tkfile
class PyTedi(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
# Instantiate Menu
main_menu = tk.Menu(self)
menu_bar = PyTediMenu(main_menu)
main_menu.add_cascade(label='File', menu=menu_bar)
self.config(menu=main_menu)
# Instantiate Text Area
text_area = PyTediTextArea(self)
text_area.pack(side=tk.BOTTOM)
# Instantiate Tool Bar
tool_bar = PyTediToolBar(self)
tool_bar.pack(side=tk.TOP)
class PyTediMenu(tk.Menu):
def __init__(self, parent):
tk.Menu.__init__(self, parent)
self.add_command(label='New', command=None)
self.add_command(label='Open', command=None)
self.add_command(label='Save', command=tkfile.asksaveasfile)
self.add_separator()
self.add_command(label='Exit', command=self.quit)
class PyTediToolBar(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent, height=30)
class PyTediTextArea(tk.Text):
def __init__(self, parent):
tk.Text.__init__(self, parent)
if __name__ == '__main__':
app = PyTedi()
app.mainloop()
Basically, I've found out, (From another stack question) That it is a good idea to create class based components... My problem is, let's say I want to create a command -> Save File. So I create a method inside my Menu and link to the save function. BUT, how do I grab the text area content and write it to a file ? They are not even part of the same class. Is it a bad design implementation or it's just me ?
Thanks !
while it is a good idea to use class based programming, i would like to point out that unless you are modifying the widget in some way, subclassing it is completely unnecessary, when you create the class PyTediTextArea you aren't actually modifying the original text class in any way, so it would be simpler for you to simply change
text_area = PyTediTextArea(self)
to
self.text_area = tk.Text(self)
that way you save yourself subclassing at the bottom and from anywhere in your main class you can simply call
self.text_area.get(0, "end")
to get all of the text in the widget
James