Testing tkinter application - python

I wrote a small application using python 3 and tkinter. Testing every widget, even though there are not many of them feels daunting so I wanted to write a couple of automated tests to simplify the process. I read some other question that seemed relevant to this problem but none fit my needs. Right now I'm doing the testing in a very simple manner - I invoke the command for every widget and manually click through it to see if it works. It does make things a bit faster, but I constantly run into some problems - i.e. I can't automatically close popup windows (like showinfo) even with using libraries to simulate keyboard clicks (namely pynput). Is there an efficient approach for testing applications using tkinter?
Here is the code I use right now:
import tkinter as tkinter
import unittest
from mygui import MyGUI
class TKinterTestCase(unittest.TestCase):
def setUp(self):
self.root = tkinter.Tk()
def tearDown(self):
if self.root:
self.root.destroy()
def test_enter(self):
v = MyGUI(self.root)
v.info_button.invoke()
v.close_button.invoke()
v.btnOut.invoke()
if __name__ == "__main__":
unittest.main()

I don't know much about unittest but I found a workaround to close popup dialogs like showinfo during the tests. The idea is to use keyboard event to invoke the button of the dialog. But since the app is waiting for the user to close the popup dialog, we need to schedule in advance the keyboard event using after:
self.root.after(100, self.root.event_generate('<Return>'))
v.button.invoke()
Full example
import tkinter
from tkinter import messagebox
import unittest
class MyGUI(tkinter.Frame):
def __init__(self, master, **kw):
tkinter.Frame.__init__(self, master, **kw)
self.info_button = tkinter.Button(self, command=self.info_cmd, text='Info')
self.info_button.pack()
self.quit_button = tkinter.Button(self, command=self.quit_cmd, text='Quit')
self.quit_button.pack()
def info_cmd(self):
messagebox.showinfo('Info', master=self)
def quit_cmd(self):
confirm = messagebox.askokcancel('Quit?', master=self)
if confirm:
self.destroy()
class TKinterTestCase(unittest.TestCase):
def setUp(self):
self.root = tkinter.Tk()
self.root.bind('<Key>', lambda e: print(self.root, e.keysym))
def tearDown(self):
if self.root:
self.root.destroy()
def test_enter(self):
v = MyGUI(self.root)
v.pack()
self.root.update_idletasks()
# info
v.after(100, lambda: self.root.event_generate('<Return>'))
v.info_button.invoke()
# quit
def cancel():
self.root.event_generate('<Tab>')
self.root.event_generate('<Return>')
v.after(100, cancel)
v.quit_button.invoke()
self.assertTrue(v.winfo_ismapped())
v.after(100, lambda: self.root.event_generate('<Return>'))
v.quit_button.invoke()
with self.assertRaises(tkinter.TclError):
v.winfo_ismapped()
if __name__ == "__main__":
unittest.main()

Related

Python Tkimport how to make one from several windows when importing from a module?

