python_introduction/learning_python3_gui.md

56 KiB

tkinter

Tkinter helloworld

The absolute most basic way to have a hello world GUI program up and running with Tkinter is the following.

from tkinter import *

root = Tk()

MyLabel = Label(root,text="hello world")
MyLabel.pack()

root.mainloop()

Tkinter have two popular architectures, the Tcl and Tk. This two architectures are different, they have their own functionality and their own official documentation. We are gonna use the Tk architecture.

The Tk architecture is used to create a GUI widget. He adds a lot of custom commands, so the widget is very customizable.

In the previous code,the mainloop() instruction allows us to open the main window and to not close immediately the window. And then, the Label() method creates label in a layout. This method has many parameters to customize a label. The instruction pack() will be always call , this instruction can add and adjust a widget.

While it works we know better by now. We should include the if __name__ == "__main__" statement.

from tkinter import *
if __name__ == "__main__":
    root = Tk()

    MyLabel = Label(root,text="hello world")
    MyLabel.pack()

    root.mainloop()

The instance of Tk() is what will actually process our event loop and the root is the content of our window. We can customize root with instruction like geometry, title,etc. In the latter we will create our button and labels so should create our own class and inherit from it.

from tkinter import *

class MainWindow(Frame):
    def __init__(self):
        Label.__init__(self, text="hello world")
        self.pack()

if __name__ == "__main__":
    root = Tk()
    root.title("title of the window") 
    root.geometry("500x300")
    MainWindow()
    root.mainloop()

We can add content to the frame, such as labels, input boxes and buttons as follows.

from tkinter import *

class MainWindow(Frame):
    def __init__(self):
        Label.__init__(self, text="hello world")
        #Label
        MyLabel = Label(self, text="This is a label")
        MyLabel.pack()

        self.config(bg="yellow")
        self.pack()

if __name__ == "__main__":
    root = Tk()
    root.title("title of the window")
    root.geometry("500x300")
    MainWindow()
    root.mainloop()

Let's try to put multiple visual object into the same frame.

from tkinter import *

class MainWindow(Frame):
    def __init__(self):
        Label.__init__(self, text="hello world")
        #Label
        MyLabel = Label(self, text="This is a label")
        MyLabel.pack()
        #Button
        MyButton = Button(self, text="I'm clickable!")
        MyButton.pack()
        
        self.config(bg="yellow")
        self.pack()

if __name__ == "__main__":
    root = Tk()
    root.title("title of the window")
    root.geometry("500x300")
    MainWindow()
    root.mainloop()

You see how they are stacked one on top of the other? We can overcome this problem with parameters of pack(). We can also use other geometry managers like grid() and place().

from tkinter import *

class MainWindow(Frame):
    def __init__(self):
        Frame.__init__(self, master=None)
        MyPanel = PanedWindow.__init__(self, bg="Blue")
        #Label
        MyLabel = Label(MyPanel, text="this is a label", bg= "yellow")
        MyLabel.pack(fill="x")
        #Bouton
        MyButton = Button(MyPanel, text="I'm clickable!")
        MyButton.place(x=10, y=50)
        self.pack(fill=BOTH,expand=True)

if __name__ == "__main__":
    root = Tk()
    root.title("this is the title of the window") 
    root.geometry("500x300") 
    win = MainWindow()
    root.mainloop()

This is looking better! But it requires some explanation though. Let's break it down. The Frame.__init__ is your window in which you create a PanedWindow.__init__ which is used to draw thing to. To this panel you're adding two different objects (Label() and Button()) each with or without their own settings (such as a label or not). It probably looks a bit convoluted but this is how most GUI libraries work internally.

  1. You create a frame
  2. Within this frame you create a drawing area and set some form of automatic layout to it.
  3. You create the visual elements you want and add them one by one to the drawing area.
  4. Success

Now how do we link user input to code actions? This is a complicated way of saying actually do something when I click the damn button! For this we'll need to create a function, or better yet a method to the Frame.__init__ objects. Each time we click the button, that method will be called. Because it is a method it has access to self so it can modify anything within the scope of the instance.

from tkinter import *
class MainWindow(Frame):
    def __init__(self): 
        Frame.__init__(self, master=None)
        MyPanel = PanedWindow.__init__(self, bg="Blue")
        #Label
        MyLabel = Label(MyPanel, text="this is a label", bg= "yellow")
        MyLabel.pack(fill="x")
        #Bouton
        MyButton = Button(MyPanel, text="I'm clickable!", command=lambda  : self.ButtonEnable(MyLabel))
        MyButton.place(x=10, y=50)
        self.pack(fill=BOTH,expand=True) 
        
    def ButtonEnable(self, Label):
        global number
        number += 1
        counter = "You have press the button {} time".format(number)
        Label.config(text=counter)


number=0

if __name__ == "__main__":
    root = Tk()
    root.title("this is the title of the window") 
    root.geometry("500x300") 
    win = MainWindow()
    root.mainloop()


We can use the same idea to grab input from the textbox.

from tkinter import *

class MainWindow(Frame):
    def __init__(self): 
        Frame.__init__(self, master=None)
        MyPanel = PanedWindow.__init__(self, bg="Blue")
        #Label
        MyLabel = Label(MyPanel, text="this is a label", bg= "yellow")
        MyLabel.pack(fill="x")
        #TextBox
        MyEntry = Entry(MyPanel)
        MyEntry.place(x=200,y=50)
        #Bouton
        MyButton = Button(MyPanel, text="I'm clickable!", command=lambda  : self.ButtonEnable(MyLabel,MyEntry))
        MyButton.place(x=10, y=50)
        self.pack(fill=BOTH,expand=True) 


    def ButtonEnable(self, MyLabel, MyEntry):
        MyText = MyEntry.get()
        MyLabel.config(text=MyText)


if __name__ == "__main__":
    root = Tk()
    root.title("this is the title of the window")
    root.geometry("500x300") 
    win = MainWindow()
    root.mainloop()


Tkinter guess the number

import time
from tkinter import *
import random

