Tkinter callbacks to main module - python

I've built a simple single-script GUI application in Tkinter which works as intended. Since the code for the Tk controls takes up half of the 600 lines script I'm trying to separate the GUI from the main application by putting it in a separate module.
In main.py file I have:
from mytkfile import MainWindow
def main():
"""Main program entry point."""
decks = initialize()
app = App(decks)
app.view.root.mainloop()
if __name__ == '__main__':
main()
In mytkfile.py there is just a huge MainWindow class, which is currently is handling the callbacks to the different interface elements:
import tkinter as tk
from tkinter import ttk
class MainWindow:
def __init__(self, deck):
self.root = tk.Tk()
# ...
The App class in main.py, simplified, looks like this:
class App:
def __init__(self, decks):
self.decks = decks
# ...
self.view = MainWindow(self.deck)
self.updateview()
def updateview(self):
# call several self.view.update_methods
The main window is drawn and basically works as it used to, because MainWindow handles a lot of the data logic itself. However, I would like to change that and have App handle everything and just ask MainWindow to update itself. I am trying to achieve this without App passing itself to MainWindow when instantiating it, because as I understand it a View should not know about its Controller, and should not be calling its own methods to handle data logic.
Problem is, how can I handle e.g. button callbacks in Tk when MainWindow doesn't know anything about App?

After reading various articles on the observer design pattern in Python I've concluded that it's indeed impossible to at least slightly couple the Tkinter view MainWindow class to the model/controller App class in my code. It had been solved by the App class passing itself to the MainWindow, with Tk buttons notifying clicks to callback functions inside App. Works like a charm.

Related

Avoid circular import when accessing instance value from main.py

I´m new to Python and programming in general. So maybe there is an easy solution for more experienced programmers.
I already read a lot of question regarding circular imports, but unfortunately there was nothing there that I can apply to my situation if I dont want to move all the code in one file.
I created an userinterface with pyqt (qt creator) and converted the mainwindow.ui to mainwindow.py.
My plan is to split the code into 3 modules. A main module to start the application, an ui module with the class of the main window and a buttons module with classes for the buttons.
My problem is that the functions within the button classes should change a label value of the main window instance. I learned to create the main window instance in the main module. As a result of this I need to import the instance from the main module into the buttons module to change the intended value and that leads to an circular import.
How do I have to organize/structure my code to avoid this?
Here is a short and simplified example for better understanding:
main.py
import sys
from qtpy import QtWidgets
from ui import MainWindow
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
ui.py
from qtpy import QtWidgets
from userinterface.mainwindow import Ui_MainWindow
import buttons
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.button_0 = buttons.NumberButton(0)
self.button_1 = buttons.NumberButton(1)
self.ui.btn_0.clicked.connect(self.button_0.button_clicked)
self.ui.btn_1.clicked.connect(self.button_1.button_clicked)
buttons.py
from main import window
class NumberButton:
def __init__(self, number):
self.number = str(number)
def button_clicked(self):
window.ui.lb_result.setText(self.number)
Your design problem is that your NumberButton class calls one specific window instance. Your have to let your buttons know to which window they belong. Try the following: remove the import statement from buttons.py and add a new parameter window to the __init__ method:
class NumberButton:
def __init__(self, window, number):
self.window = window
self.number = str(number)
def button_clicked(self):
self.window.ui.lb_result.setText(self.number)
Then instantiate in NumberButton like:
...
self.button_0 = buttons.NumberButton(self, 0)
...
If you only import the module python should automatically avoid circular imports. So do import ui and import buttons

Python - reference class each other (information flow)

I would like to know, what is the concept of information flow in GUI based apps, or any other app with same problem. When you have two seperate classes and their objects, how is the messeging process done between them. For example you have a GUI and AppLogic.
Scenario 1: Button is pressed -> GUI is processing event -> calls AppLogic method image_clicked()
Scenario 2: HTTPServer gets a message -> AppLogic receives image -> AppLogic calls GUI method render_image()
The problem is that you cannot reference classes each other because the first class does not know the second one (here AppLogic does not know GUI class):
class AppLogic():
gui : GUI
def image_clicked(self):
pass #not important
class GUI():
app_logic : AppLogic
def render_image(self):
pass #not important
I know this is more like go to school and study problem, but I would like to know how these problems are sovled, or some common practices. At least link with some detailed information. I am not able to name the problem right to find the answer.
Edit:
I can use this code without explicit type declaration and it works. But when I want to call functions of gui in AppLogic class definition, intellisense does not hint anything, because it does not know the type of attribute gui. And I don't think that it is good practice to use code like that.
class AppLogic():
def __init__(self) -> None:
self.gui = None
def image_clicked(self):
pass #not important
class GUI():
def __init__(self) -> None:
self.app_logic = None
def render_image(self):
pass #not important
app = AppLogic()
gui = GUI()
app.gui = gui
gui.app_logic = app
You need to initialize your variables.
gui = Gui()
then you can call the methods
For example:
class AppLogic:
gui: Gui
def image_clicked(self):
gui = Gui()
gui.render_image()
class Gui:
logic: AppLogic
def render_image(self) :
pass
Or you can initialize your variable directly
gui: Gui = Gui()
I hope this answers your question
from LogicClass import Logic
class Gui:
logic: AppLogic = AppLogic()
def render_image(self) :
pass
and:
from GuiClass import Gui
class AppLogic:
gui: Gui
def image_clicked(self):
gui = Gui()
gui.render_image()
from Gui import Gui
class Logic:
def __init__(self):
self.gui = Gui()
if __name__ == "__main__":
Logic()
and
class Gui:
def __init__(self):
print("GUI")

Modular Tkinter Window Opens Two Window When Run