I am writing an application in tkinter consisting of several modules in which there are classes. Each module to a separate page of the app. As I move the buttons between the pages "next", "previous" it opens a new window for me every time. How do I make it so that each time calling pages opens in the same window?
I give draft code.
thank you for your help :D
task1.py
import tkinter as tk
from Test.modul.task1 import FirstPage1
class FirstPage0:
def __init__(self, root):
self.root = root
def get_settings(self):
# Window settings
self.root.geometry("100x200")
def get_second_page(self):
FirstPage1(tk.Toplevel()).get_run_first_page()
def get_button(self):
# Add buttons
tk.Button(self.root, text="Start page", command=self.get_second_page).pack()
tk.Button(self.root, text="Exit", command=self.root.destroy).pack()
def get_run_first_page(self):
# Launching the application
self.get_settings()
self.get_button()
self.root.mainloop()
if __name__ == '__main__':
first = FirstPage0(tk.Tk())
first.get_run_first_page()
task2.py
import tkinter as tk
class FirstPage1:
def __init__(self, root):
self.root = root
def get_settings(self):
# Window settings
self.root.geometry("100x200")
def get_second_page1(self):
from Test.task import FirstPage0
FirstPage0(tk.Toplevel()).get_run_first_page()
def get_button(self):
# Add buttons
tk.Button(self.root, text="Back", command=self.get_second_page1).pack()
tk.Button(self.root, text="Exit", command=self.root.destroy).pack()
def get_run_first_page(self):
# Launching the application
self.get_settings()
self.get_button()
self.root.mainloop()
if __name__ == '__main__':
first = FirstPage1(tk.Tk())
first.get_run_first_page()
Solution
#AdrianSz, you wanted to make the buttons not stack under each other. There are three ways to do so. One, is to keep only one button and change its command and text parameters each time when the frames change. Another would be to unpack the button not needed and pack the button needed. The third would be to pack the buttons in the root window instead of frame and change the text and command parameters. I would recommend the second method as it is easier and less prone to errors.
Code
task1.py
import tkinter as tk
from Test.modul.task1 import FirstPage1
class FirstPage0:
def __init__(self, root):
self.root = root
def get_settings(self):
# Window settings
self.root.geometry("100x200")
def get_second_page(self):
self.root.pg_0_btn_start.pack_forget()
self.root_pg_0_btn_exit.pack_forget()
FirstPage1(tk.Toplevel()).get_run_first_page()
def get_button(self):
# Add buttons
self.root.pg_0_btn_start = tk.Button(self.root, text="Start page",
command=self.get_second_page).pack()
self.root_pg_0_btn_exit = tk.Button(self.root, text="Exit",
command=self.root.destroy).pack()
def get_run_first_page(self):
# Launching the application
self.get_settings()
self.get_button()
self.root.mainloop()
if __name__ == '__main__':
first = FirstPage0(tk.Tk())
first.get_run_first_page()
task2.py
import tkinter as tk
class FirstPage1:
def __init__(self, root):
self.root = root
def get_settings(self):
# Window settings
self.root.geometry("100x200")
def get_second_page1(self):
from Test.task import FirstPage0
self.root.pg_1_btn_back.pack_forget()
self.root_pg_1_btn_exit.pack_forget()
FirstPage0(tk.Toplevel()).get_run_first_page()
def get_button(self):
# Add buttons
self.root.pg_1_btn_back = tk.Button(self.root, text="Back", command=self.get_second_page1).pack()
self.root.pg_1_btn_exit = tk.Button(self.root, text="Exit", command=self.root.destroy).pack()
def get_run_first_page(self):
# Launching the application
self.get_settings()
self.get_button()
self.root.mainloop()
if __name__ == '__main__':
first = FirstPage1(tk.Tk())
first.get_run_first_page()
Note: I couldn't test this code from my side so if there are any errors, please comment on this answer
Suggestions
Since you are using modules for these, just make them inherit a class specified in a different file and operate them both from that file. There you can use self to access the methods of a subclass because the subclasses instantiate the base class and thus the self is a object of the subclass and is passed to the base class. The type of code you used is quite confusing too. I have added code to give you your wanted output using the principles I mentioned here. Hope this helped!

QPushButton.clicked.connect doesn't seem to work

I am trying to decouple entirely my GUI from my controller class, and for some reason I can't seem to manage to connect my buttons from outside of my GUI class itself.
Here's a small example of what I mean :
import sys
from PySide6 import QtWidgets
class Gui(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Gui, self).__init__(parent)
layout = QtWidgets.QHBoxLayout(self)
self.button = QtWidgets.QPushButton("Do stuff")
layout.addWidget(self.button)
class Controller(object):
def do_stuff(self):
print("something")
def startup(parent):
ctrl = Controller()
gui = Gui(parent)
gui.button.clicked.connect(ctrl.do_stuff)
return gui
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
dialog = QtWidgets.QDialog()
gui = startup(dialog)
dialog.show()
sys.exit(app.exec_())
I would expect this code, when run, to display a GUI with one push button (which it does), and when pressing the push button, I'd expect the word "something" to get printed. However this doesn't seem to be the case.
I might just be too tired, but I can't find the solution.
What am I missing?
Thanks a lot in advance!
ctrl = None
gui = None
def startup(parent):
global ctrl
global gui
ctrl = Controller()
gui = Gui(parent)
gui.button.clicked.connect(ctrl.do_stuff)
return gui
try this, and it does work. when the variable is in the function, it will be destroyed before the function is finished. the global variable is not a good coding style but is a simple way to figure out your confusion.

