Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GUI with Python/Tk

GUI with Python/Tk

Sample Tk Demo app

This application shows a number of widgets available in Python Tk. The primary goal is to show a few features that we'll learn about on the following pages.

  • On recent versions of Ubuntu you might need to install python3-tk in addition to python3 using
sudo apt-get install python3-tk
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os


def scary_action():
    messagebox.showerror(title="Scary", message="Deleting hard disk. Please wait...")


def run_code():
    text = ""
    text += "Name: {}\n".format(name.get())
    text += "Password: {}\n".format(password.get())
    text += "Animal: {}\n".format(animal.get())
    text += "Country: {}\n".format(country.get())
    text += "Colors: "
    for ix in range(len(colors)):
        if colors[ix].get():
            text += color_names[ix] + " "
    text += "\n"

    selected = list_box.curselection()  # returns a tuple
    text += "Animals: "
    text += ', '.join([list_box.get(idx) for idx in selected])
    text += "\n"

    text += "Filename: {}\n".format(os.path.basename(filename_entry.get()))

    resp = messagebox.askquestion(title="Running with", message=f"Shall I start running with the following values?\n\n{text}")
    if resp == 'yes':
        output_window['state'] = 'normal'  # allow editing of the Text widget
        output_window.insert('end', f"{text}\n--------\n")
        output_window['state'] = 'disabled'  # disable editing
        output_window.see('end')  # scroll to the end as we make progress
        app.update()


def close_app():
    app.destroy()


app = tk.Tk()
app.title('Simple App')

menubar = tk.Menu(app)
app.config(menu=menubar)

menu1 = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="File", underline=0, menu=menu1)
menu1.add_separator()
menu1.add_command(label="Exit", underline=1, command=close_app)

top_frame = tk.Frame(app)
top_frame.pack(side="top")
pw_frame = tk.Frame(app)
pw_frame.pack(side="top")

# Simple Label widget:
name_title = tk.Label(top_frame, text=" Name:", width=10, anchor="w")
name_title.pack({"side": "left"})

# Simple Entry widget:
name = tk.Entry(top_frame)
name.pack({"side": "left"})
# name.insert(0, "Your name")

# Simple Label widget:
password_title = tk.Label(pw_frame, text=" Password:", width=10, anchor="w")
password_title.pack({"side": "left"})

# In order to hide the text as it is typed (e.g. for Passwords)
# set the "show" parameter:
password = tk.Entry(pw_frame)
password["show"] = "*"
password.pack({"side": "left"})

radios = tk.Frame(app)
radios.pack()
animal = tk.StringVar()
animal.set("Red")
my_radio = []
animals = ["Cow", "Mouse", "Dog", "Car", "Snake"]
for animal_name in animals:
    radio = tk.Radiobutton(radios, text=animal_name, variable=animal, value=animal_name)
    radio.pack({"side": "left"})
    my_radio.append(radio)


checkboxes = tk.Frame(app)
checkboxes.pack()
colors = []
my_checkbox = []
color_names = ["Red", "Blue", "Green"]
for color_name in color_names:
    color_var = tk.BooleanVar()
    colors.append(color_var)
    checkbox = tk.Checkbutton(checkboxes, text=color_name, variable=color_var)
    checkbox.pack({"side": "left"})
    my_checkbox.append(checkbox)

countries = ["Japan", "Korea", "Vietnam", "China"]

def country_change(event):
    pass
    #selection = country.current()
    #print(selection)
    #print(countries[selection])

def country_clicked():
    pass
    #print(country.get())

country = ttk.Combobox(app,  values=countries)
country.pack()
country.bind("<<ComboboxSelected>>", country_change)




list_box = tk.Listbox(app, selectmode=tk.MULTIPLE, height=4)
animal_names = ['Snake', 'Mouse', 'Elephant', 'Dog', 'Cat', 'Zebra', 'Camel', 'Spider']
for val in animal_names:
    list_box.insert(tk.END, val)
list_box.pack()

def open_filename_selector():
    file_path = filedialog.askopenfilename(filetypes=(("Any file", "*"),))
    filename_entry.delete(0, tk.END)
    filename_entry.insert(0, file_path)


filename_frame = tk.Frame(app)
filename_frame.pack()
filename_label = tk.Label(filename_frame, text="Filename:", width=10)
filename_label.pack({"side": "left"})
filename_entry = tk.Entry(filename_frame, width=60)
filename_entry.pack({"side": "left"})
filename_button = tk.Button(filename_frame, text="Select file", command=open_filename_selector)
filename_button.pack({"side": "left"})

output_frame = tk.Frame(app)
output_frame.pack()
output_window = tk.Text(output_frame, state='disabled')
output_window.pack()


buttons = tk.Frame(app)
buttons.pack()

scary_button = tk.Button(buttons, text="Don't click here!", fg="red", command=scary_action)
scary_button.pack({"side": "left"})

action_button = tk.Button(buttons, text="Run", command=run_code)
action_button.pack()

app.mainloop()

# TODO: key binding?
# TODO: Option Menu
# TODO: Scale
# TODO: Progressbar (after the deleting hard disk pop-up)
# TODO: Frame (with border?)

Simple file dialog

  • filedialog
  • askopenfilename

This is another way of using the Tk widgets. Here we have a plain command-line script, but instead of using the standard input() function of Python to ask for a filename it launches a Tk File-dialog widget to allow the user to browse and select a file.

from tkinter import filedialog

input_file_path = filedialog.askopenfilename(filetypes=(("Excel files", "*.xlsx"), ("CSV files", "*.csv"), ("Any file", "*")))
print(input_file_path)

input("Press ENTER to end the script...")

GUI Toolkits

  • Tk
  • GTK
  • Qt
  • wxWidgets
  • GUI

When creating an application there are several ways to interact with the user. You can accept command line parameters. You can interact on the Standard Output / Standard Input runnin in a Unix Shell or in the Command Prompt of Windows.

Many people, especially those who are using MS Windows, will frown upon both of those. They expect a Graphical User Interface (GUI) or maybe a web interface via their browser. In this chapter we are going to look at the possibility to create a desktop GUI.

There are plenty of ways to create a GUI in Python. The major ones were listed here, but there are many more. See the additional links.

In this chapter we are going to use the Tk Toolkit.

Installation

Tk in Python is actually a wrapper arount the implementation in Tcl.

Tcl/Tk usually comes installed with Python. All we need is basically the Tkinter Python module. In some Python installations (e.g. Anaconda), Tkinter is already installed. In other cases you might need to install it yourself. For examples on Ubuntu you can use apt to install it.

