Fundamental OOP in Tkinter - python

I'm relatively new to OOPs and thus know it theoretically. I need a project to be implemented in Tkinter. So, I was going through it. I was just playing around with the most basic code like below
# https://python-textbok.readthedocs.io/en/1.0/Introduction_to_GUI_Programming.html
import tkinter as tk
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
root = tk.Tk()
app = MainApplication(root)
app.mainloop()
Here, I can say confidently that MainApplication inherits tk.Frame. So, the root is a TK object and it is passed in MainApplication constructor which assigns it as the parent of the MainApplication object.
So, when we call the app.mainloop(), the MainApplication object is called which in turn can invoke the root somehow because we have the root as a parent of the app object.
Correct me if I understood it wrong.
Now, as I browsed and another popular method people are using is
# https://docs.python.org/2/library/tkinter.html#a-simple-hello-world-program
import tkinter as tk
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
root = tk.Tk()
MainApplication(root)
root.mainloop()
Here also, as I can understand the MainApplication is invoked and root becomes the parent of it. But how does that link the MainApplication object with the root object, so that when the root.mainloop() is called, it can refer back to MainApplication object?
Any help is appreciated. Thanks in advance.

This is more about the actual structure of the tkinter module than it is about OOP principles.
mainloop is a method of the Misc class, which is an ancestor of both Tk and Frame. When called, it essentially calls the mainloop method using a reference it has to the root Tk object, so whether you invoke it from root or app, the result is the same.

Related

Can I add specificity to a kwarg in a subclass' constructor?

I am trying to get PyCharm to understand that the subclass of my base controller class only takes a specific type of widget.
Minimal example:
import tkinter as tk
class BaseWidgetController:
def __init__(self, parent: 'tk.Widget'): # Parent is always __some__ kind of widget
self._parent = parent
class EntryWidgetController(BaseWidgetController):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._parent: 'tk.Entry' # On this class, I want Pycharm to understand _parent is only ever an Entry (a subclass of tk.Widget), but even adding this line doesn't change its mind.
def say_type(self) -> None:
print(type(self._parent)) # PyCharm still thinks _parent is a tk.Widget
ew = EntryWidgetController(parent=tk.Frame())
ew.say_type() # Obviously this works fine at runtime.
If you want to constrain the EntryWidgetController so that it only accepts tk.Entry or subclasses, the fix is rather simple - just do
class EntryWidgetController(BaseWidgetController):
def __init__(self, parent: 'tk.Entry', **kwargs):
super().__init__(parent=parent, **kwargs)
That way
ew = EntryWidgetController(parent=tk.Frame())
will make PyCharm complain that Expected type 'Entry', got 'Frame' instead.

PyQt custom widget not visible

Following the examples outlined in "Create simple GUI" I have tried to create Custom Widget, but nothing seems to by shown. It seems to be the simplest widget that I can imagine but still something is missing and I have no idea what.
from PyQt5.QtWidgets import *
import sys
class customWidget(QWidget):
def __init__(self, *args, **kwargs):
super(customWidget, self).__init__(*args, **kwargs)
layout = QHBoxLayout()
label = QLabel("Que chinga")
layout.addWidget(label)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setWindowTitle("Esta locura")
label = customWidget()
self.setCentralWidget(label)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Do any of the following in the __init__ of customWidget (they are the conceptually the same thing):
add self.setLayout(layout) after creating the layout;
change layout = QHBoxLayout() to layout = QHBoxLayout(self);
The reason is that you're not setting the layout for the widget, so the QLabel has no parent and won't be shown on its own.
In practice, the customWidget instance could know anything about those objects, so there's no reason for which it should show them.
I also suggest you to:
read about classes and instances: while it's not directly related to this issue, it has lots of aspects in common, since the parenthood relation of Qt objects (such as QWidget subclasses) is closely related to the OOP aspects of instances.
always use capitalized names for classes (CustomWidget, not customWidget), as lower cased names should only be used for variable and function names.

Add custom attributes to a Tk widget

My main target is to add something like an hidden tag or string to a widget, to save short information on it.
I got the idea of creating a new custom Button class (in this case I need buttons), which inherits all the old options.
This is the code:
form tkinter import *
class NButton(Button):
def __init__(self, master, tag=None, *args, **kwargs):
Button.__init__(self, master, *args, **kwargs)
self.master, self.tag = master, tag
No trouble when creating a new NButton instance:
aria1 = NButton(treewindow, bd=2, relief=GROOVE, text="Trasmissione\naerea 1", bg="#99c4ff", tag="aria 1")
aria1.place(x=20, y=20)
The problems come out when I try to get the value of tag:
aria1["tag"]
it returns:
_tkinter.TclError: unknown option "-tag"
How can I solve this?
You need to access your custom options as object attributes:
print(aria1.tag)

Signaling between parent and child widgets in tkinter