Tkinter: properly pass the instance for unit test

I am trying to bring the Tkinter script to the testing. The application would be tested without opening the window, just simulate the button click. What I've done seems to be inappropriate, so how could I pass the instance properly? Thanks.
Here is the prototype of my code:
# importing Tkinter and math
from tkinter import *
import math
class calc:
def __init__(self,master):
"""Constructor method"""
master.title('Calculator')
master.geometry()
self.master = master
self.btn_equal = Button(self.master,text="=",width=11,height=3, fg="red", bg="light green",command=lambda:self.equals()).grid(row=4, column=4,columnspan=2)
self.e.grid(row=0,column=0,columnspan=6,pady=3)
def start_application():
root = Tk()
app = calc(root)
# print(app)
root.bind_class("Button", "<Button-1>", app.callback)
return root
if __name__ == "__main__":
start_application().mainloop()
Here is the testing code:
import unittest
from tkinter import *
from calculator2 import start_application
class TestCalculator2(unittest.TestCase):
# start the application, test, and destroy at the end of the test
async def _start_app(self):
self.app.mainloop()
def setUp(self):
self.app = start_application()
self._start_app()
def tearDown(self):
self.app.destroy()
class TestCalculation(TestCalculator2):
def test_startup(self):
title = self.app.winfo_toplevel().title()
expected = "Calculator"
self.assertEqual(title, expected)
def test_addition(self):
self.btn_equal.invoke() # _Tkinter.tkapp has no attribute, since root is not calc object
The short answer is that the Button widget calls the grid, leading to the NoneType. The correct way of instantiating a button inside a class with position should be:
self.btn_equal = Button(self.master,text="=",width=11,height=3, fg="red", bg="light green",command=lambda:self.equals())
self.btn_equal.grid(row=4, column=4,columnspan=2)
On the other hand, the correct way to instantiate the calc class mentioned above and write a successful unit test with invoke(), which clicks the button. The code won't open the GUI, and the testing script works (with warning):
class TestCalculator2(unittest.TestCase):
# start the application, test, and destroy at the end of the test
async def _start_app(self):
self.app.mainloop() # the root in Tkinter activates the mainloop()
def setUp(self):
self.app = start_application() # activate the root
self.calc = calc(self.app) # instantiate the calculator
self._start_app()
def tearDown(self):
self.app.destroy()
class TestCalculation(TestCalculator2):
def test_startup(self):
title = self.app.winfo_toplevel().title()
expected = "Calculator"
self.assertEqual(title, expected)
def test_addition(self):
self.calc.btn_AC.invoke() # click AC to clear the place
self.calc.btn_7.invoke() # click button 7
self.calc.btn_plus.invoke() # click button +
self.calc.btn_5.invoke() # click button 5
result = self.calc.btn_equal.invoke() # click button equal, get the value
self.assertEqual(result, 12)
Note that the code snippet just gives a brief idea of how unit testing works on Tkinter, the calc class provided is incomplete.

Programmatically press the `X` button on the toobar?

