Tkinter Toplevel in OOP script: how? - python

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()

Related

Python - Tkinter and OOP

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!

Python tkinter - calling a button command in a class

I'm working on functions within a class, and one of the issues I'm running into is adding a button that terminates the program. Here is the current code:
class ClassName():
def __init__(self, root):
self.root = root
def close_root(self, root):
root.destroy()
root.quit()
def addExitButton(self, root):
Button(root, text = 'Exit', width = 10, command = self.close_root).grid(row = 5,
column = 0)
Within the button arguments, I have tried command = self.close_root(root) But this gives me an error because you can't call a function if you want the button to do something (I forget the reason as to why this is). I have also tried
def close_root(self):
self.destroy()
self.quit()
def addExitButton(self, root):
Button(..., command = self.close_root,...)
And this does not work either as the class doesn't have the attribute destroy. I'm not sure how to approach this after trying a few different ways.
You need to actually access the root's functions. So using self.root.destory() or self.root.quit() will work because your root object has those methods but your class does not.
You should also only use one of them, in this case, destroy is the best option I think. And you can probably just use that when creating the button. So replace the button callback (command) with self.root.destory.
More here on how to close a Tkinter window here.

Python3 PyQt4 Calling function from QPushButton

I try to achieve a very simple thing in PyQT4 (python 3.3) :
Calling a new window from a button.
So far I have a 2nd Class (my second window, empty for now)...:
class Window2(QtGui.QWidget):
def __init__(self):
super(Window2, self).__init__()
self.initUI2()
def initUI2(self):
self.vbox2 = QtGui.QVBoxLayout()
self.setLayout(self.vbox2)
self.setGeometry(300, 300, 200, 120)
self.setWindowTitle('Image Bits')
self.show()
def main2():
w2 = QtGui.QWidget()
w2.resize(150, 150)
w2.move(300, 300)
w2.setWindowTitle('Window2')
w2.show()
which is called byt this function in my main class:
def FN_OpenWindow2(self):
win2 = Window2()
#Display it
win2.show()
Function which is called by this button in the same class :
self.NextButton = QtGui.QPushButton("Next")
self.NextButton.setCheckable(True)
self.NextButton.clicked[bool].connect(self.FN_OpenWindow2)
I must not be totally wrong because I have indeed my second window called and opening... a fraction of seconds... I guess only the time the button is "pushed".
So here's my question, how do I keep a reference to the instance of class Window2 ? so once called it stay here (and if I hit again the same button I will just destroy/create to refresh, that's my plan).
Hope I am clear enough, apologize if not I am fairly new to Python.
Thanks !
As you say, you just have to keep reference to the new window alive.
E.g. by simply storing it inside your main class:
def FN_OpenWindow2(self):
self.win2 = Window2()
#Display it
self.win2.show()
Or, if you could have multiple of those, make it a list:
def __init__(self,...):
# initialize an empty list in you main class' init function
self.secondaryWindows = []
def FN_OpenWindow2(self):
win2 = Window2()
#Display it
win2.show()
# append the window to the list
self.secondaryWindows.append(win2)
Mind though, that the references will remain when you close those widgets again.
So it maybe necessary to add additional logic that clears the references at that point.

Tkinter update values from other class