sudo apt-get install python3-tk

pip install tk

Python Tk Documentation

The documentation of Tk in Python does not cover all the aspects of Tk. If you are creating a complex GUI application you might need to dig in the documentation written for Tcl/Tk.

In the Unix world where Tk came from the various parts of a GUI application are called widgets. In the MS Windows world they are usually called controls. There are several commonly used Widgets. For example, Label, Button, Entry, Radiobutton, Checkbox. First we are going to see small examples with each one of these Widgets. Then we'll see how to combine them.

Python Tk Button

import tkinter as tk

app = tk.Tk()
app.title('Single Button')

button = tk.Button(app,
    text='Close',
    width=25,
    command=app.destroy,
    #bg='lightblue',
)
button.pack()

app.mainloop()

Python Tk Button with action

import tkinter as tk

def run_action():
    print("clicked")

app = tk.Tk()
app.title('Single Button')

action_button = tk.Button(app, text='Action', width=25, command=run_action)
action_button.pack()
#action_button.pack(side="left")

exit_button = tk.Button(app, text='Close', width=25, command=app.destroy)
exit_button.pack()

app.mainloop()

Python Tk Label

  • tkinter

  • Tk

  • Label

  • pack

  • mainloop

  • Label

import tkinter as tk

app = tk.Tk()
#app.title('Simple Label')

label = tk.Label(app, text='Some fixed text')
label.pack()

app.mainloop()

Python Tk Label - font size and color

  • config
  • color
  • font
import tkinter as tk

app = tk.Tk()
app.title('Label with font')

label = tk.Label(app, text='Some text with larger letters')
label.pack()
label.config(font=("Courier", 44))
label.config(fg="#0000FF")
label.config(bg="yellow")

app.mainloop()

Python Tk echo - change text of label

  • Entry

  • Label

  • Button

  • Entry window, Button, Label where to show the text we typed in.

import tkinter as tk

def echo():
    print('echo')
    label['text'] = entry.get()

app = tk.Tk()
app.title('Echo')

entry = tk.Entry(app)
entry.pack()

label = tk.Label(app, text="", width=10, anchor="w")
label.pack()


echo_button = tk.Button(app, text="Echo", command=echo)
echo_button.pack()

exit_button = tk.Button(app, text="Exit", fg="red", command=app.destroy)
exit_button.pack()

app.mainloop()

Python Tk Keybinding

  • bind
import tkinter as tk

app = tk.Tk()
app.title('Key binding')

label = tk.Label(app, text='Use the keyboard: (a, Ctr-b, Alt-c, F1, Alt-F4)')
label.config(font=("Courier", 44))
label.pack()

def pressed_a(event):
    print("pressed a")

def pressed_shift_a(event):
    print("pressed shift-a (aka. A)")


def pressed_control_b(event):
    print("pressed Ctr-b")

def pressed_alt_c(event):
    print("pressed Alt-c")

def pressed_f1(event):
    print("pressed F1")

app.bind("<a>", pressed_a)
app.bind("<A>", pressed_shift_a)
app.bind("<Control-b>", pressed_control_b)
app.bind("<Alt-c>", pressed_alt_c)
app.bind("<F1>", pressed_f1)


app.mainloop()
  • Alt-F4 is already bound to exit

Python Tk Mouse clicks

import tkinter as tk

app = tk.Tk()
app.title('Key binding')

label = tk.Label(app, text='Click the buttons of the mouse on the window')
label.config(font=("Courier", 44))
label.pack()

def action(event):
    print(dir(event))
    print(event.num)
    print(event.type)
    print(event.x)
    print(event.y)
    print()

app.bind("<ButtonPress-1>", action)
app.bind("<ButtonPress-2>", action)
app.bind("<ButtonPress-3>", action)
app.bind("<ButtonRelease-1>", action)
app.bind("<ButtonRelease-2>", action)
app.bind("<ButtonRelease-3>", action)

app.mainloop()

Python Tk Mouse movements (motions)

import tkinter as tk

app = tk.Tk()
app.title('Key binding')

label = tk.Label(app, text='Click the buttons of the mouse on the window')
label.config(font=("Courier", 44))
label.pack()

def action(event):
    #print(dir(event))
    print(f"{event.x} - {event.y}")

app.bind("<B1-Motion>", action)
app.bind("<B2-Motion>", action)
app.bind("<B3-Motion>", action)


app.mainloop()

Python Tk Entry (one-line text entry)

import tkinter as tk

app = tk.Tk()
app.title('Text Entry')

entry = tk.Entry(app)
entry.pack()

def clicked():
    print(entry.get())

button = tk.Button(app, text='Show', width=25, command=clicked)
button.pack()

exit_button = tk.Button(app, text='Close', width=25, command=app.destroy)
exit_button.pack()

app.mainloop()

Python Tk Entry for passwords and other secrets (hidden text)

import tkinter as tk

app = tk.Tk()
app.title('Text Entry')

entry = tk.Entry(app)
entry['show'] = '*'
entry.pack()

def clicked():
    print(entry.get())

button = tk.Button(app, text='Show', width=25, command=clicked)
button.pack()

exit_button = tk.Button(app, text='Close', width=25, command=app.destroy)
exit_button.pack()

app.mainloop()

Python Tk Checkbox

  • Checkbox
  • BooleanVar
import tkinter as tk

app = tk.Tk()
app.title('Checkbox')

var1 = tk.BooleanVar()
cb1 = tk.Checkbutton(app, text='male', variable=var1)
cb1.pack()

var2 = tk.BooleanVar()
cb2 = tk.Checkbutton(app, text='female', variable=var2)
cb2.pack()

#var1.set(True)

def clicked():
    print(var1.get())
    print(var2.get())
    #print(dir(cb1))

button = tk.Button(app, text='Show', width=25, command=clicked)
button.pack()

exit_button = tk.Button(app, text='Close', width=25, command=app.destroy)
exit_button.pack()

app.mainloop()

Python Tk Radiobutton

  • Radiobutton
import tkinter as tk

def run_action():
    print("clicked")
    print(count.get())

app = tk.Tk()
app.title('Radio button')

count = tk.IntVar()
#count.set(2)

my_radios = []
values = [(1, "One"), (2, "Two"), (3, "Three")]
for ix in range(len(values)):
    my_radios.append(tk.Radiobutton(app, text=values[ix][1], variable=count, value=values[ix][0]))
    my_radios[ix].pack()