I know I can intercept pressing the X button with protocol("WM_DELETE_WINDOW", do_something) however I am having a hard time figuring out how to activate this button or at least the protocol that triggers when this button is pressed.
Here is the situation. I have 2 classes. My main Tk class and my Menu class. When I am setting up the command to close the program with an exit button from the menu I want this button to do exactly the same thing as the X button on the Tk class.
Now I know I could simply call the controller that was passed to the menu class and then call the method I built to handle the close event however I am trying to build this menu class in such a way that I do not need to do this from the menu class. This will allow me to use the menu class on any app I build with little to no editing.
I have not been able to find a post or some documentation that tells me how I can programmatically activate the "WM_DELETE_WINDOW" protocol.
Here is an image if it is unclear what I want. Simply I want the exit button to do exactly what the X button does.
Main class:
import tkinter as tk
import PIP_MENU
class PIP(tk.Tk):
def __init__(self):
super().__init__()
PIP_MENU.start(self)
self.protocol("WM_DELETE_WINDOW", self.handle_close)
def handle_close(self):
print("Closing")
self.quit()
if __name__ == '__main__':
PIP().mainloop()
Menu class on separate .py file:
import tkinter as tk
class Menu(tk.Menu):
def __init__(self, controller):
super().__init__()
self.controller = controller
controller.config(menu=self)
file_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Exit", command=self.handle_exit)
def handle_exit(self):
# What can I do here that will be handled by
# protocol "WM_DELETE_WINDOW" of the main class?
# All I can find is destroy() and quit()
# But both of these do not get handled by "WM_DELETE_WINDOW".
def start(controller):
Menu(controller)
I have not been able to find a post or some documentation that tells me how I can programmatically active the "WM_DELETE_WINDOW" protocol.
You can't. By definition, the WM_DELETE_WINDOW protocol comes from the window manager.
Catching the protocol handler is designed to give you an opportunity to override its behavior. It is not designed to be a way to trigger some code no matter how the application is destroyed. If you want to run some code when the window is destroyed, whether that is by the user clicking the control on the window frame or through some other way, the correct way to do that is to bind to the <Destroy> event on the root window.
You have to be careful, in that any binding on the root window will be triggered for every widget. Therefore, your binding should only run when event.widget is the same as the root window.
The following example illustrates the technique. There is a method handle_close which is called whenever the window is destroyed. Whether you close the window by clicking on the control on the window frame, or whether you click on the "Close me!" button, the code still runs.
import tkinter as tk
class Example(tk.Tk):
def __init__(self):
super().__init__()
self.bind("<Destroy>", self.handle_close)
button = tk.Button(self, text="Close me!", command=self.destroy)
button.pack()
def handle_close(self, event):
if event.widget == self:
print("Closing")
self.quit()
example = Example()
example.mainloop()
I don't believe there's a method that invokes a specific protocol, since protocol seems to be a specific event watch. Here's a snippet from the module's class Tk:
class Tk(Misc, Wm):
"""Toplevel widget of Tk which represents mostly the main window
of an application. It has an associated Tcl interpreter."""
def _loadtk(self):
...
self.protocol("WM_DELETE_WINDOW", self.destroy)
As you can see, by default the module itself sets the protocol to destroy(). The protocol() method only seeks to replace the default function (at the absence of a function, it just removes the function):
def wm_protocol(self, name=None, func=None):
"""Bind function FUNC to command NAME for this widget.
Return the function bound to NAME if None is given. NAME could be
e.g. "WM_SAVE_YOURSELF" or "WM_DELETE_WINDOW"."""
if callable(func):
command = self._register(func)
else:
command = func
return self.tk.call(
'wm', 'protocol', self._w, name, command)
protocol = wm_protocol
but to achieve what you want you should be able to reference back to the same handling method with this:
def handle_exit(self):
self.controller.handle_close()
Of course, this is not as versatile since you must explicitly know the handler in your main window.
Thought I have accepted Bryan's answer I did manage to come to a workaround I think is fine here.
If I pass the method that is being used to deal with window closing to my menu class and then check if something has been passed I can then decide on weather or not to use the exit method I made or self.controller.destroy() with an if statement.
Here is my solution.
Main file:
import tkinter as tk
from tkinter import messagebox
import PIP_MENU
class PIP(tk.Tk):
def __init__(self):
super().__init__()
PIP_MENU.start(self, self.handle_close)
self.protocol("WM_DELETE_WINDOW", self.handle_close)
def handle_close(self):
x = messagebox.askquestion("DERP", "Do you want to close without saving?")
if x == "yes":
self.destroy()
if __name__ == '__main__':
PIP().mainloop()
Menu file:
import tkinter as tk
class Menu(tk.Menu):
def __init__(self, controller, exit_handler=None):
super().__init__()
self.controller = controller
self.exit_handler = exit_handler
controller.config(menu=self)
file_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Exit", command=self.handle_exit)
def handle_exit(self):
if self.exit_handler != None:
self.exit_handler()
else:
self.controller.quit()
def start(controller, exit_handler=None):
return Menu(controller, exit_handler)

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.

Categories

Resources