class MainWindow(Frame):
    def __init__(self):
        Frame.__init__(self, master=None, bg="white")
        MyPanel = PanedWindow.__init__(self)

        MyNumber = random.randint(0, 100)

        #Label
        self.MyLabel = Label(MyPanel, text="I have a number in mind...", bg= "blue")
        self.MyLabel.pack(fill="x", ipadx=25, ipady=20)
        #TextBox
        MyEntry = Entry(MyPanel)
        MyEntry.place(x=200,y=90)
        #Bouton
        MyButton = Button(MyPanel, text="I'm clickable!", command=lambda  : self.ButtonEnable(MyEntry, MyNumber))
        MyButton.place(x=10, y=90)

        self.pack(fill=BOTH,expand=True)


    def ButtonEnable(self, MyEntry, MyNumber):
        if self.IsCorrect(MyEntry.get()):
            number = int(MyEntry.get())
            if number != MyNumber:
                self.GameOver(number, MyNumber)
            else:
                self.Win()
        else:
            self.MyLabel.config(text="I need numbers!")

    def GameOver(self, number, MyNumber):
        if number > MyNumber:
            self.MyLabel.config(text="My number is smaller")
        else:
            self.MyLabel.config(text="My number is bigger")

    def Win(self):
        self.MyLabel.config(text="You WIN!")

    def IsCorrect(self, MyEntry):
        x = str(MyEntry)
        if x.isdigit() == True:
            return True
        else:
            return False


if __name__ == "__main__":
    root = Tk()
    root.title("Guess the number")
    root.geometry("500x300")
    win = MainWindow()
    root.mainloop()


MVC design pattern

A simple console only MVC. We'll add the GUI view in a bit.

from tkinter import *

class ConsoleView(object):
    """A view for console."""

    def select_task(self):
        """Asks which index to look up."""
        idx = input("which task do you want to see? ")
        return idx

    def show(self, task):
        """Displays the task to the console. This method is called from the
        controller."""
        print("your task: {}".format(task))

    def error(self, msg):
        """Prints error messages coming from the controller."""
        print("error: {}".format(msg))

class Model(object):
    """The model houses add data and should implement all methods related to
    adding, modifying and deleting tasks."""

    db = ["go to the shops", "dryhop beer", "drop of motorbike"]

    def get_task(self, idx):
        """Performs a task lookun into the database and returns it when found.
        If no task is found, it returns an error message that will be displayed
        in the view (via the controller)."""
        try:
            task = Model.db[idx]
        except IndexError:
            task = "task with {} not found!".format(idx)
        return task


class Controller(object):
    """Binds the model and the view together."""

    def __init__(self, view):
        self.model = Model()
        self.view = view 

    def run(self):
        """The controller's main function. Depending on what type of view is 
        selected, a different event loop is setup. Do note that the ConsoleView
        is not a real event loop, just a basic flow of action."""
        if self.view is ConsoleView:
            self.view = self.view()
            self._run_console_view()
        elif self.view is TkinterView:
            root = Tk()
            root.title("Task Manager")
            root.geometry("500x300")
            self.view = self.view()
            self.view._set_controller(self)
            root.mainloop()

    def get_task_from_model(self, idx):
        """Needed for the TkinterView to communicate with the controller."""
        task = self.model.get_task(idx)
        self.view.show_task(task)

    def _run_console_view(self):
        """Super simple event loop."""
        while True:
            try:
                idx = self.view.select_task()
                idx = int(idx)
            except Exception as e:
                self.view.error(e)
                continue
            task = self.model.get_task(idx)
            self.view.show(task)


if __name__ == "__main__":
    view = ConsoleView
    app = Controller(view)
    app.run()

And now with the implemented TkinterView class.

from tkinter import *

class ConsoleView(object):
    """A view for console."""

    def select_task(self):
        """Asks which index to look up."""
        idx = input("which task do you want to see? ")
        return idx

    def show(self, task):
        """Displays the task to the console. This method is called from the
        controller."""
        print("your task: {}".format(task))

    def error(self, msg):
        """Prints error messages coming from the controller."""
        print("error: {}".format(msg))


class TkinterView(Frame):
    """A view using a wx.Dialog window"""

    def __init__(self):
        Frame.__init__(self, master=None)
        #Panel
        self.panel = PanedWindow(self, bg="green")
        self.panel.pack(fill=BOTH, expand=True)

        #Task Label
        self.task = Label(self.panel, text="your task")
        self.task.pack(expand=True)

        #SpinBox
        self.idx = Spinbox(self.panel, from_=0, to=2, wrap=True )
        self.idx.pack(side= TOP)

        #Button
        self.button = Button(self.panel, text="submit", command=lambda : self.select_task())
        self.button.pack(ipadx=60, ipady=30)

        self.pack(fill=BOTH, expand=True)

    def _set_controller(self, controller):
        """Set the controller so the view can communicate it's requests to it
        and update it's values too."""
        self.controller = controller

    def select_task(self):
        """Gets the index to look up in the model and submits the request to
        the controller."""
        idx = self.idx.get()
        self.controller.get_task_from_model(idx)

    def show_task(self, task):
        """Updates the visual label in the view with the task. This method is
        called from the controller."""
        self.task.config(text=task)


class Model(object):
    """The model houses add data and should implement all methods related to
    adding, modifying and deleting tasks."""

    db = ["go to the shops", "dryhop beer", "drop of motorbike"]

    def get_task(self, idx):
        """Performs a task lookun into the database and returns it when found.
        If no task is found, it returns an error message that will be displayed
        in the view (via the controller)."""
        try:
            task = Model.db[int(idx)]
        except IndexError:
            task = "task with {} not found!".format(idx)
        return task


class Controller(object):
    """Binds the model and the view together."""

    def __init__(self, view):
        self.model = Model()
        self.view = view

    def run(self):
        """The controller's main function. Depending on what type of view is
        selected, a different event loop is setup. Do note that the ConsoleView
        is not a real event loop, just a basic flow of action."""
        if self.view is ConsoleView:
            self.view = self.view()
            self._run_console_view()
        elif self.view is TkinterView:
            root = Tk()
            root.title("Task Manager")
            root.geometry("500x300")
            self.view = self.view()
            self.view._set_controller(self)
            root.mainloop()

    def get_task_from_model(self, idx):
        """Needed for the TkinterView to communicate with the controller."""
        task = self.model.get_task(idx)
        self.view.show_task(task)

    def _run_console_view(self):
        """Super simple event loop."""
        while True:
            try:
                idx = self.view.select_task()
                idx = int(idx)
            except Exception as e:
                self.view.error(e)
                continue
            task = self.model.get_task(idx)
            self.view.show(task)