action_button = tk.Button(app, text='Action', width=25, command=run_action)
action_button.pack()

exit_button = tk.Button(app, text='Close', width=25, command=app.destroy)
exit_button.pack()

app.mainloop()

Python Tk Listbox

  • Listbox
  • END
  • curselection
  • get
import tkinter as tk

app = tk.Tk()
app.title('List box')


def clicked():
    print("clicked")
    selected = box.curselection()  # returns a tuple
    if selected:
        first = selected[0]
        color = box.get(first)
        print(color)

box = tk.Listbox(app)
values = ['Red', 'Green', 'Blue', 'Purple']
for val in values:
    box.insert(tk.END, val)
box.pack()

button = tk.Button(app, text='Show', width=25, command=clicked)
button.pack()

exit_button = tk.Button(app, text='Close', width=25, command=app.destroy)
exit_button.pack()

app.mainloop()

Python Tk Listbox Multiple

  • selectmode
  • MULTIPLE
import tkinter as tk

app = tk.Tk()
app.title('List box')


def clicked():
    print("clicked")
    selected = box.curselection()  # returns a tuple
    for idx in selected:
        print(box.get(idx))

box = tk.Listbox(app, selectmode=tk.MULTIPLE, height=4)
values = ['Red', 'Green', 'Blue', 'Purple', 'Yellow', 'Orange', 'Black', 'White']
for val in values:
    box.insert(tk.END, val)
box.pack()

button = tk.Button(app, text='Show', width=25, command=clicked)
button.pack()

exit_button = tk.Button(app, text='Close', width=25, command=app.destroy)
exit_button.pack()

app.mainloop()

Python Tk Menubar

  • Menu

  • add_cascade

  • add_command

  • Menubar

  • Menu

  • underline sets the hot-key.

  • tearoff= (the default) allows floating menu by clicking on the dashed line.

  • enable/disable menu items.

  • Set actions via command on the menu items.

import tkinter as tk

app = tk.Tk()
app.title('Menu')

def run_new():
    print("new")

def run_exit():
    print("exit")
    app.destroy()

def enable_languages():
    menu2.entryconfig("Klingon", state="normal")
def disable_languages():
    menu2.entryconfig("Klingon", state="disabled")

def set_language(lang):
    print(lang)


menubar = tk.Menu(app)

menu1 = tk.Menu(menubar, tearoff=0)
menu1.add_command(label="New", command=run_new)
menu1.add_command(label="Enable language", command=enable_languages)
menu1.add_command(label="Disable language", command=disable_languages)
menu1.add_separator()
menu1.add_command(label="Exit", underline=1, command=run_exit)

menubar.add_cascade(label="File", underline=0, menu=menu1)

menu2 = tk.Menu(menubar, tearoff=1)
menu2.add_command(label="English")
menu2.add_command(label="Hebrew")
menu2.add_command(label="Spanish")
menu2.add_command(label="Klingon", state="disabled", command=lambda : set_language('Klingon'))
menu2.add_command(label="Hungarian")

menubar.add_cascade(label="Language", menu=menu2)

app.config(menu=menubar)

app.mainloop()

Python Tk Text

  • Text
import tkinter as tk

app = tk.Tk()
app.title('Text Editor')

text = tk.Text(app)
text.pack({"side": "bottom"})

app.mainloop()
  • text.delete(1.0, tk.END)

  • text.insert('end', content)

  • content = text.get(1.0, tk.END)

  • tk text

Python Tk Dialogs

  • Dialogs
  • Simple dialogs
  • Filedialogs
  • Message boxes

Python Tk simple dialog to get a single string, int, or float

import tkinter as tk
from tkinter import simpledialog

def main():
    app.title('Dialog')

    string_button = tk.Button(app, text='Ask for string', width=25, command=ask_for_string)
    string_button.pack()

    int_button = tk.Button(app, text='Ask for int', width=25, command=ask_for_int)
    int_button.pack()

    float_button = tk.Button(app, text='Ask for float', width=25, command=ask_for_float)
    float_button.pack()

    exit_button = tk.Button(app, text='Close', width=25, command=app.destroy)
    exit_button.pack()

    app.mainloop()

def ask_for_string():
    answer = simpledialog.askstring("Input", "Type a string:", parent=app)
    print(type(answer))
    print(answer)

def ask_for_int():
    answer = simpledialog.askinteger("Input", "Type an int:", parent=app)
    print(type(answer))
    print(answer)

def ask_for_float():
    answer = simpledialog.askfloat("Input", "Type a float", parent=app)
    print(type(answer))
    print(answer)


app = tk.Tk()

main()    


Python Tk Filedialog

  • filedialog

  • askopenfilename

  • asksaveasfilename

  • askopenfile

  • asksaveasfile

  • file dialogs

  • dialog

  • askopenfilename - returns path to file

  • asksaveasfilename - returns path to file

  • askopenfile - returns filehandle opened for reading

  • asksaveasfile - retutns filehandle opened for writing

  • Allow the listing of file-extension filters.

import tkinter as tk
from tkinter import filedialog

input_file_path = None
output_file_path = None

def run_process():
    print("Parameters:")
    print(f"in: {input_file_path}")
    print(f"out: {output_file_path}")

def close_app():
    print("Bye")
    app.destroy()

def select_input_file():
    global input_file_path
    input_file_path = filedialog.askopenfilename(filetypes=(("Excel files", "*.xlsx"), ("CSV files", "*.csv"), ("Any file", "*")))
    print(input_file_path)

def select_output_file():
    global output_file_path
    output_file_path = filedialog.asksaveasfilename(filetypes=(("Excel files", "*.xlsx"), ("CSV files", "*.csv"), ("Any file", "*")))
    print(output_file_path)

app = tk.Tk()
app.title('Convert file')

input_button = tk.Button(app, text='Select input file', command=select_input_file)
input_button.pack()

output_button = tk.Button(app, text='Select output file', command=select_output_file)
output_button.pack()

process_button = tk.Button(app, text='Process', width=25, command=run_process)
process_button.pack()

exit_button = tk.Button(app, text='Close', width=25, command=close_app)
exit_button.pack()

app.mainloop()


Python Tk messagebox

import tkinter as tk
from tkinter import messagebox

app = tk.Tk()
app.title('Menu')

def run_show_info():
    messagebox.showinfo(title = "Title", message = "Show info text")

def run_show_warning():
    messagebox.showwarning(title = "Title", message = "Show warning text")