I have a moderately complex GUI that I'm building for interacting with and observing some simulations. I would like to be able to continue to refactor and add features as the project progresses. For this reason, I would like as loose as possible a coupling between different widgets in the application.
My application is structured something like this:
import tkinter as tk
class Application(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.instance_a = ClassA(self)
self.instance_b = ClassB(self)
# ... #
class ClassA(tk.Frame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# ... #
class ClassB(tk.Frame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# ... #
def main():
application = Application()
application.mainloop()
if __name__ == '__main__':
main()
I would like to be able to perform some action in one widget (such as selecting an item in a Treeview widget or clicking on part of a canvas) which changes the state of the other widget.
One way to do this is to have the following code in class A:
self.bind('<<SomeEvent>>', self.master.instance_b.callback())
With the accompanying code in class B:
def callback(self): print('The more that things change, ')
The problem that I have with this approach is that class A has to know about class B. Since the project is still a prototype, I'm changing things all the time and I want to be able to rename callback to something else, or get rid of widgets belonging to class B entirely, or make instance_a a child of some PanedWindow object (in which case master needs to be replaced by winfo_toplevel()).
Another approach is to put a method inside the application class which is called whenever some event is triggered:
class Application(tk.Tk):
# ... #
def application_callback():
self.instance_b.callback()
and modify the bound event in class A:
self.bind('<<SomeEvent>>', self.master.application_callback())
This is definitely easier to maintain, but requires more code. It also requires the application class to know about the methods implemented in class B and where instance_b is located in the hierarchy of widgets. In a perfect world, I would like to be able to do something like this:
# in class A:
self.bind('<<SomeEvent>>', lambda _: self.event_generate('<<AnotherEvent>>'))
# in class B:
self.bind('<<AnotherEvent>>', callback)
That way, if I perform an action in one widget, the second widget would automatically know to to respond in some way without either widget knowing about the implementation details of the other. After some testing and head-scratching, I came to the conclusion that this kind of behavior is impossible using tkinter's events system. So, here are my questions:
Is this desired behavior really impossible?
Is it even a good idea?
Is there a better way of achieving the degree of modularity that I want?
What modules/tools can I use in place of tkinter's built-in event system?
My code in answer avoids the issue of class A having to know about internals of class B by calling methods of a handler object. In the following code methods in class Scanner do not need to know about the internals of a ScanWindow instance. The instance of a Scanner class contains a reference to an instance of a handler class, and communicates with the instance of ScannerWindow through the methods of Handler class.
# this class could be replaced with a class inheriting
# a Tkinter widget, threading is not necessary
class Scanner(object):
def __init__(self, handler, *args, **kw):
self.thread = threading.Thread(target=self.run)
self.handler = handler
def run(self):
while True:
if self.handler.need_stop():
break
img = self.cam.read()
self.handler.send_frame(img)
class ScanWindow(tk.Toplevel):
def __init__(self, parent, *args, **kw):
tk.Toplevel.__init__(self, master=parent, *args, **kw)
# a reference to parent widget if virtual events are to be sent
self.parent = parent
self.lock = threading.Lock()
self.stop_event = threading.Event()
self.frames = []
def start(self):
class Handler(object):
# note self and self_ are different
# self refers to the instance of ScanWindow
def need_stop(self_):
return self.stop_event.is_set()
def send_frame(self_, frame):
self.lock.acquire(True)
self.frames.append(frame)
self.lock.release()
# send an event to another widget
# self.parent.event_generate('<<ScannerFrame>>', when='tail')
def send_symbol(self_, data):
self.lock.acquire(True)
self.symbols.append(data)
self.lock.release()
# send an event to another widget
# self.parent.event_generate('<<ScannerSymbol>>', when='tail')
self.stop_event.clear()
self.scanner = Scanner(Handler())

Python Tkinter safe to self.quit or sys.exit?

In the spirit of decoupling, rather than cramming many widgets into one giant class, I tried splitting them into separate classes.
The problem I ran into is the FileMenu class does not know about root, and so cannot call root.quit or root.destroy. On a whim I tried self.quit, and it works, but I have not seen this used before.
My questions are:
Is self.quit a safe way to quit the app?
Is sys.exit a safe way to quit the app?
If neither are safe, is there a good way to approach decoupling the design and safely quit without passing root as a parameter everywhere or making it a global?
My example:
import Tkinter as tk
class FileMenu(tk.Menu):
def __init__ (self, parent):
tk.Menu.__init__(self, parent, tearoff=False)
self.add_command(label='Exit', command=self.quit)
class MainMenu(tk.Menu):
def __init__ (self, parent):
tk.Menu.__init__(self, parent, tearoff=False)
self.file_menu = FileMenu(self)
self.add_cascade(label='File', menu=self.file_menu)
class View:
def __init__ (self, parent):
self.frame = tk.Frame(parent)
self.parent = parent
self.menu = MainMenu(self.frame)
self.parent.configure(menu=self.menu)
self.parent.geometry('200x200')
self.frame.pack(fill='both', expand=True)
class App:
def __init__ (self):
self.root = tk.Tk()
self.view = View(self.root)
def run (self):
self.root.title('Window Title')
self.root.mainloop()
if __name__ == '__main__':
app = App()
app.run()
Both use for different purpose:
sys.exit(): Close complete application.
self.quit: If you have multiple windows in your application and you don't want to close your whole application then you should use self.quit().
Both are safe to use but uses are different.
If the class doesn't know about the root object but you expect it to act on the root object, you should pass it in.
In App, you pass the root object into View:
self.view = View(self.root)
In View, you create a MainMenu and pass it self.frame:
self.menu = MainMenu(self.frame)
Instead, also pass it the root object:
self.menu = MainMenu(self.frame, parent)
And make MainMenu expect a parameter of that object, and pass it to FileMenu:
class MainMenu(tk.Menu):
def __init__ (self, parent, root):
tk.Menu.__init__(self, parent, tearoff=False)
self.file_menu = FileMenu(self, root)
self.add_cascade(label='File', menu=self.file_menu)
Then make FileMenu expect a parameter of that root object. Now it has access to it:
class FileMenu(tk.Menu):
def __init__ (self, parent, root):
tk.Menu.__init__(self, parent, tearoff=False)
self.add_command(label='Exit', command=root.destroy)
And you can use the Tkinter root object's destroy method to quit the application properly.

Categories

Resources