if __name__ == "__main__":
    view = TkinterView
    app = Controller(view)
    app.run()

For a GUI only login generator an implementation without MVC could look a bit like this. Note that the actual calculation is done inside the window itself. This is not a good idea because we should separate responsibilities into classes!

from tkinter import *
from tkinter import ttk
import login_generator


class MainWindow(Frame):
    def __init__(self):
        Frame.__init__(self)
        self.full_window = PanedWindow(self,orient=VERTICAL)
        self.full_window.pack(fill=BOTH,expand=True)
        self.create_top_panel()
        self.create_bottom_panel()
        self.pack(fill=BOTH, expand=True)

    def create_bottom_panel(self):
        bottom_panel = PanedWindow(self.full_window, bg="green")
        bottom_panel.pack(fill=BOTH, side=BOTTOM)
        #List
        self.login_list = ttk.Treeview(bottom_panel, columns=["username", "password"], show="headings", height=6)
        self.login_list.pack()
        self.login_list.heading("username", text="Username")
        self.login_list.heading("password", text="Password")
        self.login_list.bind("<ButtonRelease-1>", lambda e: self.show_popup())

    def create_top_panel(self):

        top_panel = PanedWindow(self.full_window, bg="red")
        self.login_amount = Text(top_panel, height=5, width=52)
        self.login_amount.place(x=10,y=10)
        self.complex = BooleanVar()
        self.complex.set(False)
        self.login_complex = Checkbutton(top_panel, text="complex",var=self.complex)
        self.login_complex.place(x=10,y=100)
        self.login_create = Button(top_panel, text="Create", command=lambda: self.add_login())
        self.login_create.place(x=100,y=100)
        top_panel.pack(expand=True, fill=BOTH)

    def show_popup(self):
        global root
        menu = Menu()
        menu.add_command(label="Copy selected items", command=lambda : self.copy_items())
        root.config(menu=menu)

    def copy_items(self):
        global root
        try:
            Item = self.login_list.item(self.login_list.focus())
            FunctionValue = Item.values()
            Array = list(FunctionValue)
            Login = Array[2]
            self.login_amount.insert(END, "Username : {} \nPassword : {} \n".format(Login[0],Login[1]))

        except:
            Msg = Toplevel(root)
            l1 = Label(Msg, text="You have to take something")
            l1.pack(fill=BOTH, expand=True)

    def add_login(self):
        self.username = login_generator.generate_username()
        self.password = login_generator.generate_password(12,self.complex.get())
        self.login_list.insert('', END,values=[self.username, self.password])

if __name__ == "__main__":
    root = Tk()
    root.title("Login Generator")
    root.geometry("500x300")
    win = MainWindow()
    root.mainloop()

Now let's assume the generate username and password function take some calculation time. We'll add in a fake time.sleep to simulate.

from tkinter import *
from tkinter import ttk
import login_generator
import time


class MainWindow(Frame):
    def __init__(self):
        Frame.__init__(self)
        self.full_window = PanedWindow(self,orient=VERTICAL)
        self.full_window.pack(fill=BOTH,expand=True)
        self.create_top_panel()
        self.create_bottom_panel()
        self.pack(fill=BOTH, expand=True)

    def create_bottom_panel(self):
        bottom_panel = PanedWindow(self.full_window, bg="green")
        bottom_panel.pack(fill=BOTH, side=BOTTOM)
        #List
        self.login_list = ttk.Treeview(bottom_panel, columns=["username", "password"], show="headings", height=6)
        self.login_list.pack()
        self.login_list.heading("username", text="Username")
        self.login_list.heading("password", text="Password")
        self.login_list.bind("<ButtonRelease-1>", lambda e: self.show_popup())

    def create_top_panel(self):
        top_panel = PanedWindow(self.full_window, bg="red")
        self.login_amount = Text(top_panel, height=5, width=52)
        self.login_amount.place(x=10,y=10)
        self.complex = BooleanVar()
        self.complex.set(False)
        self.login_complex = Checkbutton(top_panel, text="complex",var=self.complex)
        self.login_complex.place(x=10,y=100)
        self.login_create = Button(top_panel, text="Create", command=lambda: self.add_login())
        self.login_create.place(x=100,y=100)
        top_panel.pack(expand=True, fill=BOTH)

    def show_popup(self):
        global root
        menu = Menu()
        menu.add_command(label="Copy selected items", command=lambda : self.copy_items())
        root.config(menu=menu)

    def copy_items(self):
        global root
        try:
            Item = self.login_list.item(self.login_list.focus())
            FunctionValue = Item.values()
            Array = list(FunctionValue)
            Login = Array[2]
            self.login_amount.insert(END, "Username : {} \nPassword : {} \n".format(Login[0],Login[1]))
            time.sleep(1)

        except:
            Msg = Toplevel(root)
            l1 = Label(Msg, text="You have to take something")
            l1.pack(fill=BOTH, expand=True)

    def add_login(self):
        self.username = login_generator.generate_username()
        self.password = login_generator.generate_password(12,self.complex.get())
        self.login_list.insert('', END,values=[self.username, self.password])


if __name__ == "__main__":
    root = Tk()
    root.title("Login Generator")
    root.geometry("500x300")
    win = MainWindow()
    root.mainloop()

A clear separation of responsabilities can be acchieved via an MVC pattern and a login library. The library code is quite straightforward and goes as follows. It's basically the same code we did before but with added try except blocks. Can you tell me why I added those?

import random
import string


def load_file(filename):
    """
    We load a file and make a list out of it. Note that the same function is 
    used for both files (both adjectives and subjects). Functions should be 
    made as generic as possible.

    There IS a problem you can fix, some logins will have spaces in them. Try
    to remove them in this function!
    """
    words = []
    with open(filename, "r") as fp:
        lines = fp.readlines()
        for line in lines:
            words.append(line.strip())          # what does strip() do, what does append() do? remember CTRL+Q!
    return words


def generate_username():
    """
    We'll generate a random pair of adjectives and subjects from two wordlists.
    You NEED to have both files in you python project for this to work! Note 
    the capitalize method call to make it all prettier...
    """
    try:
        adjectives = load_file("./adjectives.txt")
    except:
        adjectives = ["big", "funny", "normal", "red"]
    try:
        subjects = load_file("./subjects.txt")
    except:
        subjects = ["giraffe", "elephant", "cougar", "tiger"]
    adjective = random.choice(adjectives)
    subject = random.choice(subjects)
    username = adjective.capitalize() + subject.capitalize()
    return username