def run_show_error():
    messagebox.showerror(title = "Title", message = "Show error text")

def run_ask_question():
    resp = messagebox.askquestion(title = "Title", message = "Can I ask you a question?")
    print(resp)  # "yes" / "no" (default "no")

def run_ask_okcancel():
    resp = messagebox.askokcancel(title = "Title", message = "Shall I do it?")
    print(resp)  # True / False (default = False)

def run_ask_retrycancel():
    resp = messagebox.askretrycancel(title = "Title", message = "Shall retry it?")
    print(resp)  # True / False (default = False)

def run_ask_yesno():
    resp = messagebox.askyesno(title = "Title", message = "Yes or No?")
    print(resp)  # True / False (default = False)

def run_ask_yesnocancel():
    resp = messagebox.askyesnocancel(title = "Title", message = "Yes, No, or Cancel?")
    print(resp)  # True / False / None (default = None)

def run_exit():
    app.destroy()


menubar = tk.Menu(app)

menu1 = tk.Menu(menubar, tearoff=0)
menu1.add_command(label="Info",    underline=0, command=run_show_info)
menu1.add_command(label="Warning", underline=0, command=run_show_warning)
menu1.add_command(label="Error",   underline=0, command=run_show_error)
menu1.add_separator()
menu1.add_command(label="Exit", underline=1, command=run_exit)

menubar.add_cascade(label="Show", underline=0, menu=menu1)

menu2 = tk.Menu(menubar, tearoff=0)
menu2.add_command(label="Question",           underline=0, command=run_ask_question)
menu2.add_command(label="OK Cancel",          underline=0, command=run_ask_okcancel)
menu2.add_command(label="Retry Cancel",       underline=0, command=run_ask_retrycancel)
menu2.add_command(label="Yes or No",          underline=0, command=run_ask_yesno)
menu2.add_command(label="Yes, No, or Cancel", underline=5, command=run_ask_yesnocancel)

menubar.add_cascade(label="Ask", underline=0, menu=menu2)

app.config(menu=menubar)

app.mainloop()

Python Tk - custom simple dialog with its own widgets and buttons

import tkinter as tk
from tkinter import simpledialog

class MyDialog(tk.simpledialog.Dialog):
    def __init__(self, parent, title):
        self.my_username = None
        self.my_password = None
        super().__init__(parent, title)

    def body(self, frame):
        # print(type(frame)) # tkinter.Frame
        self.my_username_label = tk.Label(frame, width=25, text="Username")
        self.my_username_label.pack()
        self.my_username_box = tk.Entry(frame, width=25)
        self.my_username_box.pack()

        self.my_password_label = tk.Label(frame, width=25, text="Password")
        self.my_password_label.pack()
        self.my_password_box = tk.Entry(frame, width=25)
        self.my_password_box.pack()
        self.my_password_box['show'] = '*'

        return frame

    def ok_pressed(self):
        # print("ok")
        self.my_username = self.my_username_box.get()
        self.my_password = self.my_password_box.get()
        self.destroy()

    def cancel_pressed(self):
        # print("cancel")
        self.destroy()


    def buttonbox(self):
        self.ok_button = tk.Button(self, text='OK', width=5, command=self.ok_pressed)
        self.ok_button.pack(side="left")
        cancel_button = tk.Button(self, text='Cancel', width=5, command=self.cancel_pressed)
        cancel_button.pack(side="right")
        self.bind("<Return>", lambda event: self.ok_pressed())
        self.bind("<Escape>", lambda event: self.cancel_pressed())

def mydialog(app):
    dialog = MyDialog(title="Login", parent=app)
    return dialog.my_username, dialog.my_password


def main():
    app.title('Dialog')

    string_button = tk.Button(app, text='Show', width=25, command=show_dialog)
    string_button.pack()

    exit_button = tk.Button(app, text='Close', width=25, command=app.destroy)
    exit_button.pack()

    app.mainloop()

def show_dialog():
    answer = mydialog(app)
    # print(type(answer)) # tuple
    print(answer)

app = tk.Tk()

main()    


Python Tk Combobox

  • Combobox
  • ComboboxSelected
  • bind
import tkinter as tk
from tkinter import ttk

countries = ["Japan", "Korea", "Vietnam", "China"]

app = tk.Tk()
app.title('Combo box')


def change(event):
    # VirtualEvent
    print("change")
    selection = country.current()
    print(selection)
    print(countries[selection])

def clicked():
    print("clicked")
    print(country.get())

country = ttk.Combobox(app, values=countries)
country.pack()
country.bind("<<ComboboxSelected>>", change)

button = tk.Button(app, text='Run', width=25, command=clicked)
button.pack()


app.mainloop()

Python Tk OptionMenu

  • OptionMenu
  • StringVar
import tkinter as tk

def run_action():
    color = color_var.get()
    print(color)

    size = size_var.get()
    print(size)

app = tk.Tk()
app.title('Option Menu')

color_var = tk.StringVar(app)
color_selector = tk.OptionMenu(app, color_var, "Red", "Green", "Blue")
color_selector.pack()

sizes = ("Small", "Medium", "Large")
size_var = tk.StringVar(app)
size_selector = tk.OptionMenu(app, size_var, *sizes)
size_selector.pack()

action_button = tk.Button(app, text='Action', width=25, command=run_action)
action_button.pack()

app.mainloop()

Python Tk Scale

  • Scale
  • HORIZONTAL
  • VERTICAL
import tkinter as tk

def run_action():
    h = scale_h.get()
    print(h)

    v = scale_v.get()
    print(v)

app = tk.Tk()
app.title('Scale')

scale_h = tk.Scale(app, from_=0, to=42, orient=tk.HORIZONTAL)
scale_h.pack()

scale_v = tk.Scale(app, from_=1, to=100, orient=tk.VERTICAL)
scale_v.pack()
scale_v.set(23)

action_button = tk.Button(app, text='Action', width=25, command=run_action)
action_button.pack()

app.mainloop()

Python Tk Progressbar

  • Progreessbar
import tkinter as tk
from tkinter import ttk

app = tk.Tk()
app.title('Single Button')

progressbar = ttk.Progressbar(app)
progressbar.pack()

def stop():
    progressbar.stop()

def start():
    app.after(10000, stop)
    progressbar.start(100)


button = tk.Button(app, text='Start', width=25, command=start)
button.pack()

exit_button = tk.Button(app, text='Close', width=25, command=app.destroy)
exit_button.pack()

