From adbdafef1ebfce73ba5471d4459b830a0a768d0e Mon Sep 17 00:00:00 2001 From: waldek Date: Wed, 27 Apr 2022 11:14:45 +0200 Subject: [PATCH] starts reorganise with tkinter --- learning_python3.md | 974 --------------------------------------- learning_python3_gui.md | 975 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 975 insertions(+), 974 deletions(-) create mode 100644 learning_python3_gui.md diff --git a/learning_python3.md b/learning_python3.md index 81b4e9e..6778473 100644 --- a/learning_python3.md +++ b/learning_python3.md @@ -3837,977 +3837,3 @@ TODO add a countdown timer to the multiple choice game * how apt does it's [progress bar](https://mdk.fr/blog/how-apt-does-its-fancy-progress-bar.html) -# GUI programming - -TODO event loop introduction - -## Tkinter helloworld - -The absolute most basic way to have a *hello world* GUI program up and running with Tkinter is the following. - -```python -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. - -```python -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. - -```python -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. - - -```python -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. - -```python3 -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()](https://wxpython.org/Phoenix/docs/html/sizers_overview.html). -We can also use other geometry managers like [grid()](https://www.pythontutorial.net/tkinter/tkinter-grid/) and [place()](https://www.pythontutorial.net/tkinter/tkinter-place/). - -```python -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. - -```python -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. - -```python -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 - -```python -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. - -```python -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. - -```python -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! - -```python -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("", 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. - -```python -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("", 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? - -```python -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. - -```python -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("", 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. - -````python - 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("", 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 diff --git a/learning_python3_gui.md b/learning_python3_gui.md new file mode 100644 index 0000000..83d7dee --- /dev/null +++ b/learning_python3_gui.md @@ -0,0 +1,975 @@ +# WXpython + + +# tkinter + +## Tkinter helloworld + +The absolute most basic way to have a *hello world* GUI program up and running with Tkinter is the following. + +```python +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. + +```python +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. + +```python +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. + + +```python +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. + +```python3 +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()](https://wxpython.org/Phoenix/docs/html/sizers_overview.html). +We can also use other geometry managers like [grid()](https://www.pythontutorial.net/tkinter/tkinter-grid/) and [place()](https://www.pythontutorial.net/tkinter/tkinter-place/). + +```python +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. + +```python +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. + +```python +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 + +```python +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. + +```python +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. + +```python +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! + +```python +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("", 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. + +```python +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("", 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? + +```python +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. + +```python +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("", 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. + +````python + 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("", 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