def generate_password(length=10, complictated=True):
    """
    We generate a password with default settings. You can overide these by
    changing the arguments in the function call.
    """
    password = ""
    if complictated:
        chars = string.ascii_letters + string.digits + string.punctuation
    else:
        chars = string.ascii_letters
    for i in range(0, length):
        password += random.choice(chars)
    return password


if __name__ == "__main__":
    # let's do some testing!
    username_test = generate_username()
    print(username_test)
    password_test = generate_password()
    print(password_test)

And now the GUI code nicely split up in a model, controller and a view. The overhead is quite large but it makes the code a lot more scalable.

from tkinter import *
from tkinter import ttk
import login_generator
import time
import threading
import queue


class View(Frame):
    def __init__(self, controller):
        Frame.__init__(self)
        self.controller = controller
        self.full_window = PanedWindow(self, orient=VERTICAL)
        self.full_window.pack(fill=BOTH, expand=True)
        self.create_top_panel()
        self.create_bottom_panel()
        self.pack(fill=BOTH, expand=True)

    def create_bottom_panel(self):
        bottom_panel = PanedWindow(self.full_window, bg="green")
        bottom_panel.pack(fill=BOTH, side=BOTTOM)
        # List
        self.login_list = ttk.Treeview(bottom_panel, columns=["username", "password"], show="headings", height=6)
        self.login_list.pack()
        self.login_list.heading("username", text="Username")
        self.login_list.heading("password", text="Password")
        self.login_list.bind("<ButtonRelease-1>", lambda e: self.show_popup())

    def create_top_panel(self):
        top_panel = PanedWindow(self.full_window, bg="red")
        self.login_amount = Text(top_panel, height=5, width=52)
        self.login_amount.place(x=10, y=10)
        self.complex = BooleanVar()
        self.complex.set(False)
        self.login_complex = Checkbutton(top_panel, text="complex", var=self.complex)
        self.login_complex.place(x=10, y=100)
        self.login_create = Button(top_panel, text="Create", command=lambda: self.submit_request())
        self.login_create.place(x=100, y=100)
        top_panel.pack(expand=True, fill=BOTH)

    def show_popup(self):
        global app
        menu = Menu()
        menu.add_command(label="Copy selected items", command=lambda : self.copy_items())
        app.config(menu=menu)

    def copy_items(self):
        try:
            Item = self.login_list.item(self.login_list.focus())
            FunctionValue = Item.values()
            Array = list(FunctionValue)
            Login = Array[2]
            self.login_amount.insert(END, "Username : {} \nPassword : {} \n".format(Login[0], Login[1]))
            time.sleep(1)

        except:
            Msg = Toplevel(root)
            l1 = Label(Msg, text="You have to take something")
            l1.pack(fill=BOTH, expand=True)

    def submit_request(self):
        amount = self.login_amount.get("1.0", END)
        complex_bool = self.complex.get()
        try:
            amount = int(amount)
        except:
            amount = 1
        self.controller.get_new_logins(amount, complex_bool)

    def update_logins(self, login):
        username, password = login
        self.login_list.insert('', END, values=[username, password])

class Controller(object):
    def __init__(self):
        global app
        app.title("Login Generator")
        app.geometry("450x300")
        self.view = View(self)
        self.model = Model(self)

    def get_new_logins(self, amount, complex_bool):
        self.model.generate_login(amount, complex_bool)

    def set_new_logins(self, logins):
        self.view.update_logins(logins)


class Model(threading.Thread):
    def __init__(self, controller):
        threading.Thread.__init__(self, target=self.main)
        self.controller = controller
        self.queue = queue.Queue()
        self.start()

    def main(self):
        while True:
            amount, complex_bool = self.queue.get()
            username = login_generator.generate_username()
            password = login_generator.generate_password(12, complex_bool)
            logins = (username, password)
            self.controller.set_new_logins(logins)
            time.sleep(1)

    def generate_login(self, amount, complex_bool):
        for i in range(0, amount):
            self.queue.put((amount, complex_bool))


if __name__ == "__main__":
    app = Tk()
    mvc = Controller()
    app.mainloop()

If you want to add a scrollbar in the project.

  from tkinter import *
from tkinter import ttk
import login_generator
import time
import threading
import queue


class View(Frame):
    def __init__(self, controller):
        Frame.__init__(self)
        self.controller = controller
        self.full_window = PanedWindow(self, orient=VERTICAL)
        self.full_window.pack(fill=BOTH, expand=True)
        self.create_top_panel()
        self.create_bottom_panel()
        self.pack(fill=BOTH, expand=True)

    def create_bottom_panel(self):
        bottom_panel = PanedWindow(self.full_window, bg="green")
        bottom_panel.pack(fill=BOTH, side=BOTTOM)
        ##Scrollbar
        self.Scrollbar = Scrollbar(bottom_panel )
        self.Scrollbar.pack(side=RIGHT, fill= Y)
        # List
        self.login_list = ttk.Treeview(bottom_panel, columns=["username", "password"], show="headings", height=6, yscrollcommand= self.Scrollbar.set)
        self.login_list.pack()
        self.login_list.heading("username", text="Username")
        self.login_list.heading("password", text="Password")
        self.login_list.bind("<ButtonRelease-1>", lambda e: self.show_popup())
        self.Scrollbar.config(command= self.login_list.yview)


    def create_top_panel(self):
        top_panel = PanedWindow(self.full_window, bg="red")
        self.login_amount = Text(top_panel, height=5, width=52)
        self.login_amount.place(x=10, y=10)
        self.complex = BooleanVar()
        self.complex.set(False)
        self.login_complex = Checkbutton(top_panel, text="complex", var=self.complex)
        self.login_complex.place(x=10, y=100)
        self.login_create = Button(top_panel, text="Create", command=lambda: self.submit_request())
        self.login_create.place(x=100, y=100)
        top_panel.pack(expand=True, fill=BOTH)

    def show_popup(self):
        global app
        menu = Menu()
        menu.add_command(label="Copy selected items", command=lambda : self.copy_items())
        app.config(menu=menu)

    def copy_items(self):
        try:
            Item = self.login_list.item(self.login_list.focus())
            FunctionValue = Item.values()
            Array = list(FunctionValue)
            Login = Array[2]
            self.login_amount.insert(END, "Username : {} \nPassword : {} \n".format(Login[0], Login[1]))
            time.sleep(1)

        except:
            Msg = Toplevel(root)
            l1 = Label(Msg, text="You have to take something")
            l1.pack(fill=BOTH, expand=True)

    def submit_request(self):
        amount = self.login_amount.get("1.0", END)
        complex_bool = self.complex.get()
        try:
            amount = int(amount)
        except:
            amount = 1
        self.controller.get_new_logins(amount, complex_bool)

    def update_logins(self, login):
        username, password = login
        self.login_list.insert('', END, values=[username, password])