app.mainloop()

Python Tk Frame

  • Frame
  • pack
  • side
import tkinter as tk

def close():
    app.destroy()

def clicked(val):
    entry.insert(tk.END, val)

app = tk.Tk()
app.title('Frame')

entry = tk.Entry(app)
entry.pack()

frames = {}
frames[1] = tk.Frame(app)
frames[1].pack(side="top")
frames[2] = tk.Frame(app)
frames[2].pack(side="top")
frames[3] = tk.Frame(app)
frames[3].pack(side="top")

btn = {}

btn["a"] = tk.Button(frames[1], text="a", width=25, command=lambda : clicked("a"))
btn["a"].pack(side="left")

btn["b"] = tk.Button(frames[1], text="b", width=25, command=lambda : clicked("b"))
btn["b"].pack(side="left")

btn["c"] = tk.Button(frames[2], text="c", width=25, command=lambda : clicked("c"))
btn["c"].pack(side="left")

btn["d"] = tk.Button(frames[2], text="d", width=25, command=lambda : clicked("d"))
btn["d"].pack(side="left")

close_btn = tk.Button(frames[3], text='Close', width=25, command=close)
close_btn.pack(side="right", expand=0)

app.mainloop()

  • width
  • side: left, right, top, bottom

Python Tk display images using Canvas

import tkinter as tk
import os
import sys
from PIL import Image, ImageTk

if len(sys.argv) != 2:
   exit(f"Usage: {sys.argv[0]} PATH_TO_IMAGE")
file_path = sys.argv[1]

app = tk.Tk()
app.title('Show image')


#img = tk.PhotoImage(file=file_path) # only seems to handle png and gif

# Tried with png, jpg, gif
img = ImageTk.PhotoImage(Image.open(file_path))
img_width = img.width()
img_height = img.height()

canvas = tk.Canvas(app, width = img_width, height = img_height)
canvas.pack()
img_on_canvas = canvas.create_image(img_width/2, img_height/2, image=img)

app.mainloop()

import tkinter as tk
from tkinter import filedialog
import os
from PIL import Image, ImageTk

file_path = None

app = tk.Tk()
app.title('Show image')

def run_open():
    global file_path
    file_path = filedialog.askopenfilename(filetypes=(("Image", "*.png *.jpg *.jpeg *.gif"), ("PNG", "*.png"), ("JPG", "*.jpg"), ("Any file", "*"),))
    if file_path and os.path.isfile(file_path):
        global img
        img = ImageTk.PhotoImage(Image.open(file_path))
        width = img.width()
        height = img.height()
        #print(width)
        #print(height)
        canvas.config(width=width, height=height)
        canvas.create_image(width/2, height/2, image=img)

def run_exit():
    print("exit")
    app.destroy()

menubar = tk.Menu(app)

menu1 = tk.Menu(menubar, tearoff=0)
menu1.add_command(label="Open", underline=0, command=run_open)
menu1.add_separator()
menu1.add_command(label="Exit", underline=1, command=run_exit)
menubar.add_cascade(label="File", underline=0, menu=menu1)

app.config(menu=menubar)

canvas = tk.Canvas(app, width = 600, height = 600)
canvas.pack()

app.mainloop()

Python Tk display Hebrew text (right to left)

import tkinter as tk

def reverse(txt):
    return txt[::-1]

app = tk.Tk()
app.title('כפתור')

label = tk.Label(app, text = reverse('שלום טיקי'))
label.pack()

button = tk.Button(app,
    text = reverse('סגור'),
    width=25,
    command=app.destroy,
    #bg='lightblue',
)
button.pack()

app.mainloop()

Python Tk Colorchooser

import tkinter as tk
import random
from tkinter.colorchooser import askcolor


def callback():
    red = random.randrange(0, 255)
    green = random.randrange(0, 255)
    blue = random.randrange(0, 255)
    print(f"red: {red}")
    print(f"green: {green}")
    print(f"blue: {blue}")
    color = f"#{red:02X}{green:02X}{blue:02X}"
    print(color)
    floating, hexa = askcolor(color=color,
                      title="Color Chooser")
    print(floating)
    print(hexa)
    if hexa is not None:
        print('red ', int(hexa[1:3], 16))
        print('green ', int(hexa[1:3], 16))
        print('blue ', int(hexa[1:3], 16))

root = tk.Tk()
tk.Button(root,
          text='Choose Color',
          fg="darkgreen",
          command=callback).pack(side=tk.LEFT, padx=10)
tk.Button(text='Quit',
          command=root.quit,
          fg="red").pack(side=tk.LEFT, padx=10)
tk.mainloop()

# Based on https://www.python-course.eu/tkinter_dialogs.php

Python Tk Timer event (after)

  • after

Schedule an event to be execute after N miliseconds.

import tkinter as tk
import datetime

def timer():
    print(datetime.datetime.now())
    app.after(1000, timer)

app = tk.Tk()
app.title('Timer')

app.after(1000, timer)

exit_button = tk.Button(app, text="Exit", fg="red", command=app.destroy)
exit_button.pack()

app.mainloop()

Python Tk Class-based Label + Button

import tkinter as tk

class MyApp():
    def __init__(self):
        self.app = tk.Tk()
        self.app.title('Class Based example')

        self.add_label()
        self.add_action_button()
        self.add_exit__button()

    def run(self):
        self.app.mainloop()

    def add_label(self):
        self.label = tk.Label(self.app,
                    text='Class Based example',
                    font=("Courier", 24),
                    fg="#0000FF",
                    bg="yellow",
                )
        self.label.pack()

    def add_exit__button(self):
        self.exit_button = tk.Button(self.app,
            text='Close',
            width=25,
            command=self.app.destroy,
            bg='lightblue',
        )
        self.exit_button.pack()

    def add_action_button(self):
        self.action_button = tk.Button(self.app,
                        text='Action',
                        width=25,
                        command=self.run_action)
        self.action_button.pack()

    def run_action(self):
        print(self) # MyApp object
        print("action")
        self.label['text'] = 'Action pressed'

MyApp().run()

Tk: Runner

  • Button
  • Text
import tkinter as tk
import time

# TODO: async or threading to run long-running other processes