When I run my Tkinter code the results end up giving me two Tkinter windows. One has the widgets/items my code says but one is a completely blank window.
file 1-
from tkinter import*
class Screen:
def __init__(self, items):
self.Tk = Tk()
self.items = items
self.Tk.mainloop()
file 2-
from tkinter import*
from Screen import*
class Module1:
def __init__(self):
Test_button = Button(text="Test")
Test_button.pack()
items = Module1()
Mainscreen = Screen(items)
I just want to make a modular Tkinter screen so my code is not that messy 😅
-Just want one/common Tkinter window
I think the trouble is you're going about things backwards a bit. You'll want to import your modules into the Python file containting your "root" window class (aka Screen), rather than the other way around.
The typical way to handle a "root" window class in tkinter usually looks like this, where the class inherits directly from Tk()
# this is the first file...
import tkinter as tk # avoid star imports!
from file2 import Module1 # import your widget classes here
# note that depending on your project structure you may need to do
# 'from .file2 import Module1' instead (this is called a 'relative import')
class Screen(tk.Tk): # the 'Screen' class inherits from 'tk.Tk'
def __init__(self):
super().__init__() # initialize tkinter.Tk()
# instance your module widgets by calling the class like so
# (you'll need to update Module1 as shown below here)
self.mod1 = Module1(self) # 'self' refers to 'Screen', the main window
You'll need to update the Module1 class so it can be bound to your parent Screen
# this is the second file...
import tkinter as tk # again, star imports bad...
class Module1:
def __init__(self, parent): # add the parent parameter
# whenever you instance your 'Module1' class, it will expect a 'parent';
# above, we used self (i.e. 'Screen') - this is where your 'button' is going!
self.parent = parent
test_button = tk.Button(self.parent, text="Test")
test_button.pack()
And you'll run the mainloop() method on an instance of your Screen class at the end of your main Python file like so
# this is the first file again...
if __name__ == '__main__': # if this script is being run at the top level...
app = Screen() # get an instance of your Screen class
app.mainloop() # run the GUI loop

Tkinter Show splash screen and hide main screen until __init__ has finished

I have a main tkinter window that can take up to a few seconds to load properly. Because of this, I wish to have a splash screen that shows until the init method of the main class has finished, and the main tkinter application can be shown. How can this be achieved?
Splash screen code:
from Tkinter import *
from PIL import Image, ImageTk
import ttk
class DemoSplashScreen:
def __init__(self, parent):
self.parent = parent
self.aturSplash()
self.aturWindow()
def aturSplash(self):
self.gambar = Image.open('../output5.png')
self.imgSplash = ImageTk.PhotoImage(self.gambar)
def aturWindow(self):
lebar, tinggi = self.gambar.size
setengahLebar = (self.parent.winfo_screenwidth()-lebar)//2
setengahTinggi = (self.parent.winfo_screenheight()-tinggi)//2
self.parent.geometry("%ix%i+%i+%i" %(lebar, tinggi, setengahLebar,setengahTinggi))
Label(self.parent, image=self.imgSplash).pack()
if __name__ == '__main__':
root = Tk()
root.overrideredirect(True)
progressbar = ttk.Progressbar(orient=HORIZONTAL, length=10000, mode='determinate')
progressbar.pack(side="bottom")
app = DemoSplashScreen(root)
progressbar.start()
root.after(6010, root.destroy)
root.mainloop()
Main tkinter window minimum working example:
import tkinter as tk
root = tk.Tk()
class Controller(tk.Frame):
def __init__(self, parent):
'''Initialises basic variables and GUI elements.'''
frame = tk.Frame.__init__(self, parent,relief=tk.GROOVE,width=100,height=100,bd=1)
control = Controller(root)
control.pack()
root.mainloop()
EDIT: I can use the main window until it has finished loading using the .withdraw() and .deiconify() methods. However my problem is that I cannot find a way to have the splash screen running in the period between these two method calls.
a simple example for python3:
#!python3
import tkinter as tk
import time
class Splash(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.title("Splash")
## required to make window show before the program gets to the mainloop
self.update()
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.withdraw()
splash = Splash(self)
## setup stuff goes here
self.title("Main Window")
## simulate a delay while loading
time.sleep(6)
## finished loading so destroy splash
splash.destroy()
## show window again
self.deiconify()
if __name__ == "__main__":
app = App()
app.mainloop()
one of the reasons things like this are difficult in tkinter is that windows are only updated when the program isn't running particular functions and so reaches the mainloop. for simple things like this you can use the update or update_idletasks commands to make it show/update, however if the delay is too long then on windows the window can become "unresponsive"
one way around this is to put multiple update or update_idletasks command throughout your loading routine, or alternatively use threading.
however if you use threading i would suggest that instead of putting the splash into its own thread (probably easier to implement) you would be better served putting the loading tasks into its own thread, keeping worker threads and GUI threads separate, as this tends to give a smoother user experience.

Class functions and Tkinter

I am currently stuck on a problem with Python and Tkinter.
I want to create a simple application with its UI made on Tkinter. To do so, I created a class to define my application and I want to create my GUI layout in a separate class function.
However, when I call it, it has no effect on my Tk window (in this particular example, the title is not modified)
Here is the code
from Tkinter import *
fen =Tk()
class test_Tk_class:
def __init__(self):
self.make_title
def make_title(self):
fen.title("Test")
a = test_Tk_class()
fen.mainloop()
Thanks for any help !
You're missing () after self.make_title:
from Tkinter import *
fen =Tk()
class test_Tk_class:
def __init__(self):
self.make_title() # <------------
def make_title(self):
fen.title("Test")
a = test_Tk_class()
fen.mainloop()

Categories

Resources