class Controller(object):
    def __init__(self):
        global app
        app.title("Login Generator")
        app.geometry("450x300")
        self.view = View(self)
        self.model = Model(self)

    def get_new_logins(self, amount, complex_bool):
        self.model.generate_login(amount, complex_bool)

    def set_new_logins(self, logins):
        self.view.update_logins(logins)


class Model(threading.Thread):
    def __init__(self, controller):
        threading.Thread.__init__(self, target=self.main)
        self.controller = controller
        self.queue = queue.Queue()
        self.start()

    def main(self):
        while True:
            amount, complex_bool = self.queue.get()
            username = login_generator.generate_username()
            password = login_generator.generate_password(12, complex_bool)
            logins = (username, password)
            self.controller.set_new_logins(logins)
            time.sleep(1)

    def generate_login(self, amount, complex_bool):
        for i in range(0, amount):
            self.queue.put((amount, complex_bool))

if __name__ == "__main__":
    app = Tk()
    mvc = Controller()
    app.mainloop()

Coding challenge - Login generator with GUI

Coding challenge - Trivial pursuit with GUI

WXpython

wxpython helloworld

The absolute most basic way to have a hello world GUI program up and running with wxpython is the following.

import wx

app = wx.App()

frame = wx.Frame(None, title="hello world")

frame.Show()

app.MainLoop()

While it works we know better by now. We should include the if __name__ == "__main__" statement.

import wx


if __name__ == "__main__":
    app = wx.App()
    frame = wx.Frame(None, title="hello world")
    frame.Show()
    app.MainLoop()

The instance of wx.App is what will actually process our event loop and the wx.Frame is the content of our window. In the latter we will create our button and labels so should create our own class and inherit from it.

import wx


class MainWindow(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="hello world")
        self.Show()


if __name__ == "__main__":
    app = wx.App()
    win = MainWindow()
    app.MainLoop()

We can add content to the frame, such as labels, input boxes and buttons as follows. Note the first argument to the wx.StaticText creation. This is where we put the label into. It kind of works but we'll encounter a problem when we pack more visual objects into the same frame.

import wx


class MainWindow(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="hello world")
        self.label = wx.StaticText(self, label="this is a label")
        self.Show()


if __name__ == "__main__":
    app = wx.App()
    win = MainWindow()
    app.MainLoop()

Let's try to put multiple visual object into the same frame.

import wx


class MainWindow(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="hello world")
        self.label = wx.StaticText(self, label="this is a label")
        self.input = wx.TextCtrl(self)
        self.button = wx.Button(self)
        self.Show()


if __name__ == "__main__":
    app = wx.App()
    win = MainWindow()
    app.MainLoop()

You see how they are stacked one on top of the other? We can overcome this problem with sizers. There are multiple ones we can use but let's dive into a first one called a box sizer.

import wx


class MainWindow(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="hello world")
        self.panel = wx.Panel(self)
        self.box = wx.BoxSizer()
        self.label = wx.StaticText(self.panel, label="this is a label")
        self.input = wx.TextCtrl(self.panel)
        self.button = wx.Button(self.panel, label="I'm clickable!")
        self.box.Add(self.label)
        self.box.Add(self.input)
        self.box.Add(self.button)
        self.panel.SetSizer(self.box)
        self.Show()


if __name__ == "__main__":
    app = wx.App()
    win = MainWindow()
    app.MainLoop()

This is looking better! But it requires some explanation though. Let's break it down. The wx.Frame is your window in which you create a wx.Panel which is used to draw thing to. To this panel you're adding three different objects (wx.StaticText, wx.TextCtrl and wx.Button) each with or without their own settings (such as a label or not). Next you add these three objects to the wx.BoxSizer and you tell the panel to use this box as sizer. It probably looks a bit convoluted but this is how most GUI libraries work internally.

  1. You create a frame
  2. Within this frame you create a drawing area and set some form of automatic layout to it (the wx.BoxSizer).
  3. You create the visual elements you want and add them one by one to the drawing area.
  4. Success

Now how do we link user input to code actions? This is a complicated way of saying actually do something when I click the damn button! For this we'll need to create a function, or better yet a method to the wx.Frame objects. Each time we click the button, that method will be called. Because it is a method it has access to self so it can modify anything within the scope of the instance.

import wx
import random 


class MainWindow(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="hello world")
        self.panel = wx.Panel(self)
        self.box = wx.BoxSizer()
        self.label = wx.StaticText(self.panel, label="this is a label")
        self.input = wx.TextCtrl(self.panel)
        self.button = wx.Button(self.panel, label="I'm clickable!")
        self.button.Bind(wx.EVT_BUTTON, self.set_label_value)
        self.box.Add(self.label)
        self.box.Add(self.input)
        self.box.Add(self.button)
        self.panel.SetSizer(self.box)
        self.Show()

    def set_label_value(self, event):
        number = random.randint(0, 100)
        self.label.SetLabel("{}".format(number))


if __name__ == "__main__":
    app = wx.App()
    win = MainWindow()
    app.MainLoop()

We can use the same idea to grab input from the textbox.

import wx


class MainWindow(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="hello world")
        self.panel = wx.Panel(self)
        self.box = wx.BoxSizer()
        self.label = wx.StaticText(self.panel, label="this is a label")
        self.input = wx.TextCtrl(self.panel)
        self.button = wx.Button(self.panel, label="I'm clickable!")
        self.button.Bind(wx.EVT_BUTTON, self.set_label_value)
        self.box.Add(self.label)
        self.box.Add(self.input)
        self.box.Add(self.button)
        self.panel.SetSizer(self.box)
        self.Show()

    def set_label_value(self, event):
        msg = self.input.GetValue()
        self.label.SetLabel("{}".format(msg))


