1862 lines
58 KiB
Markdown
1862 lines
58 KiB
Markdown
# tkinter
|
|
|
|
## Tkinter helloworld
|
|
|
|
The absolute most basic way to have a *hello world* GUI program up and running with Tkinter is the following.
|
|
It creates an *application*, sets the *title* as `hello world` and enters the **main eventloop**.
|
|
|
|
```python
|
|
import tkinter as tk
|
|
|
|
root = tk.Tk()
|
|
root.title("hello world")
|
|
|
|
root.mainloop()
|
|
```
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
While it works we know better by now.
|
|
We should include the `if __name__ == "__main__"` statement so let's do this!
|
|
|
|
```python
|
|
import tkinter as tk
|
|
|
|
if __name__ == "__main__":
|
|
root = tk.Tk()
|
|
root.title("hello world")
|
|
|
|
root.mainloop()
|
|
```
|
|
|
|
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.
|
|
|
|
```python
|
|
import tkinter as tk
|
|
|
|
|
|
class Application(tk.Tk):
|
|
def __init__(self):
|
|
tk.Tk.__init__(self)
|
|
self.title("hello world")
|
|
self.geometry("500x400")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = Application()
|
|
app.mainloop()
|
|
```
|
|
|
|
## Adding widgets
|
|
|
|
We can add content to this window, such as labels, input boxes and buttons as follows.
|
|
|
|
|
|
```python
|
|
import tkinter as tk
|
|
|
|
|
|
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()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = Application()
|
|
app.mainloop()
|
|
```
|
|
|
|
Let's try to put multiple visual object into the same window.
|
|
|
|
```python3
|
|
import tkinter as tk
|
|
|
|
|
|
class Application(tk.Tk):
|
|
def __init__(self):
|
|
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()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = Application()
|
|
app.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) 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/).
|
|
|
|
```python
|
|
import tkinter as tk
|
|
|
|
|
|
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.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")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = Application()
|
|
app.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
|
|
import tkinter as tk
|
|
|
|
|
|
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()
|
|
|
|
self.button = tk.Button(self, text="I'm clickable!", command=self.clicked)
|
|
self.button.pack()
|
|
|
|
self.output = tk.Label(self, text="I'm a second label")
|
|
self.output.pack()
|
|
|
|
self.config(bg="yellow")
|
|
|
|
def clicked(self):
|
|
print("hello world!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = Application()
|
|
app.mainloop()
|
|
```
|
|
|
|
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.
|
|
|
|
```python
|
|
import tkinter as tk
|
|
|
|
|
|
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()
|
|
|
|
self.button = tk.Button(self, text="I'm clickable!", command=self.clicked)
|
|
self.button.pack()
|
|
|
|
self.output = tk.Label(self, text="I'm a second label")
|
|
self.output.pack()
|
|
|
|
self.config(bg="yellow")
|
|
self.counter = 0
|
|
|
|
def clicked(self):
|
|
self.counter += 1
|
|
self.output.config(text="butter has been clicked {} times".format(self.counter))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = Application()
|
|
app.mainloop()
|
|
```
|
|
|
|
🏃 Try it
|
|
---
|
|
|
|
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?
|
|
|
|
```python
|
|
import tkinter as tk
|
|
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
self.output = tk.Label(self, text="...")
|
|
self.output.grid(column=1, row=3, sticky=tk.W, padx=5, pady=5)
|
|
|
|
self.config(bg="yellow")
|
|
|
|
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))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = Application()
|
|
app.mainloop()
|
|
```
|
|
|
|
# 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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = Application()
|
|
app.mainloop()
|
|
```
|
|
</details>
|
|
|
|
<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>
|
|
|
|
## MVC design pattern
|
|
|
|
A simple console only MVC.
|
|
We'll add the GUI view in a bit.
|
|
|
|
```python
|
|
import tkinter as tk
|
|
|
|
|
|
class ConsoleView(object):
|
|
"""A view for console."""
|
|
|
|
def __init__(self, controller):
|
|
self.controller = controller
|
|
|
|
def select_task(self):
|
|
"""Asks which index to look up."""
|
|
idx = input("which task do you want to see? ")
|
|
return idx
|
|
|
|
def show_message(self, msg):
|
|
print("MSG: {}".format(msg))
|
|
|
|
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))
|
|
|
|
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)
|
|
|
|
|
|
class Model(object):
|
|
"""The model houses add data and should implement all methods related to
|
|
adding, modifying and deleting tasks."""
|
|
|
|
def __init__(self):
|
|
self.db = ["go to the shops", "dryhop beer", "drop of motorbike"]
|
|
|
|
def __len__(self):
|
|
return len(self.db)
|
|
|
|
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 = self.db[idx]
|
|
except IndexError:
|
|
task = None
|
|
return task
|
|
|
|
|
|
class Controller(object):
|
|
"""Binds the model and the view together."""
|
|
|
|
def __init__(self, model):
|
|
self.model = model
|
|
|
|
def set_view(self, view):
|
|
self.view = view
|
|
if isinstance(view, TkinterView):
|
|
self.request_number_of_tasks_from_model()
|
|
|
|
def request_number_of_tasks_from_model(self):
|
|
number = len(self.model)
|
|
self.view.show_message("you have {} tasks".format(number))
|
|
|
|
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:
|
|
self.view.show(task)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
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)
|
|
|
|
def error(self, msg):
|
|
"""Prints error messages coming from the controller."""
|
|
self.output.config(text=msg)
|
|
```
|
|
|
|
<!--
|
|
|
|
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()
|
|
````
|
|
|
|
-->
|
|
|
|
## Coding challenge - Login generator with GUI
|
|
|
|
TODO
|
|
|
|
## Coding challenge - Trivial pursuit with GUI
|
|
|
|
TODO
|
|
|
|
# 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()
|
|
```
|
|
|
|
## Coding challenge - Login generator with GUI
|
|
|
|
## Coding challenge - Trivial pursuit with GUI
|
|
|
|
|