I've got a tkinter window that have 3 features: background color,foreground color, and a text label. These features is in a text config file (properties.conf) in my home folder. I want to update window features when the config file changed. I watch the changes in config file with pyinotify and I want to update window when it changes. This is the code:
#!/usr/bin/python
import threading
from Tkinter import *
import os
import ConfigParser
import pyinotify
class WatchFile(threading.Thread):
def run(self):
def onChange(ev):
Gui().updateGUI()
print 2
wm = pyinotify.WatchManager()
wm.add_watch('/home/mnrl/window_configs', pyinotify.IN_CLOSE_WRITE, onChange)
notifier = pyinotify.Notifier(wm)
notifier.loop()
class ConfigParse():
def __init__(self):
self.confDir = os.path.join(os.getenv('HOME'), 'window_configs/')
self.confFile = os.path.join(self.confDir + "properties.conf")
self.config = ConfigParser.ConfigParser()
if os.path.isfile(self.confFile):
self.config.read(self.confFile)
else:
if not os.path.exists(self.confDir):
os.makedirs(self.confDir)
self.config.add_section('bolum1')
self.config.set('section1', 'setting1', 'green')
self.config.set('section1', 'setting2', 'red')
self.config.set('section1', 'setting3', 'sample text')
with open(self.confFile, 'wb') as self.confFile:
self.config.write(self.confFile)
class Gui(object):
def __init__(self):
self.root = Tk()
self.lbl = Label(self.root, text=ConfigParse().config.get('section1', 'setting3'), fg=ConfigParse().config.get('section1', 'setting1'), bg=ConfigParse().config.get('section1', 'setting2'))
self.lbl.pack()
def updateGUI(self):
self.lbl["text"] = ConfigParse().config.get('bolum1', 'ayar3')
self.lbl["fg"] = ConfigParse().config.get('bolum1', 'ayar1')
self.lbl["bg"] = ConfigParse().config.get('bolum1', 'ayar2')
self.root.update()
WatchFile().start()
Gui().root.mainloop()
But whenever properties.conf file changes a new window more appears near the old tkinter window. So tkinter window not updates, new windows open. How can I correct it?
The problem is that in WatchFile.run() you're doing this:
def onChange(ev):
Gui().updateGUI()
This does something different than what you're expecting. It creates a new GUI instance, and then immediately calls the updateGUI() method on it. Hence the two windows.
What you need to do instead, is something along the lines of:
#!/usr/bin/env python
gui = None
class WatchFile(threading.Thread):
def run(self):
def onChange(ev):
gui.updateGUI()
[...]
WatchFile().start()
gui = Gui()
gui.root.mainloop()
Here, a variable gui is created, and has an instance of your GUI class assigned to it. Later, the updateGUI() method is called on the same instance.
This problem is more or less repeated with your usage of your ConfigParser class, for example:
self.lbl["text"] = ConfigParse().config.get('bolum1', 'ayar3')
In this case, it 'works' because your ConfigParse() class can be executed twice without side-effects (such as opening windows), but it's not very efficient. You're reading the same file multiple times.
What would be better, is to just use a function (a class with only a __init__ defined is effectively the same), run this once, and return a dict.

Python: PyQt Popup Window

So I've been creating my GUI with Qt for my Python application. I've now come to a situation where after a button has been pushed the appropriate deferred gets executed, we perform some tasks then I need to open up a separate window that contains one or two things. But I can't seem to figure out how to create this new separate window. Could anyone give me an example of how to create one?
A common error that can drive you crazy is forgetting to store the handle of the popup window you create in some python variable that will remain alive (e.g. in a data member of the main window).
The following is a simple program that creates a main window with a button where pressing the button opens a popup
#!/usr/bin/env python
#-*- coding: utf-8 -*-
import sys
from PyQt4.Qt import *
class MyPopup(QWidget):
def __init__(self):
QWidget.__init__(self)
def paintEvent(self, e):
dc = QPainter(self)
dc.drawLine(0, 0, 100, 100)
dc.drawLine(100, 0, 0, 100)
class MainWindow(QMainWindow):
def __init__(self, *args):
QMainWindow.__init__(self, *args)
self.cw = QWidget(self)
self.setCentralWidget(self.cw)
self.btn1 = QPushButton("Click me", self.cw)
self.btn1.setGeometry(QRect(0, 0, 100, 30))
self.connect(self.btn1, SIGNAL("clicked()"), self.doit)
self.w = None
def doit(self):
print "Opening a new popup window..."
self.w = MyPopup()
self.w.setGeometry(QRect(100, 100, 400, 200))
self.w.show()
class App(QApplication):
def __init__(self, *args):
QApplication.__init__(self, *args)
self.main = MainWindow()
self.connect(self, SIGNAL("lastWindowClosed()"), self.byebye )
self.main.show()
def byebye( self ):
self.exit(0)
def main(args):
global app
app = App(args)
app.exec_()
if __name__ == "__main__":
main(sys.argv)
What I think can be surprising for Python users and may be is the problem you are facing is the fact that if you don't store a reference to the new widget in the main e.g. by using w = MyPopup(...) instead of self.w = MyPopup(...) the window apparently doesn't appear (actually it's created and it's immediately destroyed).
The reason is that when the local variable w goes out of scope as no one is explicitly referencing the widget the widget gets deleted. This can be seen clearly because if you press again the button you'll see that as the second popup appears the first one is closed.
This also means that if you need to create several popups you have for example to put them in a python list and you should remove them from this list once the popups are closed by the user. The equivalent in the example could be changing to self.w = [] in constructor and then doing self.w.append(MyPopup(...)). Doing that would allow you to open several popups.
Generally, you just show multiple parentless windows with someQWidget.show(), like:
w1 = QLabel("Window 1")
w2 = QLabel("Window 2")
w1.show()
w2.show()
But most likely, you want a modal standard Dialog like this. Also be sure to understand modal dialogs.

Categories

Resources