class RunnerApp(tk.Frame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.pack()

        # Capture event when someone closes the window with the X on the top-right corner of the window
        parent.protocol("WM_DELETE_WINDOW", self.close_app)

        self.QUIT = tk.Button(self)
        self.QUIT["text"] = "QUIT"
        self.QUIT["fg"] = "red"
        self.QUIT["command"] = self.close_app
        self.QUIT.pack({"side": "left"})

        self.start_button = tk.Button(self)
        self.start_button["text"] = "Start"
        self.start_button["command"] = self.start
        self.start_button.pack({"side": "left"})

        self.stop_button = tk.Button(self)
        self.stop_button["text"] = "Stop"
        self.stop_button["command"] = self.stop
        self.stop_button.pack({"side": "left"})

        self.text = tk.Text(self, state='disabled')
        self.text.pack({"side": "bottom"})

        self.stop_process = False

    def close_app(self):
        print("close")
        self.stop_process = True
        self.quit()

    def stop(self):
        print("stop")
        self.stop_process = True
        self.add_line('stop')

    def start(self):
        self.stop_process = False
        for i in range(100):
            if self.stop_process:
                break
            self.add_line(str(i))
            time.sleep(0.1)

    def add_line(self, line):
        self.text['state'] = 'normal'  # allow editing of the Text widget
        self.text.insert('end', line + "\n")
        self.text['state'] = 'disabled'  # disable editing
        self.text.see('end')  # scroll to the end as we make progress
        self.update()  # update the content and allow other events (e.g. from stop and quit buttons) to take place


def main():
    tk_root = tk.Tk()
    app = RunnerApp(parent=tk_root)

    tk_root.lift()
    tk_root.call('wm', 'attributes', '.', '-topmost', True)
    tk_root.after_idle(tk_root.call, 'wm', 'attributes', '.', '-topmost', False)

    app.mainloop()


main()

Tk: Runner with threads

  • threading
  • queue
  • ctypes
import tkinter as tk
import time
import threading
import queue
import ctypes

class MyStopButton(Exception):
    pass

class ThreadedJob(threading.Thread):
    def __init__(self, que):
        self.que = que
        threading.Thread.__init__(self)
    def run(self):
        thread = threading.current_thread()
        print("Start thread {}".format(thread.name))
        try:
            for i in range(10):
                print(i)
                self.que.put(str(i))
                time.sleep(1)
        except Exception as err:
            print(f"Exception in {thread.name}: {err}  {err.__class__.__name__}")



    def raise_exception(self):
        thread = threading.current_thread()
        print(f"Raise exception in {thread.name}")
        thread_id = self.native_id
        res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(MyStopButton))
        if res > 1:
            ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
            print('Exception raise failure')
        print("DONE")

