python_introduction/learning_python3_gui.md

1862 lines
58 KiB
Markdown
Raw Permalink Normal View History

2022-04-27 11:14:45 +02:00
# tkinter
## Tkinter helloworld
The absolute most basic way to have a *hello world* GUI program up and running with Tkinter is the following.
2022-04-27 23:16:39 +02:00
It creates an *application*, sets the *title* as `hello world` and enters the **main eventloop**.
2022-04-27 11:14:45 +02:00
```python
2022-04-27 23:16:39 +02:00
import tkinter as tk
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
root = tk.Tk()
root.title("hello world")
2022-04-27 11:14:45 +02:00
root.mainloop()
```
2022-04-27 23:16:39 +02:00
Tkinter has two popular architectures, Tcl and Tk.
These two architectures are quite different as they each have their own functionality and their own official documentation.
We are going use the Tk architecture.
The Tk architecture is used to create a GUI widget which has a lot of **methods** and **attributes** so it's quite customizable.
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
In the previous code,the `mainloop()` method allows us to open the main window and to not close the window immediately .
This **event loop** will process all mouse event, button clicks, and changes.
2022-04-27 11:14:45 +02:00
While it works we know better by now.
2022-04-27 23:16:39 +02:00
We should include the `if __name__ == "__main__"` statement so let's do this!
2022-04-27 11:14:45 +02:00
```python
2022-04-27 23:16:39 +02:00
import tkinter as tk
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
if __name__ == "__main__":
root = tk.Tk()
root.title("hello world")
2022-04-27 11:14:45 +02:00
root.mainloop()
```
2022-04-27 23:16:39 +02:00
The instance of `tk.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.
To this instance we can attach other widgets and display them.
2022-04-27 11:14:45 +02:00
```python
2022-04-27 23:16:39 +02:00
import tkinter as tk
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
class Application(tk.Tk):
2022-04-27 11:14:45 +02:00
def __init__(self):
2022-04-27 23:16:39 +02:00
tk.Tk.__init__(self)
self.title("hello world")
self.geometry("500x400")
2022-04-27 11:14:45 +02:00
if __name__ == "__main__":
2022-04-27 23:16:39 +02:00
app = Application()
app.mainloop()
2022-04-27 11:14:45 +02:00
```
2022-05-02 23:05:15 +02:00
## Adding widgets
2022-04-27 23:16:39 +02:00
We can add content to this window, such as labels, input boxes and buttons as follows.
2022-04-27 11:14:45 +02:00
```python
2022-04-27 23:16:39 +02:00
import tkinter as tk
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
class Application(tk.Tk):
2022-04-27 11:14:45 +02:00
def __init__(self):
2022-04-27 23:16:39 +02:00
tk.Tk.__init__(self)
self.title("hello world")
self.geometry("500x400")
self.resizable(0, 0)
self.label = tk.Label(self, text="This is a label", bg="yellow")
self.label.pack()
2022-04-27 11:14:45 +02:00
if __name__ == "__main__":
2022-04-27 23:16:39 +02:00
app = Application()
app.mainloop()
2022-04-27 11:14:45 +02:00
```
2022-04-27 23:16:39 +02:00
Let's try to put multiple visual object into the same window.
2022-04-27 11:14:45 +02:00
```python3
2022-04-27 23:16:39 +02:00
import tkinter as tk
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
class Application(tk.Tk):
2022-04-27 11:14:45 +02:00
def __init__(self):
2022-04-27 23:16:39 +02:00
tk.Tk.__init__(self)
self.title("hello world")
self.geometry("500x400")
self.label = tk.Label(text="This is a label")
self.label.pack()
self.button = tk.Button(text="I'm clickable!")
self.button.pack()
self.output = tk.Label(text="I'm a second label")
self.output.pack()
2022-04-27 11:14:45 +02:00
if __name__ == "__main__":
2022-04-27 23:16:39 +02:00
app = Application()
app.mainloop()
2022-04-27 11:14:45 +02:00
```
You see how they are *stacked* one on top of the other?
2022-04-27 23:16:39 +02:00
We can overcome this problem with parameters of [pack()](https://wxpython.org/Phoenix/docs/html/sizers_overview.html) or we can use other geometry managers like [grid()](https://www.pythontutorial.net/tkinter/tkinter-grid/) and [place()](https://www.pythontutorial.net/tkinter/tkinter-place/).
2022-04-27 11:14:45 +02:00
```python
2022-04-27 23:16:39 +02:00
import tkinter as tk
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
class Application(tk.Tk):
2022-04-27 11:14:45 +02:00
def __init__(self):
2022-04-27 23:16:39 +02:00
tk.Tk.__init__(self)
self.title("hello world")
self.geometry("500x400")
self.resizable(0, 0)
self.label = tk.Label(self, text="This is a label", bg="yellow")
self.label.grid(column=0, row=0, sticky=tk.W, padx=5, pady=5)
self.button = tk.Button(self, text="I'm clickable!")
self.button.grid(column=1, row=0, sticky=tk.E, padx=5, pady=5)
self.output = tk.Label(self, text="I'm a second label")
self.output.grid(column=1, row=1, sticky=tk.E, padx=5, pady=5)
self.config(bg="yellow")
2022-04-27 11:14:45 +02:00
if __name__ == "__main__":
2022-04-27 23:16:39 +02:00
app = Application()
app.mainloop()
2022-04-27 11:14:45 +02:00
```
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
2022-04-27 23:16:39 +02:00
import tkinter as tk
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.title("hello world")
self.geometry("500x400")
self.resizable(0, 0)
self.label = tk.Label(self, text="This is a label", bg="yellow")
self.label.pack()
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.button = tk.Button(self, text="I'm clickable!", command=self.clicked)
self.button.pack()
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.output = tk.Label(self, text="I'm a second label")
self.output.pack()
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.config(bg="yellow")
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
def clicked(self):
print("hello world!")
2022-04-27 11:14:45 +02:00
if __name__ == "__main__":
2022-04-27 23:16:39 +02:00
app = Application()
app.mainloop()
2022-04-27 11:14:45 +02:00
```
2022-04-27 23:16:39 +02:00
We can see *hello world!* printed to the console with each click!
As we're using a `class` we have *access* to the instance of the application so we can use the `self.output` label to show our messages.
2022-04-27 11:14:45 +02:00
```python
2022-04-27 23:16:39 +02:00
import tkinter as tk
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.title("hello world")
self.geometry("500x400")
self.resizable(0, 0)
self.label = tk.Label(self, text="This is a label", bg="yellow")
self.label.pack()
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.button = tk.Button(self, text="I'm clickable!", command=self.clicked)
self.button.pack()
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.output = tk.Label(self, text="I'm a second label")
self.output.pack()
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.config(bg="yellow")
self.counter = 0
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
def clicked(self):
self.counter += 1
self.output.config(text="butter has been clicked {} times".format(self.counter))
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
if __name__ == "__main__":
app = Application()
app.mainloop()
2022-04-27 11:14:45 +02:00
```
2022-04-27 23:16:39 +02:00
🏃 Try it
---
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
We can use the same *idea* to grab input from the input boxes called `tk.Entry`.
The code below *adds* two numbers but it's not working properly.
Can you fix it for me please?
2022-04-27 11:14:45 +02:00
```python
2022-04-27 23:16:39 +02:00
import tkinter as tk
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.title("hello world")
self.geometry("500x400")
self.resizable(0, 0)
self.label_one = tk.Label(self, text="Number 1:", bg="yellow")
self.label_one.grid(column=0, row=0, sticky=tk.W, padx=5, pady=5)
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.label_two = tk.Label(self, text="Number 2:", bg="yellow")
self.label_two.grid(column=0, row=1, sticky=tk.W, padx=5, pady=5)
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.input_one = tk.Entry(self)
self.input_one.grid(column=1, row=0, sticky=tk.W, padx=5, pady=5)
self.input_two = tk.Entry(self)
self.input_two.grid(column=1, row=1, sticky=tk.W, padx=5, pady=5)
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.button = tk.Button(self, text="add two numbers", command=self.clicked)
self.button.grid(column=0, row=3, sticky=tk.W, padx=5, pady=5)
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.output = tk.Label(self, text="...")
self.output.grid(column=1, row=3, sticky=tk.W, padx=5, pady=5)
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
self.config(bg="yellow")
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
def clicked(self):
number_one = self.input_one.get()
number_two = self.input_two.get()
total = number_one + number_two
self.output.config(text="sum: {}".format(total))
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
if __name__ == "__main__":
app = Application()
app.mainloop()
```
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
# Coding challenge - Guess the number
Can you code me a *guess the number* game but using `tkinter`?
<details>
<summary>Spoiler warning</summary>
```python
import tkinter as tk
import random
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.title("hello world")
self.geometry("500x400")
self.resizable(0, 0)
self.number = random.randint(0, 10)
self.header = tk.Label(text="I have a number in mind...")
self.header.grid(column=0, row=0, sticky=tk.W)
self.question = tk.Label(self, text="What's your guess?")
self.question.grid(column=0, row=1, sticky=tk.W)
self.input = tk.Entry(self)
self.input.grid(column=1, row=1, sticky=tk.W)
self.button = tk.Button(self, text="check", command=self.clicked)
self.button.grid(column=0, row=2, sticky=tk.W)
self.output = tk.Label(self, text="...")
self.output.grid(column=1, row=2, sticky=tk.W)
def _is_entry_digit(self):
number = self.input.get()
if number.isdigit():
number = int(number)
return number
def clicked(self):
number = self._is_entry_digit()
if not number:
msg = "numbers please..."
else:
if number < self.number:
msg = "my number is bigger"
elif number > self.number:
msg = "my number is smaller"
elif number == self.number:
msg = "bingo!"
self.output.config(text=msg)
2022-04-27 11:14:45 +02:00
if __name__ == "__main__":
2022-04-27 23:16:39 +02:00
app = Application()
app.mainloop()
2022-04-27 11:14:45 +02:00
```
2022-04-27 23:16:39 +02:00
</details>
2022-05-02 16:49:16 +02:00
<details>
<summary>With replay function</summary>
```python
import tkinter as tk
import random
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.title("hello world")
self.geometry("500x400")
self.resizable(0, 0)
self.header = tk.Label(text="I have a number in mind...")
self.header.grid(column=0, row=0, sticky=tk.W)
self.question = tk.Label(self, text="What's your guess?")
self.question.grid(column=0, row=1, sticky=tk.W)
self.input = tk.Entry(self)
self.input.grid(column=1, row=1, sticky=tk.W)
self.button = tk.Button(self, text="check", command=self.clicked)
self.button.grid(column=0, row=2, sticky=tk.W)
self.output = tk.Label(self, text="...")
self.output.grid(column=1, row=2, sticky=tk.W)
self.init_new_game()
def _is_entry_digit(self):
number = self.input.get()
if number.isdigit():
number = int(number)
return number
def compare_numbers(self):
number = self._is_entry_digit()
if not number:
msg = "numbers please..."
else:
if number < self.number:
msg = "my number is bigger"
elif number > self.number:
msg = "my number is smaller"
elif number == self.number:
msg = "bingo!"
self.won = True
self.button.config(text="play again!")
self.output.config(text=msg)
def init_new_game(self):
self.won = False
self.number = random.randint(0, 100)
print(self.number)
self.button.config(text="check")
self.output.config(text="")
self.input.delete(0, 'end')
def clicked(self):
if self.won:
self.init_new_game()
else:
self.compare_numbers()
if __name__ == "__main__":
app = Application()
app.mainloop()
```
</detail>
2022-04-27 23:16:39 +02:00
## MVC design pattern
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
A simple console only MVC.
We'll add the GUI view in a bit.
2022-04-27 11:14:45 +02:00
```python
2022-04-27 23:16:39 +02:00
import tkinter as tk
2022-04-27 11:14:45 +02:00
class ConsoleView(object):
"""A view for console."""
2022-04-27 23:16:39 +02:00
def __init__(self, controller):
self.controller = controller
2022-04-27 11:14:45 +02:00
def select_task(self):
"""Asks which index to look up."""
idx = input("which task do you want to see? ")
return idx
2022-04-27 23:16:39 +02:00
def show_message(self, msg):
print("MSG: {}".format(msg))
2022-04-27 11:14:45 +02:00
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))
2022-04-27 23:16:39 +02:00
def mainloop(self):
"""Super simple event loop."""
while True:
self.controller.request_number_of_tasks_from_model()
try:
idx = self.select_task()
idx = int(idx)
except Exception as e:
self.error(e)
continue
self.controller.request_task_from_model(idx)
2022-04-27 11:14:45 +02:00
class Model(object):
"""The model houses add data and should implement all methods related to
adding, modifying and deleting tasks."""
2022-04-27 23:16:39 +02:00
def __init__(self):
self.db = ["go to the shops", "dryhop beer", "drop of motorbike"]
def __len__(self):
return len(self.db)
2022-04-27 11:14:45 +02:00
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:
2022-04-27 23:16:39 +02:00
task = self.db[idx]
2022-04-27 11:14:45 +02:00
except IndexError:
2022-04-27 23:16:39 +02:00
task = None
2022-04-27 11:14:45 +02:00
return task
class Controller(object):
"""Binds the model and the view together."""
2022-04-27 23:16:39 +02:00
def __init__(self, model):
self.model = model
def set_view(self, view):
2022-04-27 11:14:45 +02:00
self.view = view
2022-04-27 23:16:39 +02:00
if isinstance(view, TkinterView):
self.request_number_of_tasks_from_model()
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
def request_number_of_tasks_from_model(self):
number = len(self.model)
self.view.show_message("you have {} tasks".format(number))
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
def request_task_from_model(self, idx):
"""Needed for the ConsoleView and TkinterView to communicate with the controller."""
task = self.model.get_task(idx)
if task is None:
self.view.error("task not found!")
else:
2022-04-27 11:14:45 +02:00
self.view.show(task)
if __name__ == "__main__":
2022-04-27 23:16:39 +02:00
model = Model()
controller = Controller(model)
view = ConsoleView(controller)
controller.set_view(view)
view.mainloop()
```
Below you can see a `tkinter` class that acts as a drop in replacement for the `ConsoleView` class.
```python
class TkinterView(tk.Tk):
def __init__(self, controller):
tk.Tk.__init__(self)
self.controller = controller
self.title("hello world")
self.geometry("500x400")
self.resizable(0, 0)
self.header = tk.Label()
self.header.grid(column=0, row=0, sticky=tk.W)
self.question = tk.Label(self, text="Which task do you want to see?")
self.question.grid(column=0, row=1, sticky=tk.W)
self.input = tk.Entry(self)
self.input.grid(column=1, row=1, sticky=tk.W)
self.button = tk.Button(self, text="lookup", command=self.clicked)
self.button.grid(column=0, row=2, sticky=tk.W)
self.output = tk.Label(self, text="...")
self.output.grid(column=1, row=2, sticky=tk.W)
def _is_entry_digit(self):
number = self.input.get()
if number.isdigit():
number = int(number)
return number
def clicked(self):
number = self._is_entry_digit()
if not number:
msg = "numbers please..."
else:
msg = self.controller.request_task_from_model(number)
def show_message(self, msg):
self.header.config(text=msg)
def show(self, task):
"""Displays the task to the console. This method is called from the
controller."""
self.output.config(text=task)
2022-04-27 11:14:45 +02:00
2022-04-27 23:16:39 +02:00
def error(self, msg):
"""Prints error messages coming from the controller."""
self.output.config(text=msg)
2022-04-27 11:14:45 +02:00
```
2022-04-27 23:16:39 +02:00
<!--
2022-04-27 11:14:45 +02:00
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("<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.
```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("<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?
```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("<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.
````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("<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()
````
2022-04-27 23:16:39 +02:00
-->
2022-04-27 11:19:31 +02:00
## Coding challenge - Login generator with GUI
2022-04-27 23:16:39 +02:00
TODO
2022-04-27 11:19:31 +02:00
## Coding challenge - Trivial pursuit with GUI
2022-04-27 23:16:39 +02:00
TODO
2022-04-27 11:19:31 +02:00
# WXpython
## wxpython helloworld
The absolute most basic way to have a *hello world* GUI program up and running with wxpython is the following.
```python
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.
```python
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.
```python
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.
```python
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.
```python
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](https://wxpython.org/Phoenix/docs/html/sizers_overview.html).
There are multiple ones we can use but let's dive into a first one called a **box sizer**.
```python
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.
```python
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.
```python
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
```python
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.
```python
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.
```python
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!
```python
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.
```python
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?
```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
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()
```
2022-04-27 11:14:45 +02:00
2022-04-27 11:19:31 +02:00
## Coding challenge - Login generator with GUI
2022-04-27 11:14:45 +02:00
2022-04-27 11:19:31 +02:00
## Coding challenge - Trivial pursuit with GUI
2022-04-27 11:14:45 +02:00