tkinter review

This commit is contained in:
waldek 2022-04-27 23:16:39 +02:00
parent 382d0db2b9
commit dabb20053a
1 changed files with 340 additions and 355 deletions

View File

@ -3,134 +3,134 @@
## Tkinter helloworld ## Tkinter helloworld
The absolute most basic way to have a *hello world* GUI program up and running with Tkinter is the following. 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 ```python
from tkinter import * import tkinter as tk
root = Tk() root = tk.Tk()
root.title("hello world")
MyLabel = Label(root,text="hello world")
MyLabel.pack()
root.mainloop() root.mainloop()
``` ```
Tkinter have two popular architectures, the Tcl and Tk. This two architectures are different, they have their own functionality and their own official documentation.
We are gonna use the Tk architecture.
The Tk architecture is used to create a GUI widget. He adds a lot of custom commands, so the widget is very customizable. 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.
In the previous code,the `mainloop()` instruction allows us to open the main window and to not close immediately the window. The Tk architecture is used to create a GUI widget which has a lot of **methods** and **attributes** so it's quite customizable.
And then, the `Label()` method creates label in a layout. This method has many parameters to customize a label. The instruction `pack()` will be always call , this instruction can
add and adjust a widget. 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. While it works we know better by now.
We should include the `if __name__ == "__main__"` statement. We should include the `if __name__ == "__main__"` statement so let's do this!
```python ```python
from tkinter import * import tkinter as tk
if __name__ == "__main__":
root = Tk()
MyLabel = Label(root,text="hello world") if __name__ == "__main__":
MyLabel.pack() root = tk.Tk()
root.title("hello world")
root.mainloop() root.mainloop()
``` ```
The instance of `Tk()` is what will actually process our event loop and the `root` is the content of our window. We can customize `root` with instruction like geometry, title,etc. 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.
In the latter we will create our button and labels so should create our own class and inherit from it. To this instance we can attach other widgets and display them.
```python ```python
from tkinter import * import tkinter as tk
class MainWindow(Frame):
class Application(tk.Tk):
def __init__(self): def __init__(self):
Label.__init__(self, text="hello world") tk.Tk.__init__(self)
self.pack() self.title("hello world")
self.geometry("500x400")
if __name__ == "__main__": if __name__ == "__main__":
root = Tk() app = Application()
root.title("title of the window") app.mainloop()
root.geometry("500x300")
MainWindow()
root.mainloop()
``` ```
We can add content to the *frame*, such as labels, input boxes and buttons as follows. We can add content to this window, such as labels, input boxes and buttons as follows.
```python ```python
from tkinter import * import tkinter as tk
class MainWindow(Frame):
class Application(tk.Tk):
def __init__(self): def __init__(self):
Label.__init__(self, text="hello world") tk.Tk.__init__(self)
#Label self.title("hello world")
MyLabel = Label(self, text="This is a label") self.geometry("500x400")
MyLabel.pack() self.resizable(0, 0)
self.label = tk.Label(self, text="This is a label", bg="yellow")
self.label.pack()
self.config(bg="yellow")
self.pack()
if __name__ == "__main__": if __name__ == "__main__":
root = Tk() app = Application()
root.title("title of the window") app.mainloop()
root.geometry("500x300")
MainWindow()
root.mainloop()
``` ```
Let's try to put multiple visual object into the same frame. Let's try to put multiple visual object into the same window.
```python3 ```python3
from tkinter import * import tkinter as tk
class MainWindow(Frame):
class Application(tk.Tk):
def __init__(self): def __init__(self):
Label.__init__(self, text="hello world") tk.Tk.__init__(self)
#Label self.title("hello world")
MyLabel = Label(self, text="This is a label") self.geometry("500x400")
MyLabel.pack() self.label = tk.Label(text="This is a label")
#Button self.label.pack()
MyButton = Button(self, text="I'm clickable!")
MyButton.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()
self.config(bg="yellow")
self.pack()
if __name__ == "__main__": if __name__ == "__main__":
root = Tk() app = Application()
root.title("title of the window") app.mainloop()
root.geometry("500x300")
MainWindow()
root.mainloop()
``` ```
You see how they are *stacked* one on top of the other? You see how they are *stacked* one on top of the other?
We can overcome this problem with parameters of [pack()](https://wxpython.org/Phoenix/docs/html/sizers_overview.html). We can 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/).
We can also use other geometry managers like [grid()](https://www.pythontutorial.net/tkinter/tkinter-grid/) and [place()](https://www.pythontutorial.net/tkinter/tkinter-place/).
```python ```python
from tkinter import * import tkinter as tk
class MainWindow(Frame):
class Application(tk.Tk):
def __init__(self): def __init__(self):
Frame.__init__(self, master=None) tk.Tk.__init__(self)
MyPanel = PanedWindow.__init__(self, bg="Blue") self.title("hello world")
#Label self.geometry("500x400")
MyLabel = Label(MyPanel, text="this is a label", bg= "yellow") self.resizable(0, 0)
MyLabel.pack(fill="x") self.label = tk.Label(self, text="This is a label", bg="yellow")
#Bouton self.label.grid(column=0, row=0, sticky=tk.W, padx=5, pady=5)
MyButton = Button(MyPanel, text="I'm clickable!")
MyButton.place(x=10, y=50) self.button = tk.Button(self, text="I'm clickable!")
self.pack(fill=BOTH,expand=True) 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__": if __name__ == "__main__":
root = Tk() app = Application()
root.title("this is the title of the window") app.mainloop()
root.geometry("500x300")
win = MainWindow()
root.mainloop()
``` ```
This is looking better! This is looking better!
@ -152,137 +152,179 @@ 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. Because it is a *method* it has access to *self* so it can modify *anything* within the scope of the instance.
```python ```python
from tkinter import * import tkinter as tk
class MainWindow(Frame):
class Application(tk.Tk):
def __init__(self): def __init__(self):
Frame.__init__(self, master=None) tk.Tk.__init__(self)
MyPanel = PanedWindow.__init__(self, bg="Blue") self.title("hello world")
#Label self.geometry("500x400")
MyLabel = Label(MyPanel, text="this is a label", bg= "yellow") self.resizable(0, 0)
MyLabel.pack(fill="x") self.label = tk.Label(self, text="This is a label", bg="yellow")
#Bouton self.label.pack()
MyButton = Button(MyPanel, text="I'm clickable!", command=lambda : self.ButtonEnable(MyLabel))
MyButton.place(x=10, y=50)
self.pack(fill=BOTH,expand=True)
def ButtonEnable(self, Label): self.button = tk.Button(self, text="I'm clickable!", command=self.clicked)
global number self.button.pack()
number += 1
counter = "You have press the button {} time".format(number)
Label.config(text=counter)
self.output = tk.Label(self, text="I'm a second label")
self.output.pack()
number=0 self.config(bg="yellow")
if __name__ == "__main__": def clicked(self):
root = Tk() print("hello world!")
root.title("this is the title of the window")
root.geometry("500x300")
win = MainWindow()
root.mainloop()
```
We can use the same *idea* to grab input from the textbox.
```python
from tkinter import *
class MainWindow(Frame):
def __init__(self):
Frame.__init__(self, master=None)
MyPanel = PanedWindow.__init__(self, bg="Blue")
#Label
MyLabel = Label(MyPanel, text="this is a label", bg= "yellow")
MyLabel.pack(fill="x")
#TextBox
MyEntry = Entry(MyPanel)
MyEntry.place(x=200,y=50)
#Bouton
MyButton = Button(MyPanel, text="I'm clickable!", command=lambda : self.ButtonEnable(MyLabel,MyEntry))
MyButton.place(x=10, y=50)
self.pack(fill=BOTH,expand=True)
def ButtonEnable(self, MyLabel, MyEntry):
MyText = MyEntry.get()
MyLabel.config(text=MyText)
if __name__ == "__main__": if __name__ == "__main__":
root = Tk() app = Application()
root.title("this is the title of the window") app.mainloop()
root.geometry("500x300")
win = MainWindow()
root.mainloop()
``` ```
## Tkinter guess the number 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 ```python
import time import tkinter as tk
from tkinter import *
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 import random
class MainWindow(Frame):
class Application(tk.Tk):
def __init__(self): def __init__(self):
Frame.__init__(self, master=None, bg="white") tk.Tk.__init__(self)
MyPanel = PanedWindow.__init__(self) self.title("hello world")
self.geometry("500x400")
self.resizable(0, 0)
MyNumber = random.randint(0, 100) self.number = random.randint(0, 10)
#Label self.header = tk.Label(text="I have a number in mind...")
self.MyLabel = Label(MyPanel, text="I have a number in mind...", bg= "blue") self.header.grid(column=0, row=0, sticky=tk.W)
self.MyLabel.pack(fill="x", ipadx=25, ipady=20)
#TextBox
MyEntry = Entry(MyPanel)
MyEntry.place(x=200,y=90)
#Bouton
MyButton = Button(MyPanel, text="I'm clickable!", command=lambda : self.ButtonEnable(MyEntry, MyNumber))
MyButton.place(x=10, y=90)
self.pack(fill=BOTH,expand=True) 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)
def ButtonEnable(self, MyEntry, MyNumber): self.button = tk.Button(self, text="check", command=self.clicked)
if self.IsCorrect(MyEntry.get()): self.button.grid(column=0, row=2, sticky=tk.W)
number = int(MyEntry.get())
if number != MyNumber: self.output = tk.Label(self, text="...")
self.GameOver(number, MyNumber) 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: else:
self.Win() if number < self.number:
else: msg = "my number is bigger"
self.MyLabel.config(text="I need numbers!") elif number > self.number:
msg = "my number is smaller"
def GameOver(self, number, MyNumber): elif number == self.number:
if number > MyNumber: msg = "bingo!"
self.MyLabel.config(text="My number is smaller") self.output.config(text=msg)
else:
self.MyLabel.config(text="My number is bigger")
def Win(self):
self.MyLabel.config(text="You WIN!")
def IsCorrect(self, MyEntry):
x = str(MyEntry)
if x.isdigit() == True:
return True
else:
return False
if __name__ == "__main__": if __name__ == "__main__":
root = Tk() app = Application()
root.title("Guess the number") app.mainloop()
root.geometry("500x300")
win = MainWindow()
root.mainloop()
``` ```
</details>
## MVC design pattern ## MVC design pattern
@ -290,215 +332,152 @@ A simple console only MVC.
We'll add the GUI view in a bit. We'll add the GUI view in a bit.
```python ```python
from tkinter import * import tkinter as tk
class ConsoleView(object): class ConsoleView(object):
"""A view for console.""" """A view for console."""
def select_task(self): def __init__(self, controller):
"""Asks which index to look up."""
idx = input("which task do you want to see? ")
return idx
def show(self, task):
"""Displays the task to the console. This method is called from the
controller."""
print("your task: {}".format(task))
def error(self, msg):
"""Prints error messages coming from the controller."""
print("error: {}".format(msg))
class Model(object):
"""The model houses add data and should implement all methods related to
adding, modifying and deleting tasks."""
db = ["go to the shops", "dryhop beer", "drop of motorbike"]
def get_task(self, idx):
"""Performs a task lookun into the database and returns it when found.
If no task is found, it returns an error message that will be displayed
in the view (via the controller)."""
try:
task = Model.db[idx]
except IndexError:
task = "task with {} not found!".format(idx)
return task
class Controller(object):
"""Binds the model and the view together."""
def __init__(self, view):
self.model = Model()
self.view = view
def run(self):
"""The controller's main function. Depending on what type of view is
selected, a different event loop is setup. Do note that the ConsoleView
is not a real event loop, just a basic flow of action."""
if self.view is ConsoleView:
self.view = self.view()
self._run_console_view()
elif self.view is TkinterView:
root = Tk()
root.title("Task Manager")
root.geometry("500x300")
self.view = self.view()
self.view._set_controller(self)
root.mainloop()
def get_task_from_model(self, idx):
"""Needed for the TkinterView to communicate with the controller."""
task = self.model.get_task(idx)
self.view.show_task(task)
def _run_console_view(self):
"""Super simple event loop."""
while True:
try:
idx = self.view.select_task()
idx = int(idx)
except Exception as e:
self.view.error(e)
continue
task = self.model.get_task(idx)
self.view.show(task)
if __name__ == "__main__":
view = ConsoleView
app = Controller(view)
app.run()
```
And now with the implemented `TkinterView` class.
```python
from tkinter import *
class ConsoleView(object):
"""A view for console."""
def select_task(self):
"""Asks which index to look up."""
idx = input("which task do you want to see? ")
return idx
def show(self, task):
"""Displays the task to the console. This method is called from the
controller."""
print("your task: {}".format(task))
def error(self, msg):
"""Prints error messages coming from the controller."""
print("error: {}".format(msg))
class TkinterView(Frame):
"""A view using a wx.Dialog window"""
def __init__(self):
Frame.__init__(self, master=None)
#Panel
self.panel = PanedWindow(self, bg="green")
self.panel.pack(fill=BOTH, expand=True)
#Task Label
self.task = Label(self.panel, text="your task")
self.task.pack(expand=True)
#SpinBox
self.idx = Spinbox(self.panel, from_=0, to=2, wrap=True )
self.idx.pack(side= TOP)
#Button
self.button = Button(self.panel, text="submit", command=lambda : self.select_task())
self.button.pack(ipadx=60, ipady=30)
self.pack(fill=BOTH, expand=True)
def _set_controller(self, controller):
"""Set the controller so the view can communicate it's requests to it
and update it's values too."""
self.controller = controller self.controller = controller
def select_task(self): def select_task(self):
"""Gets the index to look up in the model and submits the request to """Asks which index to look up."""
the controller.""" idx = input("which task do you want to see? ")
idx = self.idx.get() return idx
self.controller.get_task_from_model(idx)
def show_task(self, task): def show_message(self, msg):
"""Updates the visual label in the view with the task. This method is print("MSG: {}".format(msg))
called from the controller."""
self.task.config(text=task) 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): class Model(object):
"""The model houses add data and should implement all methods related to """The model houses add data and should implement all methods related to
adding, modifying and deleting tasks.""" adding, modifying and deleting tasks."""
db = ["go to the shops", "dryhop beer", "drop of motorbike"] 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): def get_task(self, idx):
"""Performs a task lookun into the database and returns it when found. """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 If no task is found, it returns an error message that will be displayed
in the view (via the controller).""" in the view (via the controller)."""
try: try:
task = Model.db[int(idx)] task = self.db[idx]
except IndexError: except IndexError:
task = "task with {} not found!".format(idx) task = None
return task return task
class Controller(object): class Controller(object):
"""Binds the model and the view together.""" """Binds the model and the view together."""
def __init__(self, view): def __init__(self, model):
self.model = Model() self.model = model
def set_view(self, view):
self.view = view self.view = view
if isinstance(view, TkinterView):
self.request_number_of_tasks_from_model()
def run(self): def request_number_of_tasks_from_model(self):
"""The controller's main function. Depending on what type of view is number = len(self.model)
selected, a different event loop is setup. Do note that the ConsoleView self.view.show_message("you have {} tasks".format(number))
is not a real event loop, just a basic flow of action."""
if self.view is ConsoleView:
self.view = self.view()
self._run_console_view()
elif self.view is TkinterView:
root = Tk()
root.title("Task Manager")
root.geometry("500x300")
self.view = self.view()
self.view._set_controller(self)
root.mainloop()
def get_task_from_model(self, idx): def request_task_from_model(self, idx):
"""Needed for the TkinterView to communicate with the controller.""" """Needed for the ConsoleView and TkinterView to communicate with the controller."""
task = self.model.get_task(idx)
self.view.show_task(task)
def _run_console_view(self):
"""Super simple event loop."""
while True:
try:
idx = self.view.select_task()
idx = int(idx)
except Exception as e:
self.view.error(e)
continue
task = self.model.get_task(idx) task = self.model.get_task(idx)
if task is None:
self.view.error("task not found!")
else:
self.view.show(task) self.view.show(task)
if __name__ == "__main__": if __name__ == "__main__":
view = TkinterView model = Model()
app = Controller(view) controller = Controller(model)
app.run() 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. 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. Note that the actual calculation is done inside the window itself.
This is not a good idea because we should separate responsibilities into classes! This is not a good idea because we should separate responsibilities into classes!
@ -964,10 +943,16 @@ if __name__ == "__main__":
app.mainloop() app.mainloop()
```` ````
-->
## Coding challenge - Login generator with GUI ## Coding challenge - Login generator with GUI
TODO
## Coding challenge - Trivial pursuit with GUI ## Coding challenge - Trivial pursuit with GUI
TODO
# WXpython # WXpython
## wxpython helloworld ## wxpython helloworld