if __name__ == "__main__":
    app = wx.App()
    win = MainWindow()
    app.MainLoop()

wxpython guess the number

import wx
import random


class MainWindow(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="Guess the number")
        self.number = random.randint(0, 100)
        self.panel = wx.Panel(self)
        self.box = wx.BoxSizer(wx.VERTICAL)
        self.label = wx.StaticText(self.panel, label="I have a number in mind...")
        self.input = wx.TextCtrl(self.panel)
        self.button = wx.Button(self.panel, label="I'm clickable!")
        self.button.Bind(wx.EVT_BUTTON, self.set_label_value)
        self.box.Add(self.label)
        self.box.Add(self.input)
        self.box.Add(self.button)
        self.panel.SetSizer(self.box)
        self.Show()

    def set_label_value(self, event):
        result = self.input.GetValue()
        if result.isdigit():
            status, context = self.evaluate_user_number(int(result))
            self.label.SetLabel(context)
        else:
            self.label.SetLabel("I need numbers!")

    def evaluate_user_number(self, number):
        if number > self.number:
            return False, "my number is smaller"
        elif number < self.number:
            return False, "my number is bigger"
        elif number == self.number:
            return True, "You win!"


if __name__ == "__main__":
    app = wx.App()
    win = MainWindow()
    app.MainLoop()

MVC design pattern

A simple console only MVC. We'll add the GUI view in a bit.

import wx


class ConsoleView(object):
    """A view for console."""

    def select_task(self):
        """Asks which index to look up."""
        idx = input("which task do you want to see? ")
        return idx

    def show(self, task):
        """Displays the task to the console. This method is called from the
        controller."""
        print("your task: {}".format(task))

    def error(self, msg):
        """Prints error messages coming from the controller."""
        print("error: {}".format(msg))


class WxView(wx.Dialog):
    pass


class Model(object):
    """The model houses add data and should implement all methods related to
    adding, modifying and deleting tasks."""

    db = ["go to the shops", "dryhop beer", "drop of motorbike"]

    def get_task(self, idx):
        """Performs a task lookun into the database and returns it when found.
        If no task is found, it returns an error message that will be displayed
        in the view (via the controller)."""
        try:
            task = Model.db[idx]
        except IndexError:
            task = "task with {} not found!".format(idx)
        return task


class Controller(object):
    """Binds the model and the view together."""

    def __init__(self, view):
        self.model = Model()
        self.view = view 

    def run(self):
        """The controller's main function. Depending on what type of view is 
        selected, a different event loop is setup. Do note that the ConsoleView
        is not a real event loop, just a basic flow of action."""
        if self.view is ConsoleView:
            self.view = self.view()
            self._run_console_view()
        elif self.view is WxView:
            app = wx.App()
            self.view = self.view()
            self.view._set_controller(self)
            app.MainLoop()

    def get_task_from_model(self, idx):
        """Needed for the WxView to communicate with the controller."""
        task = self.model.get_task(idx)
        self.view.show_task(task)

    def _run_console_view(self):
        """Super simple event loop."""
        while True:
            try:
                idx = self.view.select_task()
                idx = int(idx)
            except Exception as e:
                self.view.error(e)
                continue
            task = self.model.get_task(idx)
            self.view.show(task)


if __name__ == "__main__":
    view = ConsoleView
    # view = WxView
    app = Controller(view)
    app.run()

And now with the implemented WxView class.

import wx


class ConsoleView(object):
    """A view for console."""

    def select_task(self):
        """Asks which index to look up."""
        idx = input("which task do you want to see? ")
        return idx

    def show(self, task):
        """Displays the task to the console. This method is called from the
        controller."""
        print("your task: {}".format(task))

    def error(self, msg):
        """Prints error messages coming from the controller."""
        print("error: {}".format(msg))


class WxView(wx.Dialog):
    """A view using a wx.Dialog window"""

    def __init__(self):
        wx.Dialog.__init__(self, None, title="Task Manager")
        self.panel = wx.Panel(self)
        self.box = wx.BoxSizer(wx.VERTICAL)
        self.task = wx.StaticText(self.panel, label="your task")
        self.idx = wx.SpinCtrl(self.panel)
        self.button = wx.Button(self.panel, label="submit")
        self.button.Bind(wx.EVT_BUTTON, self.select_task)
        self.box.Add(self.task, 0, wx.EXPAND, 1)
        self.box.Add(self.idx, 0, wx.EXPAND, 1)
        self.box.Add(self.button, 0, wx.EXPAND, 1)
        self.panel.SetSizer(self.box)
        self.Show()

    def _set_controller(self, controller):
        """Set the controller so the view can communicate it's requests to it
        and update it's values too."""
        self.controller = controller

    def select_task(self, event):
        """Gets the index to look up in the model and submits the request to
        the controller."""
        idx = self.idx.GetValue()
        self.controller.get_task_from_model(idx)

    def show_task(self, task):
        """Updates the visual label in the view with the task. This method is
        called from the controller."""
        self.task.SetLabel(task)


class Model(object):
    """The model houses add data and should implement all methods related to
    adding, modifying and deleting tasks."""

    db = ["go to the shops", "dryhop beer", "drop of motorbike"]

    def get_task(self, idx):
        """Performs a task lookun into the database and returns it when found.
        If no task is found, it returns an error message that will be displayed
        in the view (via the controller)."""
        try:
            task = Model.db[idx]
        except IndexError:
            task = "task with {} not found!".format(idx)
        return task


class Controller(object):
    """Binds the model and the view together."""

    def __init__(self, view):
        self.model = Model()
        self.view = view 

    def run(self):
        """The controller's main function. Depending on what type of view is 
        selected, a different event loop is setup. Do note that the ConsoleView
        is not a real event loop, just a basic flow of action."""
        if self.view is ConsoleView:
            self.view = self.view()
            self._run_console_view()
        elif self.view is WxView:
            app = wx.App()
            self.view = self.view()
            self.view._set_controller(self)
            app.MainLoop()

    def get_task_from_model(self, idx):
        """Needed for the WxView to communicate with the controller."""
        task = self.model.get_task(idx)
        self.view.show_task(task)

    def _run_console_view(self):
        """Super simple event loop."""
        while True:
            try:
                idx = self.view.select_task()
                idx = int(idx)
            except Exception as e:
                self.view.error(e)
                continue
            task = self.model.get_task(idx)
            self.view.show(task)