class RunnerApp(tk.Frame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.pack()

        # Capture event when someone closes the window with the X on the top-right corner of the window
        parent.protocol("WM_DELETE_WINDOW", self.close_app)

        self.QUIT = tk.Button(self)
        self.QUIT["text"] = "QUIT"
        self.QUIT["fg"] = "red"
        self.QUIT["command"] = self.close_app
        self.QUIT.pack({"side": "left"})

        self.start_button = tk.Button(self)
        self.start_button["text"] = "Start"
        self.start_button["command"] = self.start
        self.start_button.pack({"side": "left"})

        self.stop_button = tk.Button(self)
        self.stop_button["text"] = "Stop"
        self.stop_button["command"] = self.stop
        self.stop_button.pack({"side": "left"})

        self.text = tk.Text(self, state='disabled')
        self.text.pack({"side": "bottom"})

        self.stop_process = False

    def close_app(self):
        print("close")
        self.stop_process = True
        self.quit()

    def stop(self):
        print("stop")
        print(self.job.name)
        self.job.raise_exception()
        #self.stop_process = True
        self.add_line('stop')


    def start(self):
        self.stop_process = False
        self.start_button['state'] = 'disabled'
        self.que = queue.Queue()
        self.job = ThreadedJob(self.que)
        self.job.start()
        self.master.after(100, self.process_queue)

    def process_queue(self):
        print("process " + str(time.time()))
        if not self.job.is_alive():
            self.job.join()
            self.job = None
            self.stop_process = True
            self.start_button['state'] = 'normal'
            print("finished")
            return

        try:
            msg = self.que.get(0)
            self.add_line(msg)
        except queue.Empty:
            pass
        finally:
            if not self.stop_process:
                self.master.after(100, self.process_queue)

    def add_line(self, line):
        self.text['state'] = 'normal'  # allow editing of the Text widget
        self.text.insert('end', line + "\n")
        self.text['state'] = 'disabled'  # disable editing
        self.text.see('end')  # scroll to the end as we make progress
        self.update()  # update the content and allow other events (e.g. from stop and quit buttons) to take place


def main():
    tk_root = tk.Tk()
    app = RunnerApp(parent=tk_root)

    tk_root.lift()
    tk_root.call('wm', 'attributes', '.', '-topmost', True)
    tk_root.after_idle(tk_root.call, 'wm', 'attributes', '.', '-topmost', False)

    app.mainloop()


main()

Tk: Old Simple Tk app with class

  • tkinter
  • Tk
  • mainloop
  • Frame
import tkinter as tk

class Example(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent, background="white")
        self.parent = parent
        self.initUI()

    def initUI(self):
        self.parent.title("Simple")
        self.pack(fill=tk.BOTH, expand=1)

def main():
    app = tk.Tk()
    app.geometry("250x150+300+300")
    main_frame = Example(parent=app)

    # move the window to the front (needed on Mac only?)
    app.lift()
    app.call('wm', 'attributes', '.', '-topmost', True)
    app.after_idle(app.call, 'wm', 'attributes', '.', '-topmost', False)

    app.mainloop()

main()

Tk: Old Hello World

  • Label
import tkinter as tk

class Example(tk.Frame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.pack()
        self.createWidgets()

    def createWidgets(self):
        # Simple Label widget:
        self.name_title = tk.Label(self, text="Hello World!")
        self.name_title.pack({"side": "left"})

def main():
    root = tk.Tk()
    app = Example(parent=root)
    app.mainloop()

main()

Tk: Old Quit button

  • Button
import tkinter as tk

class Example(tk.Frame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.pack()
        self.createWidgets()

    def createWidgets(self):
        self.QUIT = tk.Button(self)
        self.QUIT["text"] = "QUIT"
        self.QUIT["fg"]   = "red"
        self.QUIT["command"] =  self.quit
        self.QUIT.pack({"side": "left"})

def main():
    root = tk.Tk()
    app = Example(parent=root)

    app.mainloop()

main()


Tk: Old File selector

  • Entry
  • filedialog
import tkinter as tk
from tkinter import filedialog

class Example(tk.Frame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.pack()
        self.createWidgets()

    def get_file(self):
        file_path = filedialog.askopenfilename()
        print(file_path)
        self.filename.delete(0, tk.END)
        self.filename.insert(0, file_path)

    def run_process(self):
        print("Running a process on file {}".format(self.filename.get()))

    def createWidgets(self):
        self.QUIT = tk.Button(self)
        self.QUIT["text"] = "QUIT"
        self.QUIT["fg"]   = "red"
        self.QUIT["command"] =  self.quit
        self.QUIT.pack({"side": "right"})

        # Simple Label widget:
        self.filename_title = tk.Label(self, text="Fileame:")
        self.filename_title.pack({"side": "left"})

        # Simple Entry widget:
        self.filename = tk.Entry(self, width=120)
        self.filename.pack({"side": "left"})
        self.filename.delete(0, tk.END)

        self.selector = tk.Button(self)
        self.selector["text"] = "Select",
        self.selector["command"] = self.get_file
        self.selector.pack({"side": "left"})

        self.process = tk.Button(self)
        self.process["text"] = "Process",
        self.process["command"] = self.run_process
        self.process.pack({"side": "left"})


def main():
    root = tk.Tk()
    app = Example(parent=root)

    root.lift()
    root.call('wm', 'attributes', '.', '-topmost', True)
    root.after_idle(root.call, 'wm', 'attributes', '.', '-topmost', False)

    app.mainloop()

main()


Tk: Old Checkbox

  • Checkbutton
import tkinter as tk

class Example(tk.Frame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.pack()
        self.createWidgets()

    def show_values(self):
        print("show values")
        for v in self.vars:
            print(v.get())

    def createWidgets(self):
        self.QUIT = tk.Button(self)
        self.QUIT["text"] = "QUIT"
        self.QUIT["fg"]   = "red"
        self.QUIT["command"] =  self.quit
        self.QUIT.pack({"side": "left"})


        self.vars = []
        self.cbs = []
        self.vars.append(tk.IntVar())
        cb = tk.Checkbutton(text="Blue", variable=self.vars[-1])
        cb.pack({"side": "left"})
        self.cbs.append(cb)

        self.vars.append(tk.IntVar())
        cb = tk.Checkbutton(text="Yellow", variable=self.vars[-1])
        cb.pack({"side": "left"})
        self.cbs.append(cb)

        self.show = tk.Button(self)
        self.show["text"] = "Show",
        self.show["command"] = self.show_values
        self.show.pack({"side": "left"})

def main():
    root = tk.Tk()
    app = Example(parent=root)

    root.lift()
    root.call('wm', 'attributes', '.', '-topmost', True)
    root.after_idle(root.call, 'wm', 'attributes', '.', '-topmost', False)

    app.mainloop()

main()

Tk: Old Getting started with Tk

  • Tk
import tkinter as tk

class Example(tk.Frame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.pack()
        self.createWidgets()

    def say_hi(self):
        print("hi there, everyone! ")
        print("Name: {}".format(self.name.get()))
        print("Password: {}".format(self.password.get()))
        print("count: {}".format(self.count.get()))
        self.password.delete(0, 'end')


    def createWidgets(self):
        self.QUIT = tk.Button(self)
        self.QUIT["text"] = "QUIT"
        self.QUIT["fg"]   = "red"
        self.QUIT["command"] =  self.quit
        self.QUIT.pack({"side": "left"})

        # Simple Label widget:
        self.name_title = tk.Label(self, text="Name:")
        self.name_title.pack({"side": "left"})

        # Simple Entry widget:
        self.name = tk.Entry(self)
        self.name.pack({"side": "left"})
        self.name.insert(0, "Your name")

        # Simple Label widget:
        self.password_title = tk.Label(self, text="Password:")
        self.password_title.pack({"side": "left"})

        self.count = tk.IntVar()
        self.count.set(2)
        self.my_radio = []
        radio = [(1, "One"), (2, "Two"), (3, "Three")]
        for ix in range(len(radio)):
            self.my_radio.append(tk.Radiobutton(self, text=radio[ix][1], variable=self.count, value=radio[ix][0]))
            self.my_radio[ix].pack({"side": "bottom"})

        # In order to hide the text as it is typed (e.g. for Passwords)
        # set the "show" parameter:
        self.password = tk.Entry(self)
        self.password["show"] = "*"
        self.password.pack({"side": "left"})

        self.hi_there = tk.Button(self)
        self.hi_there["text"] = "Hello",
        self.hi_there["command"] = self.say_hi

        self.hi_there.pack({"side": "left"})

def main():
    root = tk.Tk()
    app = Example(parent=root)

    root.lift()
    root.call('wm', 'attributes', '.', '-topmost', True)
    root.after_idle(root.call, 'wm', 'attributes', '.', '-topmost', False)

    app.mainloop()

main()

Exercise: Tk - Calculator one line

Write a Tk application that behaves like a one-line calculator. It has an entry box where one can enter an expression like "2 + 3" and a button. When the button is pressed the expression is calculated.

There is another button called "Quit" that will close the application.

Exercise: Tk - Calculator with buttons

  • This is a Calculator app that already has buttons for the digits 0-9
  • Buttons for the operators +-*/
  • A button for =
  • A window where we can see what we type in using the buttons or using the keyboard.

Exercise: Tk - Convert between CSV and Excel files

  • Write a Tk-based application that can convert CSV to Excel and Excel to CSV.

  • Select an existing .csv file or .xlsx file

  • Select a filename for output

  • Click a button to convert

  • Have a place for messages or use pop-up message windows.

Exercise: Tk - Shopping list

Create a Tk application that allows you to create a shopping list.

Exercise: Tk - TODO list

  • Create a Tk application to handle your TODO items.
  • A Menu to be able to exit the application
  • A List of current tasks.
  • A way to add a new task. For a start each task has a title and a status. The status can be "todo" or "done". (default is "todo")
  • A way to edit a task. (Primarily to change its title).
  • A way to mark an item as "done" or mark it as "todo".
  • A way to move items up and down in the list.
  • The application should automatically save the items in their most up-to-date state in a "database". The database can be a JSON file or and SQLite database or anything else you feel fit.

Exercise: Tk - Notepad

  • Create a Notepad like text editor.

  • It needs to have a menu called File with item: New/Open/Save/Save As/Exit

  • It needs to have an area where it can show the content of a file. Let you edit it.

  • Create a menu called About that displays an about box containing the names of the authors of the app.

  • Menu item to Search for text.

Exercise: Tk - Copy files

An application that allows you to type in, or select an existing file and another filename for which the file does not exists. Then copy the old file to the new name.

Exercise: Tk - Implement Master Mind board

Create an application that we can use to play Master Mind

Exercise: Tk - a GUI for a grep-like application

The GUI should accept:

  • A filename, a wilde-card expression, maybe a dirctory name (and then a flag to recurse or not).

  • A regular expression.

  • Various flags for regex.

  • Then it should display the lines that match the expression in the selected files.

Solution: Tk - Calculator one line

  • Entry
  • delete
  • insert
import tkinter as tk
from tkinter import messagebox

app = tk.Tk()
app.title('Calculator')

entry = tk.Entry(app)
entry.pack()

def calc():
    #print("clicked")
    inp = entry.get()
    print(f"'{inp}'")
    try:
        out = eval(inp)
    except Exception as err:
        messagebox.showwarning(title = "Error", message = f"Could not do the computation {err}")
        return
    entry.delete(0, tk.END)
    entry.insert(0, out)

def close():
    app.destroy()

calc_btn = tk.Button(app, text='Calculate', width=25, command=calc)
calc_btn.pack()


close_btn = tk.Button(app, text='Close', width=25, command=close)
close_btn.pack()

app.mainloop()

Solution: Tk - Calculator with buttons

  • Button
import tkinter as tk

app = tk.Tk()
app.title('Calculator')

label = tk.Label(app,
    width=50,
    #height=2,
    font=['Curier', 20],
    bg='white',
)
label.pack()

def backspace():
    if len(label['text']) > 0:
        label['text'] = label['text'][0:-1]

def clear():
    label['text'] = ''

def calc():
    inp = label['text']
    print(inp)
    out = eval(inp)
    label['text'] = out

def close():
    app.destroy()
    exit()

def enter(value):
    label['text'] += value

def add_button(num, frame):
    btn = tk.Button(frame, text=num, width=25, command=lambda : enter(num))
    btn.pack(side="left")
    buttons[num] = btn

numbers_frame = tk.Frame(app)
numbers_frame.pack()
numbers_row = {}
numbers_row[1] = tk.Frame(numbers_frame)
numbers_row[1].pack(side="top")
numbers_row[2] = tk.Frame(numbers_frame)
numbers_row[2].pack(side="top")
numbers_row[3] = tk.Frame(numbers_frame)
numbers_row[3].pack(side="top")
ops_row = tk.Frame(numbers_frame)
ops_row.pack(side="top")

buttons = {}

add_button('1', numbers_row[1])
add_button('2', numbers_row[1])
add_button('3', numbers_row[1])
add_button('4', numbers_row[2])
add_button('5', numbers_row[2])
add_button('6', numbers_row[2])
add_button('7', numbers_row[3])
add_button('8', numbers_row[3])
add_button('9', numbers_row[3])


for op in ['+', '-', '*', '/']:
    add_button(op, ops_row)


calc_btn = tk.Button(app, text='=', command=calc)
calc_btn.pack()

clear_btn = tk.Button(app, text='C', command=clear)
clear_btn.pack()

backspace_btn = tk.Button(app, text='Bksp', command=backspace)
backspace_btn.pack()


close_btn = tk.Button(app, text='Close', width=25, fg='red', command=close)
close_btn.pack()

app.mainloop()

Solution: Tk - Convert between CSV and Excel files

import tkinter as tk
from tkinter import filedialog

def run_process():
    print("---- Start processing ----")
    title = title_entry.get()
    print(title)
    filename = input_file.get()
    print(filename)

    app.destroy()

def select_input_file():
    file_path = filedialog.askopenfilename()
    filedialog.asksaveasfile()
    print(file_path)
    input_file.set(file_path)

app = tk.Tk()
app.title('Convert file')

input_file = tk.StringVar()

title_label = tk.Label(app, text='Title')
title_label.pack()
title_entry = tk.Entry(app)
title_entry.pack()

input_button = tk.Button(app, text='Input file', command=select_input_file)
input_button.pack()
input_label = tk.Label(app, textvariable=input_file)
input_label.pack()


button = tk.Button(app, text='Process', width=25, command=run_process)
button.pack()

app.mainloop()


Solution: Tk - Implement Master Mind board

TBD

Solution: Tk - Notepad

import tkinter as tk
from tkinter import filedialog, simpledialog, messagebox
import os

file_path = None

app = tk.Tk()
app.title('Tk Notepad')

def run_new():
    global file_path
    file_path = None
    text.delete(1.0, tk.END)

def run_open():
    global file_path
    file_path = filedialog.askopenfilename(filetypes=(("Any file", "*"),))
    if file_path and os.path.isfile(file_path):
        with open(file_path) as fh:
            content = fh.read()
        text.delete(1.0, tk.END)
        text.insert('end', content)

def run_save():
    global file_path
    if file_path is None:
        file_path = filedialog.asksaveasfilename(filetypes=(("Any file", "*"),))
        if not file_path:
            file_path = None
            return
    #print(f"'{file_path}'")
    content = text.get(1.0, tk.END)
    with open(file_path, 'w') as fh:
        fh.write(content)

def run_exit():
    print("exit")
    app.destroy()

def run_about():
    #print(dir(simpledialog))
    #answer = simpledialog.Dialog(app, "The title")
    messagebox.showinfo(title = "About", message = "This simple text editor was created as a solution for the exercise.\n\nCopyright: Gabor Szabo")

menubar = tk.Menu(app)

menu1 = tk.Menu(menubar, tearoff=0)
menu1.add_command(label="New",  underline=0, command=run_new)
menu1.add_command(label="Open", underline=0, command=run_open)
menu1.add_command(label="Save", underline=0, command=run_save)
menu1.add_separator()
menu1.add_command(label="Exit", underline=1, command=run_exit)
menubar.add_cascade(label="File", underline=0, menu=menu1)

menubar.add_command(label="About", underline=0, command=run_about)

app.config(menu=menubar)

text = tk.Text(app)
text.pack({"side": "bottom"})

app.mainloop()

# TODO: Show the name of the file somewhere? Maybe at the bottom in a status bar?
# TODO: Indicate if the file has been changed since the last save?
# TODO: Ask before exiting or before replacing the content if the file has not been saved yet.
# TODO: Undo/Redo?
# TODO: Search?
# TODO: Search and Replace?