if __name__ == "__main__":
    view = WxView
    app = Controller(view)
    app.run()

For a GUI only login generator an implementation without MVC could look a bit like this. Note that the actual calculation is done inside the window itself. This is not a good idea because we should separate responsibilities into classes!

import wx
import login_generator


class MainWindow(wx.Dialog):
    def __init__(self):
        wx.Dialog.__init__(self, None, title="Login Generator")
        self.full_window = wx.Panel(self)
        self.full_window_sizer = wx.BoxSizer(wx.VERTICAL)
        self.full_window_sizer.Add(self.create_top_panel(), 0, wx.EXPAND | wx.ALL, 20)
        self.full_window_sizer.Add(self.create_bottom_panel(), 0, wx.EXPAND, 0)
        self.full_window.SetSizer(self.full_window_sizer)
        self.Show()

    def create_bottom_panel(self):
        bottom_panel = wx.Panel(self.full_window)
        bottom_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.login_list = wx.ListCtrl(bottom_panel, style=wx.LC_REPORT)
        self.login_list.Bind(wx.EVT_RIGHT_UP, self.show_popup)
        self.login_list.InsertColumn(0, 'username', width=200)
        self.login_list.InsertColumn(1, 'password', width=200)
        bottom_panel_sizer.Add(self.login_list, 0, wx.EXPAND | wx.ALL, 200)
        return bottom_panel

    def create_top_panel(self):
        top_panel = wx.Panel(self.full_window)
        top_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.login_amount = wx.TextCtrl(top_panel)
        self.login_complex = wx.CheckBox(top_panel, label="complex")
        self.login_create = wx.Button(top_panel, label="Create")
        self.login_create.Bind(wx.EVT_BUTTON, self.add_login)
        top_panel_sizer.Add(self.login_amount, 1, wx.EXPAND|wx.ALL,0)
        top_panel_sizer.Add(self.login_complex, 1, wx.EXPAND|wx.ALL,0)
        top_panel_sizer.Add(self.login_create, 1, wx.EXPAND|wx.ALL,0)
        top_panel.SetSizer(top_panel_sizer)
        return top_panel
    
    def show_popup(self, event):
        menu = wx.Menu()
        menu.Append(1, "Copy selected items")
        menu.Bind(wx.EVT_MENU, self.copy_items, id=1)
        self.PopupMenu(menu)

    def copy_items(self, event):
        selected_items = []
        for i in range(self.login_list.GetItemCount()):
            if self.login_list.IsSelected(i):
                username = selected_items.append(
                        self.login_list.GetItem(i, 0).GetText()
                        )
                password = selected_items.append(
                        self.login_list.GetItem(i, 1).GetText()
                        )
        clipdata = wx.TextDataObject()
        clipdata.SetText("\n".join(selected_items))
        wx.TheClipboard.Open()
        wx.TheClipboard.SetData(clipdata)
        wx.TheClipboard.Close()

    def add_login(self, event):
        amount = self.login_amount.GetValue()
        complex = self.login_complex.GetValue()
        try:
            amount = int(amount)
        except:
            amount = 1
        for i in range(0, amount):
            username = login_generator.generate_username()
            password = login_generator.generate_password(12, complex)
            index = self.login_list.InsertItem(0, username)
            self.login_list.SetItem(index, 1, password)


if __name__ == "__main__":
    app = wx.App()
    win = MainWindow()
    app.MainLoop()

Now let's assume the generate username and password function take some calculation time. We'll add in a fake time.sleep to simulate.

import wx
import login_generator
import time


class MainWindow(wx.Dialog):
    def __init__(self):
        wx.Dialog.__init__(self, None, title="Login Generator")
        self.full_window = wx.Panel(self)
        self.full_window_sizer = wx.BoxSizer(wx.VERTICAL)
        self.full_window_sizer.Add(self.create_top_panel(), 0, wx.EXPAND | wx.ALL, 20)
        self.full_window_sizer.Add(self.create_bottom_panel(), 0, wx.EXPAND, 0)
        self.full_window.SetSizer(self.full_window_sizer)
        self.Show()

    def create_bottom_panel(self):
        bottom_panel = wx.Panel(self.full_window)
        bottom_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.login_list = wx.ListCtrl(bottom_panel, style=wx.LC_REPORT)
        self.login_list.Bind(wx.EVT_RIGHT_UP, self.show_popup)
        self.login_list.InsertColumn(0, 'username', width=200)
        self.login_list.InsertColumn(1, 'password', width=200)
        bottom_panel_sizer.Add(self.login_list, 0, wx.EXPAND | wx.ALL, 200)
        return bottom_panel

    def create_top_panel(self):
        top_panel = wx.Panel(self.full_window)
        top_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.login_amount = wx.TextCtrl(top_panel)
        self.login_complex = wx.CheckBox(top_panel, label="complex")
        self.login_create = wx.Button(top_panel, label="Create")
        self.login_create.Bind(wx.EVT_BUTTON, self.add_login)
        top_panel_sizer.Add(self.login_amount, 1, wx.EXPAND|wx.ALL,0)
        top_panel_sizer.Add(self.login_complex, 1, wx.EXPAND|wx.ALL,0)
        top_panel_sizer.Add(self.login_create, 1, wx.EXPAND|wx.ALL,0)
        top_panel.SetSizer(top_panel_sizer)
        return top_panel
    
    def show_popup(self, event):
        menu = wx.Menu()
        menu.Append(1, "Copy selected items")
        menu.Bind(wx.EVT_MENU, self.copy_items, id=1)
        self.PopupMenu(menu)

    def copy_items(self, event):
        selected_items = []
        for i in range(self.login_list.GetItemCount()):
            if self.login_list.IsSelected(i):
                username = selected_items.append(
                        self.login_list.GetItem(i, 0).GetText()
                        )
                password = selected_items.append(
                        self.login_list.GetItem(i, 1).GetText()
                        )
        clipdata = wx.TextDataObject()
        clipdata.SetText("\n".join(selected_items))
        wx.TheClipboard.Open()
        wx.TheClipboard.SetData(clipdata)
        wx.TheClipboard.Close()

    def add_login(self, event):
        amount = self.login_amount.GetValue()
        complex_bool = self.login_complex.GetValue()
        try:
            amount = int(amount)
        except:
            amount = 1
        for i in range(0, amount):
            username = login_generator.generate_username()
            password = login_generator.generate_password(12, complex_bool)
            time.sleep(1)
            index = self.login_list.InsertItem(0, username)
            self.login_list.SetItem(index, 1, password)


if __name__ == "__main__":
    app = wx.App()
    win = MainWindow()
    app.MainLoop()

A clear separation of responsabilities can be acchieved via an MVC pattern and a login library. The library code is quite straightforward and goes as follows. It's basically the same code we did before but with added try except blocks. Can you tell me why I added those?

import random
import string


def load_file(filename):
    """
    We load a file and make a list out of it. Note that the same function is 
    used for both files (both adjectives and subjects). Functions should be 
    made as generic as possible.

    There IS a problem you can fix, some logins will have spaces in them. Try
    to remove them in this function!
    """
    words = []
    with open(filename, "r") as fp:
        lines = fp.readlines()
        for line in lines:
            words.append(line.strip())          # what does strip() do, what does append() do? remember CTRL+Q!
    return words


def generate_username():
    """
    We'll generate a random pair of adjectives and subjects from two wordlists.
    You NEED to have both files in you python project for this to work! Note 
    the capitalize method call to make it all prettier...
    """
    try:
        adjectives = load_file("./adjectives.txt")
    except:
        adjectives = ["big", "funny", "normal", "red"]
    try:
        subjects = load_file("./subjects.txt")
    except:
        subjects = ["giraffe", "elephant", "cougar", "tiger"]
    adjective = random.choice(adjectives)
    subject = random.choice(subjects)
    username = adjective.capitalize() + subject.capitalize()
    return username


def generate_password(length=10, complictated=True):
    """
    We generate a password with default settings. You can overide these by
    changing the arguments in the function call.
    """
    password = ""
    if complictated:
        chars = string.ascii_letters + string.digits + string.punctuation
    else:
        chars = string.ascii_letters
    for i in range(0, length):
        password += random.choice(chars)
    return password


if __name__ == "__main__":
    # let's do some testing!
    username_test = generate_username()
    print(username_test)
    password_test = generate_password()
    print(password_test)

And now the GUI code nicely split up in a model, controller and a view. The overhead is quite large but it makes the code a lot more scalable.

import wx
import login_generator
import time
import threading
import queue


class View(wx.Dialog):
    def __init__(self, controller):
        wx.Dialog.__init__(self, None, title="Login Generator")
        self.controller = controller
        self.full_window = wx.Panel(self)
        self.full_window_sizer = wx.BoxSizer(wx.VERTICAL)
        self.full_window_sizer.Add(self.create_top_panel(), 0, wx.EXPAND | wx.ALL, 20)
        self.full_window_sizer.Add(self.create_bottom_panel(), 0, wx.EXPAND, 0)
        self.full_window.SetSizer(self.full_window_sizer)
        self.Show()

    def create_bottom_panel(self):
        bottom_panel = wx.Panel(self.full_window)
        bottom_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.login_list = wx.ListCtrl(bottom_panel, style=wx.LC_REPORT)
        self.login_list.Bind(wx.EVT_RIGHT_UP, self.show_popup)
        self.login_list.InsertColumn(0, 'username', width=200)
        self.login_list.InsertColumn(1, 'password', width=200)
        bottom_panel_sizer.Add(self.login_list, 0, wx.EXPAND | wx.ALL, 200)
        return bottom_panel

    def create_top_panel(self):
        top_panel = wx.Panel(self.full_window)
        top_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.login_amount = wx.TextCtrl(top_panel)
        self.login_complex = wx.CheckBox(top_panel, label="complex")
        self.login_create = wx.Button(top_panel, label="Create")
        self.login_create.Bind(wx.EVT_BUTTON, self.submit_request)
        top_panel_sizer.Add(self.login_amount, 1, wx.EXPAND|wx.ALL,0)
        top_panel_sizer.Add(self.login_complex, 1, wx.EXPAND|wx.ALL,0)
        top_panel_sizer.Add(self.login_create, 1, wx.EXPAND|wx.ALL,0)
        top_panel.SetSizer(top_panel_sizer)
        return top_panel
    
    def show_popup(self, event):
        menu = wx.Menu()
        menu.Append(1, "Copy selected items")
        menu.Bind(wx.EVT_MENU, self.copy_items, id=1)
        self.PopupMenu(menu)

    def copy_items(self, event):
        selected_items = []
        for i in range(self.login_list.GetItemCount()):
            if self.login_list.IsSelected(i):
                username = selected_items.append(
                        self.login_list.GetItem(i, 0).GetText()
                        )
                password = selected_items.append(
                        self.login_list.GetItem(i, 1).GetText()
                        )
        clipdata = wx.TextDataObject()
        clipdata.SetText("\n".join(selected_items))
        wx.TheClipboard.Open()
        wx.TheClipboard.SetData(clipdata)
        wx.TheClipboard.Close()

    def submit_request(self, event):
        amount = self.login_amount.GetValue()
        complex_bool = self.login_complex.GetValue()
        try:
            amount = int(amount)
        except:
            amount = 1
        self.controller.get_new_logins(amount, complex_bool)

    def update_logins(self, login):
        username, password = login
        index = self.login_list.InsertItem(0, username)
        self.login_list.SetItem(index, 1, password)


class Controller(object):
    def __init__(self):
        self.app = wx.App()
        self.view = View(self)
        self.model = Model(self)

    def get_new_logins(self, amount, complex_bool):
        self.model.generate_login(amount, complex_bool)

    def set_new_logins(self, logins):
        self.view.update_logins(logins)
        

class Model(threading.Thread):
    def __init__(self, controller):
        threading.Thread.__init__(self, target=self.main)
        self.controller = controller
        self.queue = queue.Queue()
        self.start()

    def main(self):
        while True:
            amount, complex_bool = self.queue.get()
            username = login_generator.generate_username()
            password = login_generator.generate_password(12, complex_bool)
            logins = (username, password)
            self.controller.set_new_logins(logins)
            time.sleep(1)

    def generate_login(self, amount, complex_bool):
        for i in range(0, amount):
            self.queue.put((amount, complex_bool))
            

if __name__ == "__main__":
    mvc = Controller()
    mvc.app.MainLoop()

Coding challenge - Login generator with GUI

Coding challenge - Trivial pursuit with GUI