├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md ├── section03 ├── lectures │ ├── 01_how_to_get_tkinter │ │ └── README.md │ ├── 02_hello_world │ │ └── app.py │ ├── 03_tkinter_buttons │ │ └── app.py │ ├── 04_greetings │ │ └── app.py │ ├── 05_packing_components │ │ ├── code.py │ │ └── slides.py │ ├── 06_packing_with_frames │ │ └── code.py │ ├── 07_greetings_with_pack │ │ ├── README.md │ │ └── app.py │ ├── 08_grid │ │ └── app.py │ └── 09_high_dpi_windows_10 │ │ ├── README.md │ │ └── app.py └── projects │ ├── greeter │ └── app.py │ ├── greeter_2 │ └── app.py │ ├── hello_world │ └── app.py │ └── hello_world_2 │ └── app.py ├── section04 └── lectures │ ├── 01_labels │ ├── code.py │ └── test_image.png │ ├── 02_text_widgets │ └── code.py │ ├── 03_scrollbars │ └── code.py │ ├── 04_separators │ └── code.py │ ├── 05_check_buttons │ └── code.py │ ├── 06_radio_buttons │ └── code.py │ ├── 07_combo_boxes │ └── code.py │ ├── 08_list_boxes │ └── code.py │ ├── 09_spin_boxes │ └── code.py │ └── 10_scales │ └── code.py ├── section05 ├── distance_converter │ ├── app_grid.py │ └── app_pack.py └── lectures │ ├── 01_set_up │ └── app.py │ ├── 02_calculating_feet │ └── app.py │ ├── 03_changing_feet_label │ └── app.py │ ├── 04_adding_shortcuts │ └── app.py │ ├── 05_changing_font │ └── code.py │ └── 06_winfo_children │ └── code.py ├── section06 ├── 01_object_oriented_window │ └── app.py ├── 02_object_oriented_frame │ └── app.py ├── 03_object_oriented_app │ └── app.py ├── 04_distance_converter_oop │ └── app.py ├── 05_adding_inner_container │ └── app.py ├── 06_feet_to_metres │ └── app.py ├── 07_switching_between_frames │ └── app.py ├── 08_adding_keybindings │ └── app.py └── app.py ├── section07 ├── 01_ttk_style_themes │ ├── README.md │ ├── code.py │ └── windows.py ├── 02_finding_widget_style_class │ └── code.py ├── 03_changing_styles │ └── code.py ├── 04_what_properties_can_we_change │ └── code.py ├── 05_creating_new_styles │ └── code.py ├── 06_state_specific_options │ └── code.py ├── 07_changing_entry_field_font_with_styles │ └── README.md ├── 08_named_fonts │ ├── README.md │ └── code.py ├── app.py └── windows.py ├── section08 ├── lectures │ ├── 02_countdown_timer │ │ └── timer.py │ ├── 03_timer_breaks │ │ └── timer.py │ ├── 04_showing_current_timer │ │ └── timer.py │ ├── 05_starting_and_stopping │ │ └── timer.py │ ├── 06_reset_timer │ │ └── timer.py │ ├── 07_linking_with_controller │ │ └── timer.py │ ├── 08_splitting_into_files │ │ ├── app.py │ │ └── frames │ │ │ ├── __init__.py │ │ │ └── timer.py │ ├── 09_the_settings_frame │ │ ├── app.py │ │ └── frames │ │ │ ├── __init__.py │ │ │ ├── settings.py │ │ │ └── timer.py │ ├── 10_switching_between_frames │ │ ├── app.py │ │ └── frames │ │ │ ├── __init__.py │ │ │ ├── settings.py │ │ │ └── timer.py │ └── 11_styling_our_app │ │ ├── app.py │ │ └── frames │ │ ├── __init__.py │ │ ├── settings.py │ │ └── timer.py └── pomodoro_timer │ ├── app.py │ ├── frames │ ├── __init__.py │ ├── settings.py │ └── timer.py │ └── windows.py ├── section09 └── lectures │ ├── (unused) PTK_09_08_responsive_breakpoints │ ├── Pipfile │ ├── app.py │ └── frames │ │ ├── chat.py │ │ └── message_window.py │ ├── PTK_09_01_app_overview │ ├── Pipfile │ ├── app.py │ └── frames │ │ ├── chat.py │ │ └── message_window.py │ ├── PTK_09_02_getting_messages_from_api │ ├── Pipfile │ ├── app.py │ └── frames │ │ └── chat.py │ ├── PTK_09_03_creating_labels_for_new_messages │ ├── Pipfile │ ├── app.py │ └── frames │ │ └── chat.py │ ├── PTK_09_04_showing_the_message_date │ ├── Pipfile │ ├── app.py │ └── frames │ │ └── chat.py │ ├── PTK_09_05_adding_sample_user_avatar │ ├── Pipfile │ ├── app.py │ └── frames │ │ └── chat.py │ ├── PTK_09_06_how_to_scroll_frame_tkinter │ ├── Pipfile │ ├── app.py │ └── frames │ │ ├── chat.py │ │ └── message_window.py │ ├── PTK_09_08_handle_resizing_and_wrapping_labels │ ├── Pipfile │ ├── app.py │ └── frames │ │ ├── chat.py │ │ └── message_window.py │ ├── PTK_09_09_sending_messages │ ├── Pipfile │ ├── app.py │ └── frames │ │ ├── chat.py │ │ └── message_window.py │ └── PTK_09_10_styling_our_app │ ├── Pipfile │ ├── app.py │ └── frames │ ├── chat.py │ └── message_window.py ├── section10 ├── Pipfile ├── Pipfile.lock ├── app.py └── assets │ ├── food.png │ └── snake.png ├── section11 ├── README.md ├── app.py ├── assets │ ├── food.png │ └── snake.png ├── lectures │ ├── PTK_11_01_installation_and_documentation │ │ └── README.md │ ├── PTK_11_02_two_ways_to_bundle │ │ └── README.md │ ├── PTK_11_06_hiding_console_window │ │ └── README.md │ ├── PTK_11_07_when_things_go_wrong │ │ └── README.md │ └── PTK_11_08_building_for_multiple_platforms │ │ ├── README.md │ │ └── README_further_learning.md └── windows.py └── test_projects ├── calculator ├── calc.gif ├── calculator_01.py ├── calculator_02.py ├── calculator_03.py └── calculator_04.py ├── currency_converter ├── Pipfile ├── Pipfile.lock ├── currency_converter_01.py ├── currency_converter_02.py └── dummyData.txt └── distance_converter ├── distance_converter_01.py └── distance_converter_02.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.pyc 3 | videos 4 | *.mp4 5 | *.mkv 6 | *.sketch 7 | exported-assets 8 | build 9 | dist 10 | *.spec 11 | __pycache__ 12 | *.tscproj 13 | *.indd 14 | *.pdf 15 | endscreens 16 | .idea 17 | *.cmproj 18 | questions 19 | *.zip 20 | promo/ 21 | *.pptx -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | black = "*" 8 | 9 | [packages] 10 | pillow = "*" 11 | playsound = "*" 12 | pyinstaller = "*" 13 | 14 | [requires] 15 | python_version = "3.7" 16 | 17 | [pipenv] 18 | allow_prereleases = "true" 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/gui-development-tkinter-course/90a3979615997223f308832ce842e002cd571be4/README.md -------------------------------------------------------------------------------- /section03/lectures/01_how_to_get_tkinter/README.md: -------------------------------------------------------------------------------- 1 | # Getting Tkinter 2 | 3 | Tkinter should be installed already. Try to run this in IDLE: 4 | 5 | ``` 6 | import tkinter 7 | tkinter._test() 8 | ``` 9 | 10 | If that fails, read on... 11 | 12 | ## Installing Tkinter on Windows 13 | 14 | Tkinter comes with Python on Windows, if you selected the appropriate option in the official Windows installer. 15 | 16 | If you have IDLE on Windows, you have Tkinter. 17 | 18 | If not, run the installer again making sure that Tcl/Tk is enabled in the installation. 19 | 20 | Instructions: https://stackoverflow.com/a/50027385/1587271 21 | 22 | ## Installing Tkinter on Mac 23 | 24 | I recommend installing Python via the official installer, which comes with Tkinter. 25 | 26 | ## Installing Tkinter on Ubuntu 27 | 28 | Enable your virtual environment, and then run: 29 | 30 | ``` 31 | sudo apt-get install python3-tk 32 | ``` -------------------------------------------------------------------------------- /section03/lectures/02_hello_world/app.py: -------------------------------------------------------------------------------- 1 | # In Python 3, the correct import is tkinter not Tkinter. Tkinter was the name of the module used for Python 2 2 | import tkinter as tk 3 | 4 | # ttk is the Python binding to the newer "themed widgets" added in Tk version 8.5 5 | from tkinter import ttk 6 | 7 | root = tk.Tk() 8 | ttk.Label(root, text="Hello, World!", padding=(30, 10)).pack() 9 | 10 | root.mainloop() 11 | -------------------------------------------------------------------------------- /section03/lectures/03_tkinter_buttons/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | def greet(): 6 | print("Hello, World!") 7 | 8 | 9 | root = tk.Tk() 10 | root.title("Hello") 11 | 12 | greet_button = ttk.Button(root, text="Greet", command=greet) 13 | greet_button.pack(side="left", fill="x", expand=True) 14 | 15 | quit_button = ttk.Button(root, text="Quit", command=root.destroy) 16 | quit_button.pack(side="left", fill="x", expand=True) # could use side="right" 17 | 18 | root.mainloop() 19 | -------------------------------------------------------------------------------- /section03/lectures/04_greetings/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | def greet(): 6 | # The get() method is used to fetch the value of a StringVar() instance. 7 | # If user_name is empty, print Hello, World! 8 | print(f"Hello, {user_name.get() or 'World'}!") 9 | 10 | 11 | root = tk.Tk() 12 | root.title("Greeter") 13 | 14 | # Here we create an instances of the StringVar() class, which is to track the content of widgets 15 | user_name = tk.StringVar() 16 | 17 | 18 | name_label = ttk.Label(root, text="Name: ") 19 | name_label.pack(side="left", padx=(0, 10)) 20 | name_entry = ttk.Entry(root, width=15, textvariable=user_name) 21 | name_entry.pack(side="left") 22 | name_entry.focus() 23 | 24 | greet_button = ttk.Button(root, text="Greet", command=greet) 25 | greet_button.pack(side="left", fill="x", expand=True) 26 | quit_button = ttk.Button(root, text="Quit", command=root.destroy) 27 | quit_button.pack(side="right", fill="x", expand=True) 28 | 29 | root.mainloop() 30 | -------------------------------------------------------------------------------- /section03/lectures/05_packing_components/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | # -- Aligning with `side` --- 4 | 5 | root = tk.Tk() 6 | 7 | tk.Label(root, text="Label 1", bg="green").pack(side="left", fill="y", expand=True) 8 | tk.Label(root, text="Label 2", bg="red").pack(side="left") 9 | 10 | root.mainloop() 11 | 12 | # -- Filling in one direction -- 13 | 14 | root = tk.Tk() 15 | 16 | tk.Label(root, text="Label 1", bg="green").pack(side="left", fill="y") 17 | tk.Label(root, text="Label 2", bg="red").pack(side="top", fill="x") 18 | 19 | root.mainloop() 20 | 21 | # -- Filling in both directions -- 22 | 23 | root = tk.Tk() 24 | 25 | tk.Label(root, text="Label 1", bg="green").pack(side="left", fill="both") 26 | tk.Label(root, text="Label 2", bg="red").pack(side="top", fill="both") 27 | 28 | root.mainloop() 29 | 30 | 31 | # -- Even if either label doesn't fill -- 32 | 33 | root = tk.Tk() 34 | 35 | tk.Label(root, text="Label 1", bg="green").pack(side="left") 36 | tk.Label(root, text="Label 2", bg="red").pack(side="top", fill="both") 37 | 38 | root.mainloop() 39 | 40 | 41 | root = tk.Tk() 42 | 43 | tk.Label(root, text="Label 1", bg="green").pack(side="left", fill="both") 44 | tk.Label(root, text="Label 2", bg="red").pack(side="top") 45 | 46 | root.mainloop() 47 | 48 | # -- expand can make it grow as much as possible. It won't hide other widgets, but other widgets will be compressed -- 49 | 50 | root = tk.Tk() 51 | 52 | tk.Label(root, text="Label 1", bg="green").pack(side="left", expand=True, fill="both") 53 | tk.Label(root, text="Label 2", bg="red").pack(side="top") 54 | 55 | root.mainloop() 56 | 57 | 58 | # -- expanding two widgets means they share the available space evenly -- 59 | 60 | root = tk.Tk() 61 | 62 | tk.Label(root, text="Label 2", bg="red").pack(side="top", expand=True, fill="both") 63 | tk.Label(root, text="Label 2", bg="red").pack(side="top", expand=True, fill="both") 64 | 65 | root.mainloop() 66 | 67 | 68 | # -- whichever side comes first gets expansion priority -- 69 | 70 | root = tk.Tk() 71 | 72 | tk.Label(root, text="Label left", bg="green").pack( 73 | side="left", expand=True, fill="both" 74 | ) 75 | tk.Label(root, text="Label top", bg="red").pack(side="top", expand=True, fill="both") 76 | tk.Label(root, text="Label top", bg="red").pack(side="top", expand=True, fill="both") 77 | 78 | root.mainloop() 79 | 80 | root = tk.Tk() 81 | 82 | tk.Label(root, text="Label top", bg="red").pack(side="top", expand=True, fill="both") 83 | tk.Label(root, text="Label top", bg="red").pack(side="top", expand=True, fill="both") 84 | tk.Label(root, text="Label left", bg="green").pack( 85 | side="left", expand=True, fill="both" 86 | ) 87 | 88 | root.mainloop() 89 | 90 | # As you can see, even though we specificied `side="left"`, the last label was still underneath. 91 | # We can't just use packing on the root to take care of all our layout needs. 92 | # Hence, Tkinter has `Frame`, which is a container for other widgets. 93 | -------------------------------------------------------------------------------- /section03/lectures/05_packing_components/slides.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | root = tk.Tk() 4 | root.geometry("600x400") 5 | 6 | rectangle_1 = tk.Label(root, text="Rectangle 1", bg="green", fg="white") 7 | rectangle_1.pack(side="left", ipadx=10, ipady=10, fill="both", expand=True) 8 | 9 | rectangle_2 = tk.Label(root, text="Rectangle 2", bg="red", fg="white") 10 | rectangle_2.pack(side="top", ipadx=10, ipady=10, fill="both", expand=True) 11 | 12 | rectangle_3 = tk.Label(root, text="Rectangle 3", bg="black", fg="white") 13 | rectangle_3.pack(side="left", ipadx=10, ipady=10, fill="both", expand=True) 14 | 15 | rectangle_4 = tk.Label(root, text="Rectangle 4", bg="black", fg="white") 16 | rectangle_4.pack(side="top", ipadx=10, ipady=10, fill="both", expand=True) 17 | 18 | root.mainloop() -------------------------------------------------------------------------------- /section03/lectures/06_packing_with_frames/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | root = tk.Tk() 5 | 6 | main = ttk.Frame(root) 7 | main.pack(side="left", fill="both", expand=True) 8 | 9 | tk.Label(main, text="Label top", bg="red").pack(side="top", expand=True, fill="both") 10 | tk.Label(main, text="Label top", bg="red").pack(side="top", expand=True, fill="both") 11 | tk.Label(root, text="Label left", bg="green").pack( 12 | side="left", expand=True, fill="both" 13 | ) 14 | 15 | root.mainloop() 16 | -------------------------------------------------------------------------------- /section03/lectures/07_greetings_with_pack/README.md: -------------------------------------------------------------------------------- 1 | Here we'd go back to the code from `04_greetings` but add frames. -------------------------------------------------------------------------------- /section03/lectures/07_greetings_with_pack/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | def greet(): 6 | # The get() method is used to fetch the value of a StringVar() instance. 7 | # If user_name is empty, print Hello, World! 8 | print(f"Hello, {user_name.get() or 'World'}!") 9 | 10 | 11 | root = tk.Tk() 12 | root.title("Greeter") 13 | 14 | # Here we create an instances of the StringVar() class, which is to track the content of widgets 15 | user_name = tk.StringVar() 16 | 17 | # We define two frames to keep the input on different lines. In the next version we will switch to grid() geometry. 18 | # Padding accepts a tuple of up to four values. Clockwise like CSS. 19 | input_frame = ttk.Frame(root, padding=(20, 10, 20, 0)) 20 | input_frame.pack(fill="both") 21 | 22 | name_label = ttk.Label(input_frame, text="Name: ") 23 | name_label.pack(side="left", padx=(0, 10)) 24 | name_entry = ttk.Entry(input_frame, width=15, textvariable=user_name) 25 | name_entry.pack(side="left") 26 | name_entry.focus() 27 | 28 | buttons = ttk.Frame(root, padding=(20, 10)) 29 | buttons.pack(fill="both") 30 | 31 | greet_button = ttk.Button(buttons, text="Greet", command=greet) 32 | greet_button.pack(side="left", fill="x", expand=True) 33 | quit_button = ttk.Button(buttons, text="Quit", command=root.destroy) 34 | quit_button.pack(side="right", fill="x", expand=True) 35 | 36 | root.mainloop() 37 | -------------------------------------------------------------------------------- /section03/lectures/08_grid/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | root = tk.Tk() 5 | root.title("Greeter") 6 | 7 | user_name = tk.StringVar() 8 | 9 | main = ttk.Frame(root, padding=(20, 10, 20, 0)) 10 | main.grid() 11 | 12 | root.columnconfigure(0, weight=1) 13 | root.rowconfigure(1, weight=1) 14 | 15 | name_label = ttk.Label(main, text="Name:") 16 | name_label.grid(row=0, column=0, padx=(0, 10)) 17 | name_entry = ttk.Entry(main, width=15, textvariable=user_name) 18 | name_entry.grid(row=0, column=1) 19 | name_entry.focus() 20 | 21 | buttons = ttk.Frame(root, padding=(20, 10)) 22 | buttons.grid(row=1, column=0, sticky="EW") 23 | 24 | buttons.columnconfigure(0, weight=1) 25 | buttons.columnconfigure(1, weight=1) 26 | 27 | greet_button = ttk.Button(buttons, text="Greet") 28 | greet_button.grid(row=0, column=0, sticky="EW") 29 | quit_button = ttk.Button(buttons, text="Quit", command=root.destroy) 30 | quit_button.grid(row=0, column=1, sticky="EW") 31 | 32 | root.mainloop() 33 | 34 | -------------------------------------------------------------------------------- /section03/lectures/09_high_dpi_windows_10/README.md: -------------------------------------------------------------------------------- 1 | # High DPI Settings 2 | 3 | To enable High DPI compatibility in Windows 10, you normally just have to add this: 4 | 5 | ```python 6 | try: 7 | from ctypes import windll 8 | windll.shcore.SetProcessDpiAwareness(1) 9 | except: 10 | pass 11 | ``` 12 | 13 | However if you still run into scaling problems, check out [this StackOverflow topic](https://stackoverflow.com/questions/41315873/attempting-to-resolve-blurred-tkinter-text-scaling-on-windows-10-high-dpi-disp). -------------------------------------------------------------------------------- /section03/lectures/09_high_dpi_windows_10/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | try: 5 | from ctypes import windll 6 | windll.shcore.SetProcessDpiAwareness(1) 7 | except: 8 | pass 9 | 10 | root = tk.Tk() 11 | root.title("Greeter") 12 | 13 | user_name = tk.StringVar() 14 | 15 | main = ttk.Frame(root, padding=(20, 10, 20, 0)) 16 | main.grid() 17 | 18 | root.columnconfigure(0, weight=1) 19 | root.rowconfigure(1, weight=1) 20 | 21 | name_label = ttk.Label(main, text="Name:") 22 | name_label.grid(row=0, column=0, padx=(0, 10)) 23 | name_entry = ttk.Entry(main, width=15, textvariable=user_name) 24 | name_entry.grid(row=0, column=1) 25 | name_entry.focus() 26 | 27 | buttons = ttk.Frame(root, padding=(20, 10)) 28 | buttons.grid(row=1, column=0, sticky="EW") 29 | 30 | buttons.columnconfigure(0, weight=1) 31 | buttons.columnconfigure(1, weight=1) 32 | 33 | greet_button = ttk.Button(buttons, text="Greet") 34 | greet_button.grid(row=0, column=0, sticky="EW") 35 | quit_button = ttk.Button(buttons, text="Quit", command=root.destroy) 36 | quit_button.grid(row=0, column=1, sticky="EW") 37 | 38 | root.mainloop() 39 | 40 | -------------------------------------------------------------------------------- /section03/projects/greeter/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | def greet(): 6 | # The get() method is used to fetch the value of a StringVar() instance. 7 | # If user_name is empty, print Hello, World! 8 | print(f"Hello, {user_name.get() or 'World'}!") 9 | 10 | 11 | root = tk.Tk() 12 | root.title("Greeter") 13 | 14 | # Here we create an instances of the StringVar() class, which is to track the content of widgets 15 | user_name = tk.StringVar() 16 | 17 | # We define two frames to keep the input on different lines. In the next version we will switch to grid() geometry. 18 | # Padding accepts a tuple of up to four values. Clockwise like CSS. 19 | input_frame = ttk.Frame(root, padding=(20, 10, 20, 0)) 20 | input_frame.pack(fill="both") 21 | 22 | name_label = ttk.Label(input_frame, text="Name: ") 23 | name_label.pack(side="left", padx=(0, 10)) 24 | name_entry = ttk.Entry(input_frame, width=15, textvariable=user_name) 25 | name_entry.pack(side="left") 26 | name_entry.focus() 27 | 28 | buttons = ttk.Frame(root, padding=(20, 10)) 29 | buttons.pack(fill="both") 30 | 31 | greet_button = ttk.Button(buttons, text="Greet", command=greet) 32 | greet_button.pack(side="left", fill="x", expand=True) 33 | quit_button = ttk.Button(buttons, text="Quit", command=root.destroy) 34 | quit_button.pack(side="right", fill="x", expand=True) 35 | 36 | root.mainloop() 37 | -------------------------------------------------------------------------------- /section03/projects/greeter_2/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | def greet(*args): 6 | name = user_name.get() 7 | greeting_message.set(f"Hello, {name or 'World'}!") 8 | greeting.grid(row=1, column=0, columnspan=2, sticky="W", pady=(10, 0)) 9 | 10 | 11 | root = tk.Tk() 12 | root.title("Greeter") 13 | 14 | user_name = tk.StringVar() 15 | greeting_message = tk.StringVar() 16 | 17 | main = ttk.Frame(root, padding=(20, 10, 20, 0)) 18 | main.grid() 19 | 20 | root.columnconfigure(0, weight=1) 21 | root.rowconfigure(1, weight=1) 22 | 23 | name_label = ttk.Label(main, text="Name:") 24 | name_label.grid(row=0, column=0, padx=(0, 10)) 25 | name_entry = ttk.Entry(main, width=15, textvariable=user_name) 26 | name_entry.grid(row=0, column=1) 27 | name_entry.focus() 28 | 29 | greeting = ttk.Label(main, textvariable=greeting_message) 30 | 31 | buttons = ttk.Frame(main, padding=(0, 10)) 32 | buttons.grid(row=2, column=0, columnspan=2, sticky="EW") 33 | 34 | # buttons.columnconfigure(0, weight=1) 35 | # buttons.columnconfigure(1, weight=1) 36 | buttons.columnconfigure((0, 1), weight=1) 37 | 38 | greet_button = ttk.Button(buttons, text="Greet", command=greet) 39 | greet_button.grid(row=0, column=0, sticky="EW") 40 | quit_button = ttk.Button(buttons, text="Quit", command=root.destroy) 41 | quit_button.grid(row=0, column=1, sticky="EW") 42 | 43 | root.bind("", greet) 44 | root.bind("KP_Enter", greet) 45 | 46 | root.mainloop() 47 | 48 | -------------------------------------------------------------------------------- /section03/projects/hello_world/app.py: -------------------------------------------------------------------------------- 1 | # Implementation in Tcl/Tk 2 | # 3 | # package require Tk 4 | # grid [ttk::button .b -text "Hello, World!"] 5 | 6 | # In Python 3, the correct import is tkinter not Tkinter. Tkinter was the name of the module used for Python 2 7 | import tkinter as tk 8 | 9 | # ttk is the Python binding to the newer "themed widgets" added in Tk version 8.5 10 | from tkinter import ttk 11 | 12 | root = tk.Tk() 13 | ttk.Label(root, text="Hello, World!", padding=(30, 10)).pack() 14 | 15 | root.mainloop() 16 | -------------------------------------------------------------------------------- /section03/projects/hello_world_2/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | def greet(): 6 | print("Hello, World!") 7 | 8 | 9 | root = tk.Tk() 10 | # Window title is defined on root using the .title() method 11 | root.title("Hello") 12 | 13 | """ 14 | Below we define a Frame using the .Frame() method. 15 | Frames are general purpose rectangular grouping widgets for collecting widgets into a single group. 16 | Frame takes an optional master positional argument, which defaults to None, and then a number of configuration options. 17 | 18 | Frame(master=None, **options) 19 | 20 | In our example, we specify root (the main application window) as the master of the Frame main. 21 | As an optional config argument, we specify a padding value of 20px on all sides. 22 | """ 23 | # Padding is defined in pixels by default. Multiple values possible to specify padding in different directions. 24 | main = ttk.Frame(root, padding=20) 25 | main.pack(fill="both") 26 | 27 | """ 28 | Below we define two buttons, both of which are children of main. 29 | 30 | The text keyword argument provided to the Button() method specifies the text content of the button. 31 | The command keyword argument is the name of a function to call when the button is clicked. 32 | 33 | When a user clicks the Greet button, we call the greet() function, printing "Hello, World!" to the terminal. 34 | When a user clicks the Quit button, we call the root.destroy() method. 35 | 36 | root.destroy() terminates the mainloop, and since all other widgets are children of root, they get destroyed along with it. 37 | """ 38 | greet_button = ttk.Button(main, text="Greet", command=greet) 39 | greet_button.pack(side="left", fill="x", expand=True) 40 | 41 | quit_button = ttk.Button(main, text="Quit", command=root.destroy) 42 | quit_button.pack(side="right", fill="x", expand=True) 43 | 44 | """ 45 | Missing off the value for the expand parameter causes the buttons to stay the same size, centered in a pair of imaginary boxes 46 | which is filling up the space as the page increases in size. 47 | """ 48 | # ttk.Button(main, text="Greet", command=greet).pack(side=LEFT, fill=X) 49 | # ttk.Button(main, text="Quit", command=root.destroy).pack(side=RIGHT, fill=X) 50 | 51 | """ 52 | Missing off the value for the fill parameter means that the buttons will stay at the size defined by their text content, but 53 | will remain stuck to the edges we defined using side. 54 | """ 55 | # ttk.Button(main, text="Greet", command=greet).pack(side=LEFT) 56 | # ttk.Button(main, text="Quit", command=root.destroy).pack(side=RIGHT) 57 | 58 | """ 59 | By default, the buttons will stack underneath each other. 60 | """ 61 | # ttk.Button(main, text="Greet", command=greet).pack() 62 | # ttk.Button(main, text="Quit", command=root.destroy).pack() 63 | 64 | root.mainloop() 65 | -------------------------------------------------------------------------------- /section04/lectures/01_labels/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | # -- Windows only configuration -- 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | # -- End Windows only configuration -- 11 | 12 | root = tk.Tk() 13 | root.resizable(False, False) 14 | root.title("Widget Examples") 15 | 16 | # -- Label as text, as we've seen -- 17 | 18 | label = ttk.Label(root, text="Hello, World!", padding=20) 19 | label.config(font=("Segoe UI", 20)) # Could be in the constructor instead. 20 | label.pack() 21 | 22 | 23 | # -- Labels with images -- 24 | 25 | from PIL import Image, ImageTk # Need to pip install Pillow 26 | 27 | # Need an image in the folder you run the file from, called "test_image.png" 28 | image = Image.open("test_image.png") #.resize((64, 64)) 29 | photo = ImageTk.PhotoImage(image) 30 | ttk.Label(root, image=photo, padding=5).pack() 31 | 32 | # This is how you change an image associated with a label, if necessary: 33 | # label["image"] = photo 34 | 35 | 36 | # -- Changing the text of a label dynamically -- 37 | 38 | greeting = tk.StringVar() 39 | 40 | label = ttk.Label(root, padding=10) 41 | label["textvariable"] = greeting 42 | greeting.set("Hello, John!") # This can change during your program and the label will update. 43 | label.pack() 44 | 45 | 46 | # -- Combining text and images -- 47 | 48 | text_image = Image.open("test_image.png") 49 | text_photo = ImageTk.PhotoImage(text_image) 50 | ttk.Label(root, text="Image with text.", image=text_photo, padding=5, compound="right").pack() 51 | 52 | root.mainloop() -------------------------------------------------------------------------------- /section04/lectures/01_labels/test_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/gui-development-tkinter-course/90a3979615997223f308832ce842e002cd571be4/section04/lectures/01_labels/test_image.png -------------------------------------------------------------------------------- /section04/lectures/02_text_widgets/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | # -- Windows only configuration -- 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | # -- End Windows only configuration -- 11 | 12 | root = tk.Tk() 13 | root.resizable(False, False) 14 | root.title("Widget Examples") 15 | 16 | # -- Text widgets -- 17 | 18 | # height is the number of rows of text 19 | text = tk.Text(root, height=8) # Show what happens if you `.pack()` here, while still assigning to variable. 20 | text.pack() 21 | 22 | # Insert content into the text area 23 | text.insert("1.0", "Please enter a comment...") # Can use \n to insert multiple lines. 24 | 25 | # First argument: position within textarea. 26 | # Second argument: text to insert after that position. 27 | 28 | # The position is given as two numbers, separated by a `.`. 29 | # First number is the line number, starting at 1. 30 | # Second number is character number within the line, starting at 0. 31 | # So 1.0 is the first line, first character. 32 | 33 | # -- Disable text widget -- 34 | 35 | text["state"] = "disabled" # "normal" is the counterpart 36 | 37 | # -- Get text content -- 38 | 39 | text_content = text.get("1.0", "end") 40 | print(text_content) 41 | 42 | root.mainloop() -------------------------------------------------------------------------------- /section04/lectures/03_scrollbars/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | # -- Windows only configuration -- 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | # -- End Windows only configuration -- 11 | 12 | 13 | root = tk.Tk() 14 | root.grid_columnconfigure(0, weight=1) 15 | root.grid_rowconfigure(0, weight=1) 16 | 17 | text = tk.Text(root, height=8) 18 | text.grid(row=0, column=0, sticky="ew") 19 | text.insert("1.0", "Please enter a comment...") 20 | 21 | text_scroll = ttk.Scrollbar(root, orient="vertical", command=text.yview) 22 | text_scroll.grid(row=0, column=1, sticky="ns") 23 | text['yscrollcommand'] = text_scroll.set 24 | 25 | root.mainloop() -------------------------------------------------------------------------------- /section04/lectures/04_separators/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | # -- Windows only configuration -- 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | # -- End Windows only configuration -- 11 | 12 | root = tk.Tk() 13 | root.title("Widget Examples") 14 | 15 | label = ttk.Label(root, text="Hello, World!", padding=20) 16 | # label.config(font=("Segoe UI", 20)) 17 | label.pack() 18 | 19 | main_sep = ttk.Separator(root, orient="horizontal") 20 | main_sep.pack(fill="x") # Remove fill, what happens? 21 | 22 | label = ttk.Label(root, text="Hello, World!", padding=20) 23 | # label.config(font=("Segoe UI", 20)) 24 | label.pack() 25 | 26 | root.mainloop() -------------------------------------------------------------------------------- /section04/lectures/05_check_buttons/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | # -- Windows only configuration -- 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | # -- End Windows only configuration -- 11 | 12 | root = tk.Tk() 13 | root.title("Widget Examples") 14 | 15 | # Like labels, here we can use images or compounds of images and text. 16 | check_button = ttk.Checkbutton(root, text="Check me!") 17 | check_button.pack() 18 | 19 | check_button["state"] = "disabled" # "normal" is the counterpart 20 | 21 | 22 | # -- All options -- 23 | 24 | selected_option = tk.StringVar() 25 | 26 | def print_current_option(): 27 | print(selected_option.get()) 28 | 29 | check = tk.Checkbutton( 30 | root, 31 | text="Check Example", 32 | variable=selected_option, 33 | command=print_current_option, 34 | onvalue="On", 35 | offvalue="Off" 36 | ) 37 | 38 | check.pack() 39 | 40 | 41 | root.mainloop() -------------------------------------------------------------------------------- /section04/lectures/06_radio_buttons/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | # -- Windows only configuration -- 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | # -- End Windows only configuration -- 11 | 12 | root = tk.Tk() 13 | root.title("Widget Examples") 14 | 15 | storage_variable = tk.StringVar() 16 | 17 | option_one = ttk.Radiobutton( 18 | root, 19 | text="Option 1", 20 | variable=storage_variable, 21 | value="First option" 22 | ) 23 | 24 | option_two = ttk.Radiobutton( 25 | root, 26 | text="Option 2", 27 | variable=storage_variable, 28 | value="Second option" 29 | ) 30 | 31 | option_three = ttk.Radiobutton( 32 | root, 33 | text="Option 3", 34 | variable=storage_variable, 35 | value="Third option" 36 | ) 37 | 38 | option_one.pack() 39 | option_two.pack() 40 | option_three.pack() 41 | 42 | 43 | root.mainloop() 44 | 45 | print(storage_variable.get(), "was selected.") -------------------------------------------------------------------------------- /section04/lectures/07_combo_boxes/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | # -- Windows only configuration -- 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | # -- End Windows only configuration -- 11 | 12 | root = tk.Tk() 13 | root.title("Widget Examples") 14 | 15 | selected_weekday = tk.StringVar() 16 | weekday = ttk.Combobox(root, textvariable=selected_weekday) 17 | weekday["values"] = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday") 18 | weekday["state"] = "readonly" # "normal is the counterpart" 19 | weekday.pack() 20 | 21 | 22 | def handle_selection(event): 23 | print("Today is", weekday.get()) 24 | print("But we're gonna change it to Friday.") 25 | weekday.set("Friday") 26 | print(weekday.current()) # This can return -1 if the user types their own value. 27 | 28 | 29 | weekday.bind("<>", handle_selection) 30 | 31 | root.mainloop() 32 | 33 | print(selected_weekday.get(), "was selected.") -------------------------------------------------------------------------------- /section04/lectures/08_list_boxes/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | # -- Windows only configuration -- 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | # -- End Windows only configuration -- 11 | 12 | root = tk.Tk() 13 | root.title("Widget Examples") 14 | 15 | programming_languages = ("C", "Go", "JavaScript", "Perl", "Python", "Rust") 16 | 17 | pl = tk.StringVar(value=programming_languages) 18 | pl_select = tk.Listbox(root, listvariable=pl, height=6) 19 | pl_select.pack(padx=10, pady=10) 20 | 21 | pl_select["selectmode"] = "extended" # Allows multiple selection, "browse" is the counterpart (yeah, I know it's a bad name!) 22 | 23 | 24 | def handle_selection_change(event): 25 | selected_indices = pl_select.curselection() 26 | for i in selected_indices: 27 | print(pl_select.get(i)) 28 | 29 | 30 | pl_select.bind("<>", handle_selection_change) 31 | root.mainloop() 32 | -------------------------------------------------------------------------------- /section04/lectures/09_spin_boxes/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | # -- Windows only configuration -- 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | # -- End Windows only configuration -- 11 | 12 | root = tk.Tk() 13 | root.title("Widget Examples") 14 | 15 | 16 | initial_value = tk.StringVar(value=20) 17 | spin_box = tk.Spinbox( 18 | root, 19 | from_=0, 20 | to=30, 21 | textvariable=initial_value, 22 | wrap=False) 23 | # spin_box = tk.Spinbox(root, values=(5, 10, 15, 20, 25, 30), textvariable=initial_value, wrap=False) 24 | # The alternative uses values instead of a range. 25 | 26 | spin_box.pack() 27 | 28 | print(spin_box.get()) # You'd usually use this when clicking a button (e.g. to submit) 29 | # Can't call `.get()` after the mainloop finishes, of course. 30 | 31 | root.mainloop() 32 | -------------------------------------------------------------------------------- /section04/lectures/10_scales/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | # -- Windows only configuration -- 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | # -- End Windows only configuration -- 11 | 12 | root = tk.Tk() 13 | root.title("Widget Examples") 14 | 15 | 16 | def handle_scale_change(event): 17 | print(scale.get()) # `.set()` can be used to change the value dynamically. 18 | 19 | 20 | scale = ttk.Scale(root, orient="horizontal", from_=0, to=10, command=handle_scale_change) 21 | scale.pack(fill="x") 22 | 23 | # scale["state"] = "disabled" # "normal" is the counterpart 24 | 25 | root.mainloop() 26 | -------------------------------------------------------------------------------- /section05/distance_converter/app_grid.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | root = tk.Tk() 5 | root.title("Distance Converter") 6 | 7 | feet_value = tk.StringVar() 8 | metres_value = tk.StringVar() 9 | 10 | 11 | def calculate_feet(*args): 12 | try: 13 | value = float(metres_value.get()) 14 | feet_value.set('%.3f' % (value * 3.28084)) 15 | except ValueError: 16 | pass 17 | 18 | 19 | main = ttk.Frame(root, padding="30 15 30 15") 20 | main.grid() 21 | 22 | root.columnconfigure(0, weight=1) 23 | root.rowconfigure(1, weight=1) 24 | 25 | metres_label = ttk.Label(main, text="metres") 26 | metres_label.grid(column=0, row=0, sticky="W", padx=5, pady=5) 27 | metres_input = ttk.Entry(main, width=10, textvariable=metres_value) 28 | metres_input.grid(column=1, row=0, sticky="EW", padx=5, pady=5) 29 | metres_input.focus() 30 | 31 | feet_label = ttk.Label(main, text="feet") 32 | feet_label.grid(column=0, row=1, sticky="W", padx=5, pady=5) 33 | feet_display = ttk.Label(main, textvariable=feet_value) 34 | feet_display.grid(column=1, row=1, sticky="EW", padx=5, pady=5) 35 | 36 | calc_button = ttk.Button(main, text="Calculate", command=calculate_feet) 37 | calc_button.grid(column=0, row=2, columnspan=2, sticky="EW", padx=5, pady=5) 38 | 39 | root.bind("", calculate_feet) 40 | root.bind("", calculate_feet) 41 | 42 | root.mainloop() 43 | -------------------------------------------------------------------------------- /section05/distance_converter/app_pack.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | root = tk.Tk() 5 | root.title("Distance Converter") 6 | 7 | feet_value = tk.StringVar() 8 | metres_value = tk.StringVar() 9 | 10 | 11 | def calculate_feet(*args): 12 | try: 13 | value = float(metres_value.get()) 14 | feet_value.set('%.3f' % (value * 3.28084)) 15 | except ValueError: 16 | pass 17 | 18 | 19 | main = ttk.Frame(root, padding="30 15 30 15") 20 | main.pack() 21 | 22 | metres_section = ttk.Frame(main) 23 | metres_section.pack(fill="both") 24 | 25 | metres_label = ttk.Label(metres_section, text="metres") 26 | metres_label.pack(side="left", padx=5, pady=5) 27 | metres_input = ttk.Entry(metres_section, width=10, textvariable=metres_value) 28 | metres_input.pack(side="left", padx=5, pady=5) 29 | metres_input.focus() 30 | 31 | feet_section = ttk.Frame(main) 32 | feet_section.pack(fill="both") 33 | 34 | feet_label = ttk.Label(feet_section, text="feet") 35 | feet_label.pack(side="left", padx=5, pady=5) 36 | feet_display = ttk.Label(feet_section, textvariable=feet_value) 37 | feet_display.pack(side="left", padx=5, pady=5) 38 | 39 | calc_button = ttk.Button(main, text="Calculate", command=calculate_feet) 40 | calc_button.pack(padx=5, pady=5, expand=True, fill="both") 41 | 42 | root.bind("", calculate_feet) 43 | root.bind("", calculate_feet) 44 | 45 | root.mainloop() 46 | -------------------------------------------------------------------------------- /section05/lectures/01_set_up/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | try: 5 | from ctypes import windll 6 | windll.shcore.SetProcessDpiAwareness(1) 7 | except: 8 | pass 9 | 10 | root = tk.Tk() 11 | root.title("Distance Converter") 12 | 13 | root.columnconfigure(0, weight=1) 14 | 15 | main = ttk.Frame(root, padding=(30, 15)) 16 | main.grid() # column=0 row=0 by default 17 | 18 | # -- Widgets -- 19 | 20 | metres_label = ttk.Label(main, text="metres") 21 | metres_input = ttk.Entry(main, width=10) 22 | feet_label = ttk.Label(main, text="feet") 23 | feet_display = ttk.Label(main, text="Feet shown here") 24 | calc_button = ttk.Button(main, text="Calculate") 25 | 26 | # -- Layout -- 27 | 28 | metres_label.grid(column=0, row=0, sticky="W", padx=5, pady=5) 29 | metres_input.grid(column=1, row=0, sticky="EW", padx=5, pady=5) 30 | metres_input.focus() 31 | 32 | feet_label.grid(column=0, row=1, sticky="W", padx=5, pady=5) 33 | feet_display.grid(column=1, row=1, sticky="EW", padx=5, pady=5) 34 | 35 | calc_button.grid(column=0, row=2, columnspan=2, sticky="EW", padx=5, pady=5) 36 | 37 | 38 | root.mainloop() -------------------------------------------------------------------------------- /section05/lectures/02_calculating_feet/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | try: 5 | from ctypes import windll 6 | windll.shcore.SetProcessDpiAwareness(1) 7 | except: 8 | pass 9 | 10 | root = tk.Tk() 11 | root.title("Distance Converter") 12 | 13 | metres_value = tk.StringVar() 14 | 15 | def calculate_feet(*args): 16 | try: 17 | metres = float(metres_value.get()) 18 | feet = metres * 3.28084 19 | print(f"{metres} metres is equal to {feet:.3f} feet.") 20 | except ValueError: 21 | pass 22 | 23 | 24 | main = ttk.Frame(root, padding=(30, 15)) 25 | main.grid() 26 | 27 | root.columnconfigure(0, weight=1) 28 | 29 | # -- Widgets -- 30 | 31 | metres_label = ttk.Label(main, text="metres") 32 | metres_input = ttk.Entry(main, width=10, textvariable=metres_value) 33 | feet_label = ttk.Label(main, text="feet") 34 | feet_display = ttk.Label(main, text="Feet shown here") 35 | calc_button = ttk.Button(main, text="Calculate", command=calculate_feet) 36 | 37 | # -- Layout -- 38 | 39 | metres_label.grid(column=0, row=0, sticky="W", padx=5, pady=5) 40 | metres_input.grid(column=1, row=0, sticky="EW", padx=5, pady=5) 41 | metres_input.focus() 42 | 43 | feet_label.grid(column=0, row=1, sticky="W", padx=5, pady=5) 44 | feet_display.grid(column=1, row=1, sticky="EW", padx=5, pady=5) 45 | 46 | calc_button.grid(column=0, row=2, columnspan=2, sticky="EW", padx=5, pady=5) 47 | 48 | root.mainloop() -------------------------------------------------------------------------------- /section05/lectures/03_changing_feet_label/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | try: 5 | from ctypes import windll 6 | windll.shcore.SetProcessDpiAwareness(1) 7 | except: 8 | pass 9 | 10 | root = tk.Tk() 11 | root.title("Distance Converter") 12 | 13 | metres_value = tk.StringVar() 14 | feet_value = tk.StringVar() 15 | 16 | def calculate_feet(*args): 17 | try: 18 | metres = float(metres_value.get()) 19 | feet = metres * 3.28084 20 | feet_value.set(f"{feet:.3f}") 21 | except ValueError: 22 | pass 23 | 24 | 25 | main = ttk.Frame(root, padding=(30, 15)) 26 | main.grid() 27 | 28 | root.columnconfigure(0, weight=1) 29 | 30 | # -- Widgets -- 31 | 32 | metres_label = ttk.Label(main, text="metres") 33 | metres_input = ttk.Entry(main, width=10, textvariable=metres_value) 34 | feet_label = ttk.Label(main, text="feet") 35 | feet_display = ttk.Label(main, textvariable=feet_value) 36 | calc_button = ttk.Button(main, text="Calculate", command=calculate_feet) 37 | 38 | # -- Layout -- 39 | 40 | metres_label.grid(column=0, row=0, sticky="W", padx=5, pady=5) 41 | metres_input.grid(column=1, row=0, sticky="EW", padx=5, pady=5) 42 | metres_input.focus() 43 | 44 | feet_label.grid(column=0, row=1, sticky="W", padx=5, pady=5) 45 | feet_display.grid(column=1, row=1, sticky="EW", padx=5, pady=5) 46 | 47 | calc_button.grid(column=0, row=2, columnspan=2, sticky="EW", padx=5, pady=5) 48 | 49 | root.mainloop() -------------------------------------------------------------------------------- /section05/lectures/04_adding_shortcuts/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | try: 5 | from ctypes import windll 6 | windll.shcore.SetProcessDpiAwareness(1) 7 | except: 8 | pass 9 | 10 | root = tk.Tk() 11 | root.title("Distance Converter") 12 | 13 | metres_value = tk.StringVar() 14 | feet_value = tk.StringVar() 15 | 16 | def calculate_feet(*args): 17 | try: 18 | metres = float(metres_value.get()) 19 | feet = metres * 3.28084 20 | feet_value.set(f"{feet:.3f}") 21 | except ValueError: 22 | pass 23 | 24 | 25 | main = ttk.Frame(root, padding=(30, 15)) 26 | main.grid() 27 | 28 | root.columnconfigure(0, weight=1) 29 | 30 | # -- Widgets -- 31 | 32 | metres_label = ttk.Label(main, text="metres") 33 | metres_input = ttk.Entry(main, width=10, textvariable=metres_value) 34 | feet_label = ttk.Label(main, text="feet") 35 | feet_display = ttk.Label(main, textvariable=feet_value) 36 | calc_button = ttk.Button(main, text="Calculate", command=calculate_feet) 37 | 38 | # -- Layout -- 39 | 40 | metres_label.grid(column=0, row=0, sticky="W", padx=5, pady=5) 41 | metres_input.grid(column=1, row=0, sticky="EW", padx=5, pady=5) 42 | metres_input.focus() 43 | 44 | feet_label.grid(column=0, row=1, sticky="W", padx=5, pady=5) 45 | feet_display.grid(column=1, row=1, sticky="EW", padx=5, pady=5) 46 | 47 | calc_button.grid(column=0, row=2, columnspan=2, sticky="EW", padx=5, pady=5) 48 | 49 | root.bind("", calculate_feet) 50 | root.bind("", calculate_feet) 51 | 52 | root.mainloop() -------------------------------------------------------------------------------- /section05/lectures/05_changing_font/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import tkinter.font as font 3 | from tkinter import ttk 4 | 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | 11 | root = tk.Tk() 12 | root.title("Distance Converter") 13 | 14 | font.nametofont("TkDefaultFont").configure(size=15) 15 | 16 | feet_value = tk.StringVar() 17 | metres_value = tk.StringVar() 18 | 19 | 20 | def calculate_feet(*args): 21 | try: 22 | metres = float(metres_value.get()) 23 | feet = metres * 3.28084 24 | feet_value.set(f"{feet:.3f}") 25 | except ValueError: 26 | pass 27 | 28 | 29 | main = ttk.Frame(root, padding=(60, 30)) 30 | main.grid() 31 | 32 | root.columnconfigure(0, weight=1) 33 | 34 | # -- Widgets -- 35 | 36 | metres_label = ttk.Label(main, text="metres") 37 | metres_input = ttk.Entry(main, width=10, textvariable=metres_value, font=(None, 15)) # None means "don't change the font". 38 | feet_label = ttk.Label(main, text="feet") 39 | feet_display = ttk.Label(main, textvariable=feet_value) 40 | calc_button = ttk.Button(main, text="Calculate", command=calculate_feet) 41 | 42 | # -- Layout -- 43 | 44 | metres_label.grid(column=0, row=0, sticky="W", padx=15, pady=15) 45 | metres_input.grid(column=1, row=0, sticky="EW", padx=15, pady=15) 46 | metres_input.focus() 47 | 48 | feet_label.grid(column=0, row=1, sticky="W", padx=15, pady=15) 49 | feet_display.grid(column=1, row=1, sticky="EW", padx=15, pady=15) 50 | 51 | calc_button.grid(column=0, row=2, columnspan=2, sticky="EW", padx=15, pady=15) 52 | 53 | 54 | root.bind("", calculate_feet) 55 | root.bind("", calculate_feet) 56 | 57 | root.mainloop() -------------------------------------------------------------------------------- /section05/lectures/06_winfo_children/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import tkinter.font as font 3 | from tkinter import ttk 4 | 5 | try: 6 | from ctypes import windll 7 | windll.shcore.SetProcessDpiAwareness(1) 8 | except: 9 | pass 10 | 11 | root = tk.Tk() 12 | root.title("Distance Converter") 13 | 14 | font.nametofont("TkDefaultFont").configure(size=15) 15 | 16 | feet_value = tk.StringVar() 17 | metres_value = tk.StringVar() 18 | 19 | 20 | def calculate_feet(*args): 21 | try: 22 | metres = float(metres_value.get()) 23 | feet = metres * 3.28084 24 | feet_value.set(f"{feet:.3f}") 25 | except ValueError: 26 | pass 27 | 28 | 29 | main = ttk.Frame(root, padding=(60, 30)) 30 | main.grid() 31 | 32 | root.columnconfigure(0, weight=1) 33 | 34 | # -- Widgets -- 35 | 36 | metres_label = ttk.Label(main, text="metres") 37 | metres_input = ttk.Entry(main, width=10, textvariable=metres_value, font=(None, 15)) # None means "don't change the font". 38 | feet_label = ttk.Label(main, text="feet") 39 | feet_display = ttk.Label(main, textvariable=feet_value) 40 | calc_button = ttk.Button(main, text="Calculate", command=calculate_feet) 41 | 42 | # -- Layout -- 43 | 44 | metres_label.grid(column=0, row=0, sticky="W") 45 | metres_input.grid(column=1, row=0, sticky="EW") 46 | metres_input.focus() 47 | 48 | feet_label.grid(column=0, row=1, sticky="W") 49 | feet_display.grid(column=1, row=1, sticky="EW") 50 | 51 | calc_button.grid(column=0, row=2, columnspan=2, sticky="EW") 52 | 53 | 54 | # winfo_children stands for "widget info children", and gets all the children of a widget. 55 | for child in main.winfo_children(): 56 | child.grid_configure(padx=15, pady=15) 57 | 58 | 59 | root.bind("", calculate_feet) 60 | root.bind("", calculate_feet) 61 | 62 | root.mainloop() -------------------------------------------------------------------------------- /section06/01_object_oriented_window/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class HelloWorld(tk.Tk): 6 | 7 | def __init__(self): 8 | super().__init__() 9 | 10 | self.title("Hello World!") 11 | 12 | ttk.Label(self, text="Hello, World!").pack() 13 | 14 | 15 | root = HelloWorld() 16 | root.mainloop() -------------------------------------------------------------------------------- /section06/02_object_oriented_frame/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class UserInputFrame(ttk.Frame): 6 | def __init__(self, container): 7 | super().__init__(container) 8 | 9 | self.user_input = tk.StringVar() 10 | 11 | label = ttk.Label(self, text="Enter your name: ") 12 | entry = ttk.Entry(self) 13 | button = ttk.Button(self, command=self.greet) 14 | 15 | label.pack(side="left") 16 | entry.pack(side="left") 17 | button.pack(side="left") 18 | 19 | def greet(self): 20 | print(f"Hello, {self.user_input.get()}!") 21 | 22 | 23 | 24 | root = tk.Tk() 25 | frame = UserInputFrame(root) 26 | frame.pack() 27 | 28 | root.mainloop() -------------------------------------------------------------------------------- /section06/03_object_oriented_app/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class HelloWorld(tk.Tk): 6 | 7 | def __init__(self): 8 | super().__init__() 9 | 10 | self.title("Hello World!") 11 | 12 | frame = UserInputFrame(self) 13 | frame.pack() 14 | 15 | 16 | class UserInputFrame(ttk.Frame): 17 | def __init__(self, container): 18 | super().__init__(container) 19 | 20 | self.user_input = tk.StringVar() 21 | 22 | label = ttk.Label(self, text="Enter your name: ") 23 | entry = ttk.Entry(self) 24 | button = ttk.Button(self, command=self.greet) 25 | 26 | label.pack(side="left") 27 | entry.pack(side="left") 28 | button.pack(side="left") 29 | 30 | def greet(self): 31 | print(f"Hello, {self.user_input.get()}!") 32 | 33 | 34 | 35 | root = HelloWorld() 36 | root.mainloop() -------------------------------------------------------------------------------- /section06/04_distance_converter_oop/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class DistanceConverter(tk.Tk): 6 | 7 | def __init__(self, *args, **kwargs): 8 | super().__init__(*args, **kwargs) 9 | 10 | self.title("Distance Converter") 11 | 12 | frame = MetresToFeet(self) 13 | frame.grid(row=0, column=0, sticky="NSEW") 14 | 15 | 16 | class MetresToFeet(ttk.Frame): 17 | 18 | def __init__(self, container): 19 | super().__init__(container) 20 | 21 | self.feet_value = tk.StringVar() 22 | self.metres_value = tk.StringVar() 23 | 24 | metres_label = ttk.Label(self, text="Metres:") 25 | metres_label.grid(column=1, row=1, sticky="W", ipadx=5) 26 | metres_input = ttk.Entry(self, width=10, textvariable=self.metres_value) 27 | metres_input.grid(column=2, row=1, sticky="EW") 28 | metres_input.focus() 29 | 30 | feet_label = ttk.Label(self, text="Feet:") 31 | feet_label.grid(column=1, row=2, sticky="W", ipadx=5) 32 | feet_display = ttk.Label(self, textvariable=self.feet_value) 33 | feet_display.grid(column=2, row=2, sticky="EW") 34 | 35 | calculate_button = ttk.Button( 36 | self, 37 | text="Calculate", 38 | command=self.calculate_feet 39 | ) 40 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 41 | 42 | for child in self.winfo_children(): 43 | child.grid_configure(padx=5, pady=5) 44 | 45 | def calculate_feet(self, *args): 46 | try: 47 | value = float(self.metres_value.get()) 48 | self.feet_value.set('%.3f' % (value * 3.28084)) 49 | except ValueError: 50 | pass 51 | 52 | 53 | root = DistanceConverter() 54 | root.mainloop() 55 | -------------------------------------------------------------------------------- /section06/05_adding_inner_container/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class DistanceConverter(tk.Tk): 6 | 7 | def __init__(self, *args, **kwargs): 8 | super().__init__(*args, **kwargs) 9 | 10 | self.title("Distance Calculator") 11 | 12 | container = ttk.Frame(self) 13 | container.grid(padx=10, pady=10, sticky="EW") 14 | 15 | frame = MetresToFeet(container) 16 | frame.grid(row=0, column=0, sticky="NSEW") 17 | 18 | 19 | class MetresToFeet(ttk.Frame): 20 | 21 | def __init__(self, container): 22 | super().__init__(container) 23 | 24 | self.feet_value = tk.StringVar() 25 | self.metres_value = tk.StringVar() 26 | 27 | metres_label = ttk.Label(self, text="Metres:") 28 | metres_label.grid(column=1, row=1, sticky="W", ipadx=5) 29 | metres_input = ttk.Entry(self, width=10, textvariable=self.metres_value) 30 | metres_input.grid(column=2, row=1, sticky="EW") 31 | metres_input.focus() 32 | 33 | feet_label = ttk.Label(self, text="Feet:") 34 | feet_label.grid(column=1, row=2, sticky="W", ipadx=5) 35 | feet_display = ttk.Label(self, textvariable=self.feet_value) 36 | feet_display.grid(column=2, row=2, sticky="EW") 37 | 38 | calculate_button = ttk.Button( 39 | self, 40 | text="Calculate", 41 | command=self.calculate_feet 42 | ) 43 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 44 | 45 | for child in self.winfo_children(): 46 | child.grid_configure(padx=5, pady=5) 47 | 48 | def calculate_feet(self, *args): 49 | try: 50 | value = float(self.metres_value.get()) 51 | self.feet_value.set('%.3f' % (value * 3.28084)) 52 | except ValueError: 53 | pass 54 | 55 | 56 | root = DistanceConverter() 57 | root.mainloop() 58 | -------------------------------------------------------------------------------- /section06/06_feet_to_metres/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class DistanceConverter(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.title("Distance Calculator") 10 | 11 | container = ttk.Frame(self) 12 | container.grid(padx=10, pady=10, sticky="EW") 13 | 14 | frame = MetresToFeet(container) # Alternative, FeetToMetres 15 | frame.grid(row=0, column=0, sticky="NSEW") 16 | 17 | 18 | class MetresToFeet(ttk.Frame): 19 | def __init__(self, container): 20 | super().__init__(container) 21 | 22 | self.feet_value = tk.StringVar() 23 | self.metres_value = tk.StringVar() 24 | 25 | metres_label = ttk.Label(self, text="Metres:") 26 | metres_label.grid(column=1, row=1, sticky="W", ipadx=5) 27 | metres_input = ttk.Entry(self, width=10, textvariable=self.metres_value) 28 | metres_input.grid(column=2, row=1, sticky="EW") 29 | metres_input.focus() 30 | 31 | feet_label = ttk.Label(self, text="Feet:") 32 | feet_label.grid(column=1, row=2, sticky="W", ipadx=5) 33 | feet_display = ttk.Label(self, textvariable=self.feet_value) 34 | feet_display.grid(column=2, row=2, sticky="EW") 35 | 36 | calculate_button = ttk.Button( 37 | self, 38 | text="Calculate", 39 | command=self.calculate_feet 40 | ) 41 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 42 | 43 | for child in self.winfo_children(): 44 | child.grid_configure(padx=5, pady=5) 45 | 46 | def calculate_feet(self, *args): 47 | try: 48 | value = float(self.metres_value.get()) 49 | self.feet_value.set('%.3f' % (value * 3.28084)) 50 | except ValueError: 51 | pass 52 | 53 | 54 | class FeetToMetres(ttk.Frame): 55 | def __init__(self, container): 56 | super().__init__(container) 57 | 58 | self.feet_value = tk.StringVar() 59 | self.metres_value = tk.StringVar() 60 | 61 | feet_label = ttk.Label(self, text="feet") 62 | feet_label.grid(column=1, row=1, sticky="W", ipadx=5) 63 | feet_input = ttk.Entry(self, width=10, textvariable=self.feet_value) 64 | feet_input.grid(column=2, row=1, sticky="EW") 65 | feet_input.focus() 66 | 67 | metres_label = ttk.Label(self, text="metres") 68 | metres_label.grid(column=1, row=2, sticky="W", ipadx=5) 69 | metres_display = ttk.Label(self, textvariable=self.metres_value) 70 | metres_display.grid(column=2, row=2, sticky="EW") 71 | 72 | calculate_button = ttk.Button( 73 | self, 74 | text="Calculate", 75 | command=self.calculate_metres 76 | ) 77 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 78 | 79 | for child in self.winfo_children(): 80 | child.grid_configure(padx=5, pady=5) 81 | 82 | def calculate_metres(self, *args): 83 | try: 84 | value = float(self.feet_value.get()) 85 | self.metres_value.set('%.3f' % (value / 3.28084)) 86 | except ValueError: 87 | pass 88 | 89 | 90 | root = DistanceConverter() 91 | root.mainloop() 92 | -------------------------------------------------------------------------------- /section06/07_switching_between_frames/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class DistanceConverter(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.title("Distance Calculator") 10 | self.frames = dict() 11 | 12 | container = ttk.Frame(self) 13 | container.grid(padx=10, pady=10, sticky="EW") 14 | 15 | """ 16 | metres_to_feet = MetresToFeet(container, self) 17 | metres_to_feet.grid(row=0, column=0, sticky="NSEW") 18 | 19 | feet_to_metres = FeetToMetres(container, self) 20 | feet_to_metres.grid(row=0, column=0, sticky="NSEW") 21 | 22 | self.frames[MetresToFeet] = metres_to_feet 23 | self.frames[FeetToMetres] = feet_to_metres 24 | """ 25 | 26 | for FrameClass in (MetresToFeet, FeetToMetres): 27 | frame = FrameClass(container, self) 28 | self.frames[FrameClass] = frame 29 | frame.grid(row=0, column=0, sticky="NSEW") 30 | 31 | self.show_frame(MetresToFeet) 32 | 33 | def show_frame(self, container): 34 | frame = self.frames[container] 35 | frame.tkraise() 36 | 37 | 38 | class MetresToFeet(ttk.Frame): 39 | def __init__(self, container, controller): 40 | super().__init__(container) 41 | 42 | self.feet_value = tk.StringVar() 43 | self.metres_value = tk.StringVar() 44 | 45 | metres_label = ttk.Label(self, text="Metres:") 46 | metres_label.grid(column=1, row=1, sticky="W", ipadx=5) 47 | metres_input = ttk.Entry(self, width=10, textvariable=self.metres_value) 48 | metres_input.grid(column=2, row=1, sticky="EW") 49 | metres_input.focus() 50 | 51 | feet_label = ttk.Label(self, text="Feet:") 52 | feet_label.grid(column=1, row=2, sticky="W", ipadx=5) 53 | feet_display = ttk.Label(self, textvariable=self.feet_value) 54 | feet_display.grid(column=2, row=2, sticky="EW") 55 | 56 | calculate_button = ttk.Button( 57 | self, 58 | text="Calculate", 59 | command=self.calculate_feet 60 | ) 61 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 62 | 63 | switch_page_button = ttk.Button( 64 | self, 65 | text="Switch to feet conversion", 66 | command=lambda: controller.show_frame(FeetToMetres) 67 | ) 68 | switch_page_button.grid(column=1, row=4, columnspan=2, sticky="EW") 69 | 70 | for child in self.winfo_children(): 71 | child.grid_configure(padx=5, pady=5) 72 | 73 | def calculate_feet(self, *args): 74 | try: 75 | value = float(self.metres_value.get()) 76 | self.feet_value.set('%.3f' % (value * 3.28084)) 77 | except ValueError: 78 | pass 79 | 80 | 81 | class FeetToMetres(ttk.Frame): 82 | def __init__(self, container, controller): 83 | super().__init__(container) 84 | 85 | self.feet_value = tk.StringVar() 86 | self.metres_value = tk.StringVar() 87 | 88 | feet_label = ttk.Label(self, text="Feet:") 89 | feet_label.grid(column=1, row=1, sticky="W", ipadx=5) 90 | feet_input = ttk.Entry(self, width=10, textvariable=self.feet_value) 91 | feet_input.grid(column=2, row=1, sticky="EW") 92 | feet_input.focus() 93 | 94 | metres_label = ttk.Label(self, text="Metres:") 95 | metres_label.grid(column=1, row=2, sticky="W", ipadx=5) 96 | metres_display = ttk.Label(self, textvariable=self.metres_value) 97 | metres_display.grid(column=2, row=2, sticky="EW") 98 | 99 | calculate_button = ttk.Button( 100 | self, 101 | text="Calculate", 102 | command=self.calculate_metres 103 | ) 104 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 105 | 106 | switch_page_button = ttk.Button( 107 | self, 108 | text="Switch to metres conversion", 109 | command=lambda: controller.show_frame(MetresToFeet) 110 | ) 111 | switch_page_button.grid(column=1, row=4, columnspan=2, sticky="EW") 112 | 113 | for child in self.winfo_children(): 114 | child.grid_configure(padx=5, pady=5) 115 | 116 | def calculate_metres(self, *args): 117 | try: 118 | value = float(self.feet_value.get()) 119 | self.metres_value.set('%.3f' % (value / 3.28084)) 120 | except ValueError: 121 | pass 122 | 123 | 124 | root = DistanceConverter() 125 | root.mainloop() 126 | -------------------------------------------------------------------------------- /section06/08_adding_keybindings/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class DistanceConverter(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.title("Distance Calculator") 10 | self.frames = dict() 11 | 12 | container = ttk.Frame(self) 13 | container.grid(padx=10, pady=10, sticky="EW") 14 | 15 | for FrameClass in (MetresToFeet, FeetToMetres): 16 | frame = FrameClass(container, self) 17 | self.frames[FrameClass] = frame 18 | frame.grid(row=0, column=0, sticky="NSEW") 19 | 20 | self.show_frame(MetresToFeet) 21 | 22 | def show_frame(self, container): 23 | frame = self.frames[container] 24 | self.bind("", frame.calculate) 25 | self.bind("", frame.calculate) 26 | frame.tkraise() 27 | 28 | 29 | class MetresToFeet(ttk.Frame): 30 | def __init__(self, container, controller): 31 | super().__init__(container) 32 | 33 | self.feet_value = tk.StringVar() 34 | self.metres_value = tk.StringVar() 35 | 36 | metres_label = ttk.Label(self, text="Metres:") 37 | metres_label.grid(column=1, row=1, sticky="W", ipadx=5) 38 | metres_input = ttk.Entry(self, width=10, textvariable=self.metres_value) 39 | metres_input.grid(column=2, row=1, sticky="EW") 40 | metres_input.focus() 41 | 42 | feet_label = ttk.Label(self, text="Feet:") 43 | feet_label.grid(column=1, row=2, sticky="W", ipadx=5) 44 | feet_display = ttk.Label(self, textvariable=self.feet_value) 45 | feet_display.grid(column=2, row=2, sticky="EW") 46 | 47 | calculate_button = ttk.Button( 48 | self, 49 | text="Calculate", 50 | command=self.calculate 51 | ) 52 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 53 | 54 | switch_page_button = ttk.Button( 55 | self, 56 | text="Switch to feet conversion", 57 | command=lambda: controller.show_frame(FeetToMetres) 58 | ) 59 | switch_page_button.grid(column=1, row=4, columnspan=2, sticky="EW") 60 | 61 | for child in self.winfo_children(): 62 | child.grid_configure(padx=5, pady=5) 63 | 64 | def calculate(self, *args): 65 | try: 66 | value = float(self.metres_value.get()) 67 | self.feet_value.set('%.3f' % (value * 3.28084)) 68 | except ValueError: 69 | pass 70 | 71 | 72 | class FeetToMetres(ttk.Frame): 73 | def __init__(self, container, controller): 74 | super().__init__(container) 75 | 76 | self.feet_value = tk.StringVar() 77 | self.metres_value = tk.StringVar() 78 | 79 | feet_label = ttk.Label(self, text="Feet:") 80 | feet_label.grid(column=1, row=1, sticky="W", ipadx=5) 81 | feet_input = ttk.Entry(self, width=10, textvariable=self.feet_value) 82 | feet_input.grid(column=2, row=1, sticky="EW") 83 | feet_input.focus() 84 | 85 | metres_label = ttk.Label(self, text="Metres:") 86 | metres_label.grid(column=1, row=2, sticky="W", ipadx=5) 87 | metres_display = ttk.Label(self, textvariable=self.metres_value) 88 | metres_display.grid(column=2, row=2, sticky="EW") 89 | 90 | calculate_button = ttk.Button( 91 | self, 92 | text="Calculate", 93 | command=self.calculate 94 | ) 95 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 96 | 97 | switch_page_button = ttk.Button( 98 | self, 99 | text="Switch to metres conversion", 100 | command=lambda: controller.show_frame(MetresToFeet) 101 | ) 102 | switch_page_button.grid(column=1, row=4, columnspan=2, sticky="EW") 103 | 104 | for child in self.winfo_children(): 105 | child.grid_configure(padx=5, pady=5) 106 | 107 | def calculate(self, *args): 108 | try: 109 | value = float(self.feet_value.get()) 110 | self.metres_value.set('%.3f' % (value / 3.28084)) 111 | except ValueError: 112 | pass 113 | 114 | 115 | root = DistanceConverter() 116 | root.mainloop() 117 | -------------------------------------------------------------------------------- /section06/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class DistanceConverter(tk.Tk): 6 | 7 | def __init__(self, *args, **kwargs): 8 | super().__init__(*args, **kwargs) 9 | 10 | self.title("Distance Calculator") 11 | self.frames = dict() 12 | 13 | container = ttk.Frame(self) 14 | container.grid(padx=10, pady=10) 15 | container.columnconfigure(0, weight=1) 16 | 17 | for FrameClass in (MetresToFeet, FeetToMetres): 18 | frame = FrameClass(container, self) 19 | self.frames[FrameClass] = frame 20 | frame.grid(row=0, column=0, sticky="NSEW") 21 | 22 | self.show_frame(MetresToFeet) 23 | 24 | def show_frame(self, container): 25 | frame = self.frames[container] 26 | self.bind("", frame.calculate) 27 | self.bind("", frame.calculate) 28 | frame.tkraise() 29 | 30 | 31 | class MetresToFeet(ttk.Frame): 32 | 33 | def __init__(self, container, controller): 34 | super().__init__(container) 35 | 36 | self.feet_value = tk.StringVar() 37 | self.metres_value = tk.StringVar() 38 | self.columnconfigure((0, 1), weight=1) 39 | 40 | metres_label = ttk.Label(self, text="Metres:") 41 | metres_label.grid(column=1, row=1, sticky="W", ipadx=5) 42 | metres_input = ttk.Entry(self, width=10, textvariable=self.metres_value) 43 | metres_input.grid(column=1, row=0, sticky="EW") 44 | metres_input.focus() 45 | 46 | feet_label = ttk.Label(self, text="Feet:") 47 | feet_label.grid(column=1, row=2, sticky="W", ipadx=5) 48 | feet_display = ttk.Label(self, textvariable=self.feet_value) 49 | feet_display.grid(column=1, row=1, sticky="EW") 50 | 51 | calculate_button = ttk.Button( 52 | self, 53 | text="Calculate", 54 | command=self.calculate_feet 55 | ) 56 | calculate_button.grid(column=0, row=2, columnspan=2, sticky="EW") 57 | 58 | switch_page_button = ttk.Button( 59 | self, 60 | text="Switch to feet conversion", 61 | command=lambda: controller.show_frame(FeetToMetres) 62 | ) 63 | switch_page_button.grid(column=0, row=3, columnspan=2, sticky="EW") 64 | 65 | for child in self.winfo_children(): 66 | child.grid_configure(padx=5, pady=5) 67 | 68 | def calculate_feet(self, *args): 69 | try: 70 | value = float(self.metres_value.get()) 71 | self.feet_value.set('%.3f' % (value * 3.28084)) 72 | except ValueError: 73 | pass 74 | 75 | 76 | class FeetToMetres(ttk.Frame): 77 | 78 | def __init__(self, container, controller): 79 | super().__init__(container) 80 | 81 | self.feet_value = tk.StringVar() 82 | self.metres_value = tk.StringVar() 83 | self.columnconfigure((0, 1), weight=1) 84 | 85 | feet_label = ttk.Label(self, text="Feet:") 86 | feet_label.grid(column=1, row=1, sticky="W", ipadx=5) 87 | feet_input = ttk.Entry(self, width=10, textvariable=self.feet_value) 88 | feet_input.grid(column=2, row=1, sticky="EW") 89 | feet_input.focus() 90 | 91 | metres_label = ttk.Label(self, text="Metres:") 92 | metres_label.grid(column=1, row=2, sticky="W", ipadx=5) 93 | metres_display = ttk.Label(self, textvariable=self.metres_value) 94 | metres_display.grid(column=2, row=2, sticky="EW") 95 | 96 | calculate_button = ttk.Button( 97 | self, 98 | text="Calculate", 99 | command=self.calculate_metres 100 | ) 101 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 102 | 103 | switch_page_button = ttk.Button( 104 | self, 105 | text="Switch to metres conversion", 106 | command=lambda: controller.show_frame(MetresToFeet) 107 | ) 108 | switch_page_button.grid(column=1, row=4, columnspan=2, sticky="EW") 109 | 110 | for child in self.winfo_children(): 111 | child.grid_configure(padx=5, pady=5) 112 | 113 | def calculate_metres(self, *args): 114 | try: 115 | value = float(self.feet_value.get()) 116 | self.metres_value.set('%.3f' % (value / 3.28084)) 117 | except ValueError: 118 | pass 119 | 120 | 121 | root = DistanceConverter() 122 | root.mainloop() 123 | -------------------------------------------------------------------------------- /section07/01_ttk_style_themes/README.md: -------------------------------------------------------------------------------- 1 | # ttk Style themes 2 | 3 | We can access the style database by creating an instance of `ttk.Style()`: 4 | 5 | ```python 6 | import tkinter as tk 7 | from tkinter import ttk 8 | 9 | root = tk.Tk() 10 | style = ttk.Style(root) 11 | 12 | ... 13 | ``` 14 | 15 | This allows us to access and customize styles for the Tk application passed, in this case `root`. 16 | 17 | ## Themes 18 | 19 | Every Operating System comes with themes. A theme is a collection of styles for Tkinter widgets, so that if you enable one theme, those styles will be applied. 20 | 21 | You can see which themes are available like so: 22 | 23 | ```python 24 | style = ttk.Style(root) 25 | print(style.theme_names()) 26 | ``` 27 | 28 | And you can change the theme like so: 29 | 30 | ```python 31 | style = ttk.Style(root) 32 | style.theme_use("classic") # Use a theme available in your system 33 | ``` -------------------------------------------------------------------------------- /section07/01_ttk_style_themes/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from windows import set_dpi_awareness 4 | 5 | set_dpi_awareness() 6 | 7 | 8 | def greet(*args): 9 | print(f"Hello, {user_name.get()}!") 10 | 11 | 12 | # Do not create the Style() database before initialising a window, as it will create one 13 | # style = ttk.Style() 14 | 15 | root = tk.Tk() 16 | root.resizable(False, False) 17 | root.title("Greeter") 18 | 19 | # Create it after instead 20 | style = ttk.Style(root) # Pass in which application this style is for. 21 | 22 | # Get the themes available in your system 23 | print(style.theme_names()) 24 | 25 | print(style.theme_use()) 26 | print(style.theme_use("default")) 27 | 28 | main = ttk.Frame(root, padding=(40, 20)) 29 | main.grid() 30 | 31 | user_name = tk.StringVar() 32 | 33 | name_label = ttk.Label(main, text="Name:") 34 | name_label.grid(row=0, column=0, padx=(0, 10)) 35 | name_entry = ttk.Entry(main, width=15, textvariable=user_name) 36 | name_entry.grid(row=0, column=1, padx=10) 37 | name_entry.focus() 38 | 39 | greet_button = ttk.Button(main, text="Greet", command=greet, style="PomodoroButton.TButton") 40 | greet_button.grid(row=0, column=2, sticky="EW", padx=10) 41 | 42 | root.mainloop() 43 | 44 | -------------------------------------------------------------------------------- /section07/01_ttk_style_themes/windows.py: -------------------------------------------------------------------------------- 1 | def set_dpi_awareness(): 2 | try: 3 | from ctypes import windll 4 | windll.shcore.SetProcessDpiAwareness(1) 5 | except: 6 | pass -------------------------------------------------------------------------------- /section07/02_finding_widget_style_class/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | root = tk.Tk() 6 | style = ttk.Style(root) 7 | 8 | name = ttk.Label(root, text="Hello, world!") 9 | entry = ttk.Entry(root, width=15) 10 | name.pack() 11 | 12 | print(name.winfo_class()) 13 | print(entry.winfo_class()) # No need to place in window to get style class 14 | 15 | root.mainloop() 16 | 17 | -------------------------------------------------------------------------------- /section07/03_changing_styles/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | root = tk.Tk() 6 | style = ttk.Style(root) 7 | 8 | name = ttk.Label(root, text="Hello, world!", style="TLabel") # Change to CustomLabel and see error appear 9 | entry = ttk.Entry(root, width=15) 10 | # entry["style"] = "CustomBtnStyle" # Change style after creation is possible too 11 | name.pack() 12 | 13 | # This is how to change a property of the style. 14 | # What can we change? Next video will tell us... 15 | style.configure("TLabel", font=("Segoe UI", 20)) 16 | 17 | root.mainloop() 18 | 19 | -------------------------------------------------------------------------------- /section07/04_what_properties_can_we_change/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | root = tk.Tk() 6 | style = ttk.Style(root) 7 | 8 | name = ttk.Label(root, text="Hello, world!") 9 | name.pack() 10 | 11 | # This tells us the elements within a widget 12 | print(style.layout("TLabel")) 13 | 14 | # This tells us the modifiable properties of each element. 15 | # Something _really_ important is that these can be different per-theme. 16 | print(style.element_options("Label.border")) 17 | print(style.element_options("Label.padding")) 18 | print(style.element_options("Label.label")) 19 | 20 | # This tells us the current value of a property 21 | print(style.lookup("TLabel", "font")) 22 | print(style.lookup("TLabel", "foreground")) 23 | print(style.lookup("TLabel", "compound")) # Can be empty if not set 24 | 25 | 26 | style.theme_use("clam") 27 | 28 | # This tells us the modifiable properties of each element. 29 | # Something _really_ important is that these can be different per-theme. 30 | print(style.element_options("Label.border")) 31 | print(style.element_options("Label.padding")) 32 | print(style.element_options("Label.label")) 33 | 34 | # This tells us the current value of a property 35 | print(style.lookup("TLabel", "font")) 36 | print(style.lookup("TLabel", "foreground")) 37 | print(style.lookup("TLabel", "compound")) # Can be empty if not set 38 | 39 | style.configure("TLabel", bordercolor="#f00") 40 | style.configure("TLabel", borderwidth=40) 41 | style.configure("TLabel", relief="solid") 42 | # More info: https://stackoverflow.com/a/39416145/1587271 43 | 44 | root.mainloop() 45 | 46 | -------------------------------------------------------------------------------- /section07/05_creating_new_styles/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | root = tk.Tk() 6 | style = ttk.Style(root) 7 | 8 | # Add this after showing error happens when using non-existent style 9 | style.configure("CustomEntryStyle.TEntry", padding=20) # This adds an inherited style, copying everything from TEntry 10 | 11 | name = ttk.Label(root, text="Hello, world!", style="TLabel") # Change to CustomLabel and see error appear 12 | entry = ttk.Entry(root, width=15) 13 | entry["style"] = "CustomEntryStyle.TEntry" # Change style after creation is possible too 14 | name.pack() 15 | entry.pack() 16 | 17 | # Add this even later, to show the ttk.Entry hasn't changed, it's just due to the style. 18 | entry2 = ttk.Entry(root, width=15) 19 | entry2.pack() 20 | 21 | root.mainloop() 22 | 23 | -------------------------------------------------------------------------------- /section07/06_state_specific_options/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | root = tk.Tk() 6 | style = ttk.Style(root) 7 | 8 | style.theme_use("clam") 9 | 10 | style.map("CustomButton.TButton", 11 | foreground=[('pressed', 'red'), ('active', 'white')], 12 | background=[('pressed', '!disabled', 'black'), ('active', 'black')], # Note that background colour can't be changed in the default Windows theme. 13 | font=[("pressed", ("TkDefaultFont", 15))] # This may not make sense, but it still works 14 | ) 15 | 16 | name = ttk.Label(root, text="Hello, world!") 17 | entry = ttk.Entry(root, width=15) 18 | button = ttk.Button(root, text="Press me.", style="CustomButton.TButton") 19 | name.pack() 20 | entry.pack() 21 | button.pack() 22 | 23 | 24 | root.mainloop() -------------------------------------------------------------------------------- /section07/07_changing_entry_field_font_with_styles/README.md: -------------------------------------------------------------------------------- 1 | # Changing ttk.Entry field font with ttk.Style 2 | 3 | The gist of is, you can't. There might be a bug in the Tk engine which means that applying a `ttk.Style()` which uses the `font=` argument on a `ttk.Entry` results in the font change not being shown on the entry field. 4 | 5 | Therefore, for `ttk.Entry` fields you must pass the `font=` argument directly to the constructor. 6 | 7 | In other words, this won't work on entry fields. Or at least, the font will continue being the default font: 8 | 9 | ```python 10 | style = ttk.Style() 11 | style.configure("LargeEntry.TEntry", font=("Segoe UI", 15)) 12 | entry = ttk.Entry(root, style="LargeEntry.TEntry") 13 | entry.pack() 14 | ``` 15 | 16 | However, this _will_ change the font: 17 | 18 | ```python 19 | entry = ttk.Entry(root, font=("Segoe UI", 15)) 20 | entry.pack() 21 | ``` 22 | 23 | ## Using the global entry font 24 | 25 | The next lecture in this course talks how to use the global entry font, and how that can greatly simplify your font-setting on `ttk.Entry` fields. However, it is partially limited in that it will change the styling for _all_ entry fields: 26 | 27 | ```python 28 | import tkinter.font as font 29 | 30 | ... 31 | 32 | font.nametofont("TkEntryFont").configure(size=15) 33 | 34 | ... 35 | 36 | entry = ttk.Entry(root) # The font is 15 pixels 37 | entry.pack() 38 | ``` 39 | 40 | ## Tkinter specially named fonts 41 | 42 | Font name | Description 43 | -------------------|---------------------------------------------- 44 | TkDefaultFont | Default for items not otherwise specified. 45 | TkTextFont | Used for entry widgets, listboxes, etc. 46 | TkFixedFont | A standard fixed-width font. 47 | TkMenuFont | The font used for menu items. 48 | TkHeadingFont | Font for column headings in lists and tables. 49 | TkCaptionFont | A font for window and dialog caption bars. 50 | TkSmallCaptionFont | A smaller caption font for tool dialogs. 51 | TkIconFont | A font for icon captions. 52 | TkTooltipFont | A font for tooltips. 53 | 54 | ## Using named fonts 55 | 56 | The most flexible approach is to define custom fonts (that can inherit from `TkEntryFont` or `TkDefaultFont`), and then manually pass them to each entry field. We also look at that in this course, in the lecture after the next. -------------------------------------------------------------------------------- /section07/08_named_fonts/README.md: -------------------------------------------------------------------------------- 1 | # Named fonts 2 | 3 | Read the Tk Docs for more: https://tkdocs.com/tutorial/fonts.html 4 | 5 | A couple of things are important to know: 6 | 7 | > The "family" specifies the font name; the names Courier, Times, and Helvetica are guaranteed to be supported (and mapped to an appropriate monospaced, serif, or sans-serif font). 8 | 9 | ## Create your own font 10 | 11 | Creating your own font is easy enough. It gets stored in a variable and can be reused anywhere where our known font tuple could be passed: 12 | 13 | ```python 14 | import tkinter.font as font 15 | 16 | warningLabelFont = font.Font(family="Helvetica", size=14, weight="bold") 17 | ``` 18 | 19 | ## Re-use the default font 20 | 21 | If you do this: 22 | 23 | ```python 24 | import tkinter.font as font 25 | 26 | warningLabelFont = font.nametofont("TkDefaultFont") 27 | warningLabelFont.configure(size=15) 28 | ``` 29 | 30 | You'll have problems, because the `warningLabelFont` _is_ the default font. That means that all widgets which use the default font will change size. 31 | 32 | Instead, `.copy()` the font to only use it in certain widgets: 33 | 34 | ```python 35 | import tkinter.font as font 36 | 37 | warningLabelFont = font.nametofont("TkDefaultFont").copy() 38 | warningLabelFont.configure(size=15) 39 | ``` -------------------------------------------------------------------------------- /section07/08_named_fonts/code.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import tkinter.font as font 4 | 5 | root = tk.Tk() 6 | style = ttk.Style(root) 7 | 8 | print(font.families()) -------------------------------------------------------------------------------- /section07/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import tkinter.font as font 4 | from windows import set_dpi_awareness 5 | 6 | set_dpi_awareness() 7 | 8 | 9 | class DistanceConverter(tk.Tk): 10 | def __init__(self, *args, **kwargs): 11 | super().__init__(*args, **kwargs) 12 | 13 | self.title("Distance Converter") 14 | self.frames = dict() 15 | 16 | container = ttk.Frame(self) 17 | container.grid(padx=60, pady=30, sticky="EW") 18 | 19 | for FrameClass in (MetresToFeet, FeetToMetres): 20 | frame = FrameClass(container, self) 21 | self.frames[FrameClass] = frame 22 | frame.grid(row=0, column=0, sticky="NSEW") 23 | 24 | self.show_frame(MetresToFeet) 25 | 26 | def show_frame(self, container): 27 | frame = self.frames[container] 28 | self.bind("", frame.calculate) 29 | self.bind("", frame.calculate) 30 | frame.tkraise() 31 | 32 | 33 | class MetresToFeet(ttk.Frame): 34 | def __init__(self, container, controller, **kwargs): 35 | super().__init__(container, **kwargs) 36 | 37 | self.feet_value = tk.StringVar() 38 | self.metres_value = tk.StringVar() 39 | 40 | metres_label = ttk.Label(self, text="Metres:") 41 | metres_input = ttk.Entry(self, width=10, textvariable=self.metres_value) 42 | feet_label = ttk.Label(self, text="Feet:") 43 | feet_display = ttk.Label(self, textvariable=self.feet_value) 44 | calc_button = ttk.Button(self, text="Calculate", command=self.calculate) 45 | switch_page_button = ttk.Button( 46 | self, 47 | text="Switch to feet conversion", 48 | command=lambda: controller.show_frame(FeetToMetres) 49 | ) 50 | 51 | metres_label.grid(column=0, row=0, sticky="W") 52 | metres_input.grid(column=1, row=0, sticky="EW") 53 | metres_input.focus() 54 | 55 | feet_label.grid(column=0, row=1, sticky="W") 56 | feet_display.grid(column=1, row=1, sticky="EW") 57 | 58 | calc_button.grid(column=0, row=2, columnspan=2, sticky="EW") 59 | switch_page_button.grid(column=0, row=3, columnspan=2, sticky="EW") 60 | 61 | for child in self.winfo_children(): 62 | child.grid_configure(padx=15, pady=15) 63 | 64 | def calculate(self, *args): 65 | try: 66 | metres = float(self.metres_value.get()) 67 | feet = metres * 3.28084 68 | self.feet_value.set(f"{feet:.3f}") 69 | except ValueError: 70 | pass 71 | 72 | 73 | class FeetToMetres(ttk.Frame): 74 | def __init__(self, container, controller, **kwargs): 75 | super().__init__(container, **kwargs) 76 | 77 | self.feet_value = tk.StringVar() 78 | self.metres_value = tk.StringVar() 79 | 80 | feet_label = ttk.Label(self, text="Feet:") 81 | feet_input = ttk.Entry(self, width=10, textvariable=self.feet_value) 82 | metres_label = ttk.Label(self, text="Metres:") 83 | metres_display = ttk.Label(self, textvariable=self.metres_value) 84 | calc_button = ttk.Button(self, text="Calculate", command=self.calculate, style="AwesomeBtnStyle.TButton") 85 | switch_page_button = ttk.Button( 86 | self, 87 | text="Switch to metres conversion", 88 | command=lambda: controller.show_frame(MetresToFeet) 89 | ) 90 | 91 | feet_label.grid(column=0, row=0, sticky="W") 92 | feet_input.grid(column=1, row=0, sticky="EW") 93 | feet_input.focus() 94 | 95 | metres_label.grid(column=0, row=1, sticky="W") 96 | metres_display.grid(column=1, row=1, sticky="EW") 97 | 98 | calc_button.grid(column=0, row=2, columnspan=2, sticky="EW") 99 | switch_page_button.grid(column=0, row=3, columnspan=2, sticky="EW") 100 | 101 | for child in self.winfo_children(): 102 | child.grid_configure(padx=15, pady=15) 103 | 104 | def calculate(self, *args): 105 | try: 106 | feet = float(self.feet_value.get()) 107 | metres = feet / 3.28084 108 | self.metres_value.set(f"{metres:.3f}") 109 | except ValueError: 110 | pass 111 | 112 | 113 | root = DistanceConverter() 114 | 115 | font.nametofont("TkDefaultFont").configure(size=15) 116 | font.nametofont("TkTextFont").configure(size=15) 117 | 118 | root.columnconfigure(0, weight=1) 119 | root.mainloop() 120 | -------------------------------------------------------------------------------- /section07/windows.py: -------------------------------------------------------------------------------- 1 | def set_dpi_awareness(): 2 | try: 3 | from ctypes import windll 4 | windll.shcore.SetProcessDpiAwareness(1) 5 | except: 6 | pass -------------------------------------------------------------------------------- /section08/lectures/02_countdown_timer/timer.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class PomodoroTimer(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.title("Pomodoro Timer") 10 | self.columnconfigure(0, weight=1) 11 | self.rowconfigure(1, weight=1) 12 | 13 | container = ttk.Frame(self) 14 | container.grid() 15 | container.columnconfigure(0, weight=1) 16 | 17 | timer_frame = Timer(container) 18 | timer_frame.grid(row=0, column=0, sticky="NESW") 19 | 20 | 21 | class Timer(ttk.Frame): 22 | def __init__(self, parent): 23 | super().__init__(parent) 24 | 25 | self.current_time = tk.StringVar(value="00:10") 26 | self.timer_running = True 27 | 28 | timer_frame = ttk.Frame(self, height="100") 29 | timer_frame.grid(pady=(10, 0), sticky="NSEW") 30 | 31 | timer_counter = ttk.Label( 32 | timer_frame, 33 | textvariable=self.current_time 34 | ) 35 | timer_counter.grid() 36 | 37 | self.decrement_time() 38 | 39 | def decrement_time(self): 40 | current_time = self.current_time.get() 41 | 42 | if self.timer_running and current_time != "00:00": 43 | minutes, seconds = current_time.split(":") 44 | 45 | if int(seconds) > 0: 46 | seconds = int(seconds) - 1 47 | minutes = int(minutes) 48 | else: 49 | seconds = 59 50 | minutes = int(minutes) - 1 51 | 52 | self.current_time.set(f"{minutes:02d}:{seconds:02d}") 53 | self.after(1000, self.decrement_time) 54 | 55 | 56 | app = PomodoroTimer() 57 | app.mainloop() -------------------------------------------------------------------------------- /section08/lectures/03_timer_breaks/timer.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from collections import deque 4 | 5 | class PomodoroTimer(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.title("Pomodoro Timer") 10 | self.columnconfigure(0, weight=1) 11 | self.rowconfigure(1, weight=1) 12 | 13 | container = ttk.Frame(self) 14 | container.grid() 15 | container.columnconfigure(0, weight=1) 16 | 17 | timer_frame = Timer(container) 18 | timer_frame.grid(row=0, column=0, sticky="NESW") 19 | 20 | 21 | class Timer(ttk.Frame): 22 | def __init__(self, parent): 23 | super().__init__(parent) 24 | 25 | self.current_time = tk.StringVar(value="00:10") 26 | self.timer_order = ["Pomodoro", "Short Break", "Pomodoro", "Short Break", "Pomodoro", "Long Break"] 27 | self.timer_schedule = deque(self.timer_order) 28 | self.timer_running = True 29 | 30 | timer_frame = ttk.Frame(self, height="100") 31 | timer_frame.grid(pady=(10, 0), sticky="NSEW") 32 | 33 | timer_counter = ttk.Label( 34 | timer_frame, 35 | textvariable=self.current_time 36 | ) 37 | timer_counter.grid() 38 | 39 | self.decrement_time() 40 | 41 | def decrement_time(self): 42 | current_time = self.current_time.get() 43 | 44 | if self.timer_running and current_time != "00:00": 45 | minutes, seconds = current_time.split(":") 46 | 47 | if int(seconds) > 0: 48 | seconds = int(seconds) - 1 49 | minutes = int(minutes) 50 | else: 51 | seconds = 59 52 | minutes = int(minutes) - 1 53 | 54 | self.current_time.set(f"{minutes:02d}:{seconds:02d}") 55 | self.after(1000, self.decrement_time) 56 | elif self.timer_running and current_time == "00:00": 57 | self.timer_schedule.rotate(-1) 58 | next_up = self.timer_schedule[0] 59 | 60 | if next_up == "Pomodoro": 61 | self.current_time.set("25:00") 62 | elif next_up == "Short Break": 63 | self.current_time.set("05:00") 64 | elif next_up == "Long Break": 65 | self.current_time.set("15:00") 66 | 67 | self.after(1000, self.decrement_time) 68 | 69 | 70 | app = PomodoroTimer() 71 | app.mainloop() -------------------------------------------------------------------------------- /section08/lectures/04_showing_current_timer/timer.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from collections import deque 4 | 5 | class PomodoroTimer(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.title("Pomodoro Timer") 10 | self.columnconfigure(0, weight=1) 11 | self.rowconfigure(1, weight=1) 12 | 13 | container = ttk.Frame(self) 14 | container.grid() 15 | container.columnconfigure(0, weight=1) 16 | 17 | timer_frame = Timer(container) 18 | timer_frame.grid(row=0, column=0, sticky="NESW") 19 | 20 | 21 | class Timer(ttk.Frame): 22 | def __init__(self, parent): 23 | super().__init__(parent) 24 | 25 | self.current_time = tk.StringVar(value="00:10") 26 | self.timer_order = ["Pomodoro", "Short Break", "Pomodoro", "Short Break", "Pomodoro", "Long Break"] 27 | self.timer_schedule = deque(self.timer_order) 28 | self.current_timer_label = tk.StringVar(value=self.timer_schedule[0]) 29 | self.timer_running = True 30 | 31 | timer_description = ttk.Label( 32 | self, 33 | textvariable=self.current_timer_label 34 | ) 35 | 36 | timer_description.grid(row=0, column=0, sticky="W", padx=(10, 0), pady=(10, 0)) 37 | 38 | timer_frame = ttk.Frame(self, height="100") 39 | timer_frame.grid(pady=(10, 0), sticky="NSEW") 40 | 41 | timer_counter = ttk.Label( 42 | timer_frame, 43 | textvariable=self.current_time 44 | ) 45 | timer_counter.place(relx=0.5, rely=0.5, anchor="center") 46 | 47 | self.decrement_time() 48 | 49 | def decrement_time(self): 50 | current_time = self.current_time.get() 51 | 52 | if self.timer_running and current_time != "00:00": 53 | minutes, seconds = current_time.split(":") 54 | 55 | if int(seconds) > 0: 56 | seconds = int(seconds) - 1 57 | minutes = int(minutes) 58 | else: 59 | seconds = 59 60 | minutes = int(minutes) - 1 61 | 62 | self.current_time.set(f"{minutes:02d}:{seconds:02d}") 63 | self.after(1000, self.decrement_time) 64 | elif self.timer_running and current_time == "00:00": 65 | self.timer_schedule.rotate(-1) 66 | next_up = self.timer_schedule[0] 67 | self.current_timer_label.set(next_up) 68 | 69 | if next_up == "Pomodoro": 70 | self.current_time.set("25:00") 71 | elif next_up == "Short Break": 72 | self.current_time.set("05:00") 73 | elif next_up == "Long Break": 74 | self.current_time.set("15:00") 75 | 76 | self.after(1000, self.decrement_time) 77 | 78 | 79 | app = PomodoroTimer() 80 | app.mainloop() -------------------------------------------------------------------------------- /section08/lectures/05_starting_and_stopping/timer.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from collections import deque 4 | 5 | class PomodoroTimer(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.title("Pomodoro Timer") 10 | self.columnconfigure(0, weight=1) 11 | self.rowconfigure(1, weight=1) 12 | 13 | container = ttk.Frame(self) 14 | container.grid() 15 | container.columnconfigure(0, weight=1) 16 | 17 | timer_frame = Timer(container) 18 | timer_frame.grid(row=0, column=0, sticky="NESW") 19 | 20 | 21 | class Timer(ttk.Frame): 22 | def __init__(self, parent): 23 | super().__init__(parent) 24 | self.columnconfigure(0, weight=1) 25 | self.columnconfigure(1, weight=0) 26 | 27 | self.current_time = tk.StringVar(value="00:10") 28 | self.timer_order = ["Pomodoro", "Short Break", "Pomodoro", "Short Break", "Pomodoro", "Long Break"] 29 | self.timer_schedule = deque(self.timer_order) 30 | self.current_timer_label = tk.StringVar(value=self.timer_schedule[0]) 31 | self.timer_running = False 32 | self._timer_decrement_job = None 33 | 34 | timer_description = ttk.Label( 35 | self, 36 | textvariable=self.current_timer_label 37 | ) 38 | 39 | timer_description.grid(row=0, column=0, sticky="W", padx=(10, 0), pady=(10, 0)) 40 | 41 | timer_frame = ttk.Frame(self, height="100") 42 | timer_frame.grid(pady=(10, 0), sticky="NSEW") 43 | 44 | timer_counter = ttk.Label( 45 | timer_frame, 46 | textvariable=self.current_time 47 | ) 48 | timer_counter.place(relx=0.5, rely=0.5, anchor="center") 49 | 50 | button_container = ttk.Frame(self, padding=10) 51 | button_container.grid(row=2, column=0, sticky="EW") 52 | button_container.columnconfigure((0, 1), weight=1) 53 | 54 | self.start_button = ttk.Button( 55 | button_container, 56 | text="Start", 57 | command=self.start_timer, 58 | cursor="hand2" 59 | ) 60 | 61 | self.start_button.grid(row=0, column=0, sticky="EW") 62 | 63 | self.stop_button = ttk.Button( 64 | button_container, 65 | text="Stop", 66 | state="disabled", 67 | command=self.stop_timer, 68 | cursor="hand2" 69 | ) 70 | 71 | self.stop_button.grid(row=0, column=1, sticky="EW", padx=5) 72 | 73 | def start_timer(self): 74 | self.timer_running = True 75 | self.start_button["state"] = "disabled" 76 | self.stop_button["state"] = "enabled" 77 | self.decrement_time() 78 | 79 | def stop_timer(self): 80 | self.timer_running = False 81 | self.start_button["state"] = "enabled" 82 | self.stop_button["state"] = "disabled" 83 | 84 | if self._timer_decrement_job: 85 | self.after_cancel(self._timer_decrement_job) 86 | self._timer_decrement_job = None 87 | 88 | def decrement_time(self): 89 | current_time = self.current_time.get() 90 | 91 | if self.timer_running and current_time != "00:00": 92 | minutes, seconds = current_time.split(":") 93 | 94 | if int(seconds) > 0: 95 | seconds = int(seconds) - 1 96 | minutes = int(minutes) 97 | else: 98 | seconds = 59 99 | minutes = int(minutes) - 1 100 | 101 | self.current_time.set(f"{minutes:02d}:{seconds:02d}") 102 | self._timer_decrement_job = self.after(1000, self.decrement_time) 103 | elif self.timer_running and current_time == "00:00": 104 | self.timer_schedule.rotate(-1) 105 | next_up = self.timer_schedule[0] 106 | self.current_timer_label.set(next_up) 107 | 108 | if next_up == "Pomodoro": 109 | self.current_time.set("25:00") 110 | elif next_up == "Short Break": 111 | self.current_time.set("05:00") 112 | elif next_up == "Long Break": 113 | self.current_time.set("15:00") 114 | 115 | self._timer_decrement_job = self.after(1000, self.decrement_time) 116 | 117 | 118 | app = PomodoroTimer() 119 | app.mainloop() -------------------------------------------------------------------------------- /section08/lectures/08_splitting_into_files/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from collections import deque 4 | from frames import Timer 5 | 6 | 7 | class PomodoroTimer(tk.Tk): 8 | def __init__(self, *args, **kwargs): 9 | super().__init__(*args, **kwargs) 10 | 11 | self.title("Pomodoro Timer") 12 | self.columnconfigure(0, weight=1) 13 | self.rowconfigure(1, weight=1) 14 | 15 | self.pomodoro = tk.StringVar(value=25) 16 | self.long_break = tk.StringVar(value=15) 17 | self.short_break = tk.StringVar(value=5) 18 | self.timer_order = ["Pomodoro", "Short Break", "Pomodoro", "Short Break", "Pomodoro", "Long Break"] 19 | self.timer_schedule = deque(self.timer_order) 20 | 21 | container = ttk.Frame(self) 22 | container.grid() 23 | container.columnconfigure(0, weight=1) 24 | 25 | timer_frame = Timer(container, self) 26 | timer_frame.grid(row=0, column=0, sticky="NESW") 27 | 28 | 29 | app = PomodoroTimer() 30 | app.mainloop() -------------------------------------------------------------------------------- /section08/lectures/08_splitting_into_files/frames/__init__.py: -------------------------------------------------------------------------------- 1 | from frames.timer import Timer 2 | -------------------------------------------------------------------------------- /section08/lectures/08_splitting_into_files/frames/timer.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from collections import deque 4 | 5 | 6 | class Timer(ttk.Frame): 7 | def __init__(self, parent, controller): 8 | super().__init__(parent) 9 | 10 | self.controller = controller 11 | self.columnconfigure(0, weight=1) 12 | self.columnconfigure(1, weight=0) 13 | 14 | self.current_timer_label = tk.StringVar(value=self.controller.timer_schedule[0]) 15 | self.current_time = tk.StringVar(value=f"{controller.pomodoro.get()}:00") 16 | self.timer_running = False 17 | self._timer_decrement_job = None 18 | 19 | timer_description = ttk.Label( 20 | self, 21 | textvariable=self.current_timer_label 22 | ) 23 | 24 | timer_description.grid(row=0, column=0, sticky="W", padx=(10, 0), pady=(10, 0)) 25 | 26 | timer_frame = ttk.Frame(self, height="100") 27 | timer_frame.grid(pady=(10, 0), sticky="NSEW") 28 | 29 | timer_counter = ttk.Label( 30 | timer_frame, 31 | textvariable=self.current_time 32 | ) 33 | timer_counter.place(relx=0.5, rely=0.5, anchor="center") 34 | 35 | button_container = ttk.Frame(self, padding=10) 36 | button_container.grid(row=2, column=0, columnspan=2, sticky="EW") 37 | button_container.columnconfigure((0, 1, 2), weight=1) 38 | 39 | self.start_button = ttk.Button( 40 | button_container, 41 | text="Start", 42 | command=self.start_timer, 43 | cursor="hand2" 44 | ) 45 | 46 | self.start_button.grid(row=0, column=0, sticky="EW") 47 | 48 | self.stop_button = ttk.Button( 49 | button_container, 50 | text="Stop", 51 | state="disabled", 52 | command=self.stop_timer, 53 | cursor="hand2" 54 | ) 55 | 56 | self.stop_button.grid(row=0, column=1, sticky="EW", padx=5) 57 | 58 | reset_button = ttk.Button( 59 | button_container, 60 | text="Reset", 61 | command=self.reset_timer, 62 | cursor="hand2" 63 | ) 64 | 65 | reset_button.grid(row=0, column=2, sticky="EW") 66 | 67 | def start_timer(self): 68 | self.timer_running = True 69 | self.start_button["state"] = "disabled" 70 | self.stop_button["state"] = "enabled" 71 | self.decrement_time() 72 | 73 | def stop_timer(self): 74 | self.timer_running = False 75 | self.start_button["state"] = "enabled" 76 | self.stop_button["state"] = "disabled" 77 | 78 | if self._timer_decrement_job: 79 | self.after_cancel(self._timer_decrement_job) 80 | self._timer_decrement_job = None 81 | 82 | def reset_timer(self): 83 | self.stop_timer() 84 | pomodoro_time = self.controller.pomodoro.get() 85 | current_time = self.current_time.set(f"{int(pomodoro_time):02d}:00") 86 | self.controller.timer_schedule = deque(self.controller.timer_order) 87 | self.current_timer_label.set(self.controller.timer_schedule[0]) 88 | 89 | def decrement_time(self): 90 | current_time = self.current_time.get() 91 | 92 | if self.timer_running and current_time != "00:00": 93 | minutes, seconds = current_time.split(":") 94 | 95 | if int(seconds) > 0: 96 | seconds = int(seconds) - 1 97 | minutes = int(minutes) 98 | else: 99 | seconds = 59 100 | minutes = int(minutes) - 1 101 | 102 | self.current_time.set(f"{minutes:02d}:{seconds:02d}") 103 | self._timer_decrement_job = self.after(1000, self.decrement_time) 104 | elif self.timer_running and current_time == "00:00": 105 | self.controller.timer_schedule.rotate(-1) 106 | next_up = self.controller.timer_schedule[0] 107 | self.current_timer_label.set(next_up) 108 | 109 | if next_up == "Pomodoro": 110 | pomodoro_time = int(self.controller.pomodoro.get()) 111 | self.current_time.set(f"{pomodoro_time:02d}:00") 112 | elif next_up == "Short Break": 113 | short_break_time = int(self.controller.short_break.get()) 114 | self.current_time.set(f"{short_break_time:02d}:00") 115 | elif next_up == "Long Break": 116 | long_break_time = int(self.controller.long_break.get()) 117 | self.current_time.set(f"{long_break_time:02d}:00") 118 | 119 | self._timer_decrement_job = self.after(1000, self.decrement_time) 120 | -------------------------------------------------------------------------------- /section08/lectures/09_the_settings_frame/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from collections import deque 4 | from frames import Timer, Settings 5 | 6 | 7 | class PomodoroTimer(tk.Tk): 8 | def __init__(self, *args, **kwargs): 9 | super().__init__(*args, **kwargs) 10 | 11 | self.title("Pomodoro Timer") 12 | self.columnconfigure(0, weight=1) 13 | self.rowconfigure(1, weight=1) 14 | 15 | self.pomodoro = tk.StringVar(value=25) 16 | self.long_break = tk.StringVar(value=15) 17 | self.short_break = tk.StringVar(value=5) 18 | self.timer_order = ["Pomodoro", "Short Break", "Pomodoro", "Short Break", "Pomodoro", "Long Break"] 19 | self.timer_schedule = deque(self.timer_order) 20 | 21 | container = ttk.Frame(self) 22 | container.grid() 23 | container.columnconfigure(0, weight=1) 24 | 25 | settings_frame = Settings(container, self) 26 | # timer_frame = Timer(container, self) 27 | settings_frame.grid(row=0, column=0, sticky="NESW") 28 | # timer_frame.grid(row=0, column=0, sticky="NESW") 29 | 30 | app = PomodoroTimer() 31 | app.mainloop() -------------------------------------------------------------------------------- /section08/lectures/09_the_settings_frame/frames/__init__.py: -------------------------------------------------------------------------------- 1 | from frames.timer import Timer 2 | from frames.settings import Settings 3 | -------------------------------------------------------------------------------- /section08/lectures/09_the_settings_frame/frames/settings.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class Settings(ttk.Frame): 6 | def __init__(self, parent, controller): 7 | super().__init__(parent) 8 | 9 | self.columnconfigure(0, weight=1) 10 | self.rowconfigure(2, weight=1) 11 | 12 | settings_container = ttk.Frame( 13 | self, 14 | padding="30 15 30 15", 15 | ) 16 | 17 | settings_container.grid(row=0, column=0, sticky="EW", padx=10, pady=10) 18 | 19 | settings_container.columnconfigure(0, weight=1) 20 | settings_container.rowconfigure(1, weight=1) 21 | 22 | pomodoro_label = ttk.Label( 23 | settings_container, 24 | text="Pomodoro: " 25 | ) 26 | pomodoro_label.grid(column=0, row=0, sticky="W") 27 | 28 | pomodoro_input = tk.Spinbox( 29 | settings_container, 30 | from_=0, 31 | to=120, 32 | increment=1, 33 | justify="center", 34 | textvariable=controller.pomodoro, 35 | width=10, 36 | ) 37 | pomodoro_input.grid(column=1, row=0, sticky="EW") 38 | pomodoro_input.focus() 39 | 40 | long_break_label = ttk.Label( 41 | settings_container, 42 | text="Long break time: " 43 | ) 44 | long_break_label.grid(column=0, row=1, sticky="W") 45 | 46 | long_break_input = tk.Spinbox( 47 | settings_container, 48 | from_=0, 49 | to=60, 50 | increment=1, 51 | justify="center", 52 | textvariable=controller.long_break, 53 | width=10, 54 | ) 55 | long_break_input.grid(column=1, row=1, sticky="EW") 56 | 57 | short_break_label = ttk.Label( 58 | settings_container, 59 | text="Short break time: " 60 | ) 61 | short_break_label.grid(column=0, row=2, sticky="W") 62 | 63 | short_break_input = tk.Spinbox( 64 | settings_container, 65 | from_=0, 66 | to=30, 67 | increment=1, 68 | justify="center", 69 | textvariable=controller.short_break, 70 | width=10, 71 | ) 72 | short_break_input.grid(column=1, row=2, sticky="EW") 73 | 74 | for child in settings_container.winfo_children(): 75 | child.grid_configure(padx=5, pady=5) 76 | -------------------------------------------------------------------------------- /section08/lectures/09_the_settings_frame/frames/timer.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from collections import deque 4 | 5 | 6 | class Timer(ttk.Frame): 7 | def __init__(self, parent, controller): 8 | super().__init__(parent) 9 | 10 | self.controller = controller 11 | self.columnconfigure(0, weight=1) 12 | self.columnconfigure(1, weight=0) 13 | 14 | self.current_timer_label = tk.StringVar(value=self.controller.timer_schedule[0]) 15 | self.current_time = tk.StringVar(value=f"{controller.pomodoro.get()}:00") 16 | self.timer_running = False 17 | self._timer_decrement_job = None 18 | 19 | timer_description = ttk.Label( 20 | self, 21 | textvariable=self.current_timer_label 22 | ) 23 | 24 | timer_description.grid(row=0, column=0, sticky="W", padx=(10, 0), pady=(10, 0)) 25 | 26 | timer_frame = ttk.Frame(self, height="100") 27 | timer_frame.grid(pady=(10, 0), sticky="NSEW") 28 | 29 | timer_counter = ttk.Label( 30 | timer_frame, 31 | textvariable=self.current_time 32 | ) 33 | timer_counter.place(relx=0.5, rely=0.5, anchor="center") 34 | 35 | button_container = ttk.Frame(self, padding=10) 36 | button_container.grid(row=2, column=0, columnspan=2, sticky="EW") 37 | button_container.columnconfigure((0, 1, 2), weight=1) 38 | 39 | self.start_button = ttk.Button( 40 | button_container, 41 | text="Start", 42 | command=self.start_timer, 43 | cursor="hand2" 44 | ) 45 | 46 | self.start_button.grid(row=0, column=0, sticky="EW") 47 | 48 | self.stop_button = ttk.Button( 49 | button_container, 50 | text="Stop", 51 | state="disabled", 52 | command=self.stop_timer, 53 | cursor="hand2" 54 | ) 55 | 56 | self.stop_button.grid(row=0, column=1, sticky="EW", padx=5) 57 | 58 | reset_button = ttk.Button( 59 | button_container, 60 | text="Reset", 61 | command=self.reset_timer, 62 | cursor="hand2" 63 | ) 64 | 65 | reset_button.grid(row=0, column=2, sticky="EW") 66 | 67 | def start_timer(self): 68 | self.timer_running = True 69 | self.start_button["state"] = "disabled" 70 | self.stop_button["state"] = "enabled" 71 | self.decrement_time() 72 | 73 | def stop_timer(self): 74 | self.timer_running = False 75 | self.start_button["state"] = "enabled" 76 | self.stop_button["state"] = "disabled" 77 | 78 | if self._timer_decrement_job: 79 | self.after_cancel(self._timer_decrement_job) 80 | self._timer_decrement_job = None 81 | 82 | def reset_timer(self): 83 | self.stop_timer() 84 | pomodoro_time = self.controller.pomodoro.get() 85 | current_time = self.current_time.set(f"{int(pomodoro_time):02d}:00") 86 | self.controller.timer_schedule = deque(self.controller.timer_order) 87 | self.current_timer_label.set(self.controller.timer_schedule[0]) 88 | 89 | def decrement_time(self): 90 | current_time = self.current_time.get() 91 | 92 | if self.timer_running and current_time != "00:00": 93 | minutes, seconds = current_time.split(":") 94 | 95 | if int(seconds) > 0: 96 | seconds = int(seconds) - 1 97 | minutes = int(minutes) 98 | else: 99 | seconds = 59 100 | minutes = int(minutes) - 1 101 | 102 | self.current_time.set(f"{minutes:02d}:{seconds:02d}") 103 | self._timer_decrement_job = self.after(1000, self.decrement_time) 104 | elif self.timer_running and current_time == "00:00": 105 | self.controller.timer_schedule.rotate(-1) 106 | next_up = self.controller.timer_schedule[0] 107 | self.current_timer_label.set(next_up) 108 | 109 | if next_up == "Pomodoro": 110 | pomodoro_time = int(self.controller.pomodoro.get()) 111 | self.current_time.set(f"{pomodoro_time:02d}:00") 112 | elif next_up == "Short Break": 113 | short_break_time = int(self.controller.short_break.get()) 114 | self.current_time.set(f"{short_break_time:02d}:00") 115 | elif next_up == "Long Break": 116 | long_break_time = int(self.controller.long_break.get()) 117 | self.current_time.set(f"{long_break_time:02d}:00") 118 | 119 | self._timer_decrement_job = self.after(1000, self.decrement_time) 120 | -------------------------------------------------------------------------------- /section08/lectures/10_switching_between_frames/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from collections import deque 4 | from frames import Timer, Settings 5 | 6 | 7 | class PomodoroTimer(tk.Tk): 8 | def __init__(self, *args, **kwargs): 9 | super().__init__(*args, **kwargs) 10 | 11 | self.title("Pomodoro Timer") 12 | self.columnconfigure(0, weight=1) 13 | self.rowconfigure(1, weight=1) 14 | 15 | self.pomodoro = tk.StringVar(value=25) 16 | self.long_break = tk.StringVar(value=15) 17 | self.short_break = tk.StringVar(value=5) 18 | self.timer_order = ["Pomodoro", "Short Break", "Pomodoro", "Short Break", "Pomodoro", "Long Break"] 19 | self.timer_schedule = deque(self.timer_order) 20 | 21 | container = ttk.Frame(self) 22 | container.grid() 23 | container.columnconfigure(0, weight=1) 24 | 25 | self.frames = {} 26 | 27 | settings_frame = Settings(container, self, lambda: self.show_frame(Timer)) 28 | timer_frame = Timer(container, self, lambda: self.show_frame(Settings)) 29 | settings_frame.grid(row=0, column=0, sticky="NESW") 30 | timer_frame.grid(row=0, column=0, sticky="NESW") 31 | 32 | self.frames[Settings] = settings_frame 33 | self.frames[Timer] = timer_frame 34 | 35 | self.show_frame(Timer) 36 | 37 | def show_frame(self, container): 38 | frame = self.frames[container] 39 | frame.tkraise() 40 | 41 | app = PomodoroTimer() 42 | app.mainloop() -------------------------------------------------------------------------------- /section08/lectures/10_switching_between_frames/frames/__init__.py: -------------------------------------------------------------------------------- 1 | from frames.timer import Timer 2 | from frames.settings import Settings 3 | -------------------------------------------------------------------------------- /section08/lectures/10_switching_between_frames/frames/settings.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class Settings(ttk.Frame): 6 | def __init__(self, parent, controller, show_timer): 7 | super().__init__(parent) 8 | 9 | self.columnconfigure(0, weight=1) 10 | self.rowconfigure(2, weight=1) 11 | 12 | settings_container = ttk.Frame( 13 | self, 14 | padding="30 15 30 15" 15 | ) 16 | 17 | settings_container.grid(row=0, column=0, sticky="EW", padx=10, pady=10) 18 | 19 | settings_container.columnconfigure(0, weight=1) 20 | settings_container.rowconfigure(1, weight=1) 21 | 22 | pomodoro_label = ttk.Label( 23 | settings_container, 24 | text="Pomodoro: " 25 | ) 26 | pomodoro_label.grid(column=0, row=0, sticky="W") 27 | 28 | pomodoro_input = tk.Spinbox( 29 | settings_container, 30 | from_=0, 31 | to=120, 32 | increment=1, 33 | justify="center", 34 | textvariable=controller.pomodoro, 35 | width=10 36 | ) 37 | pomodoro_input.grid(column=1, row=0, sticky="EW") 38 | pomodoro_input.focus() 39 | 40 | long_break_label = ttk.Label( 41 | settings_container, 42 | text="Long break time: " 43 | ) 44 | long_break_label.grid(column=0, row=1, sticky="W") 45 | 46 | long_break_input = tk.Spinbox( 47 | settings_container, 48 | from_=0, 49 | to=60, 50 | increment=1, 51 | justify="center", 52 | textvariable=controller.long_break, 53 | width=10 54 | ) 55 | long_break_input.grid(column=1, row=1, sticky="EW") 56 | 57 | short_break_label = ttk.Label( 58 | settings_container, 59 | text="Short break time: " 60 | ) 61 | short_break_label.grid(column=0, row=2, sticky="W") 62 | 63 | short_break_input = tk.Spinbox( 64 | settings_container, 65 | from_=0, 66 | to=30, 67 | increment=1, 68 | justify="center", 69 | textvariable=controller.short_break, 70 | width=10 71 | ) 72 | short_break_input.grid(column=1, row=2, sticky="EW") 73 | 74 | for child in settings_container.winfo_children(): 75 | child.grid_configure(padx=5, pady=5) 76 | 77 | button_container = ttk.Frame(self) 78 | button_container.grid(sticky="EW", padx=10) 79 | button_container.columnconfigure(0, weight=1) 80 | 81 | timer_button = ttk.Button( 82 | button_container, 83 | text="← Back", 84 | command=show_timer, 85 | cursor="hand2" # hand1 in some systems 86 | ) 87 | 88 | timer_button.grid(column=0, row=0, sticky="EW", padx=2) 89 | -------------------------------------------------------------------------------- /section08/lectures/11_styling_our_app/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from collections import deque 4 | from frames import Timer, Settings 5 | 6 | 7 | COLOUR_PRIMARY = "#2e3f4f" 8 | COLOUR_SECONDARY = "#293846" 9 | COLOUR_LIGHT_BACKGROUND = "#fff" 10 | COLOUR_LIGHT_TEXT = "#eee" 11 | COLOUR_DARK_TEXT = "#8095a8" 12 | 13 | 14 | class PomodoroTimer(tk.Tk): 15 | def __init__(self, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | 18 | style = ttk.Style() 19 | style.theme_use("clam") 20 | 21 | style.configure("Timer.TFrame", background=COLOUR_LIGHT_BACKGROUND) 22 | style.configure("Background.TFrame", background=COLOUR_PRIMARY) 23 | style.configure( 24 | "TimerText.TLabel", 25 | background=COLOUR_LIGHT_BACKGROUND, 26 | foreground=COLOUR_DARK_TEXT, 27 | font="Courier 38" 28 | ) 29 | 30 | style.configure( 31 | "LightText.TLabel", 32 | background=COLOUR_PRIMARY, 33 | foreground=COLOUR_LIGHT_TEXT, 34 | ) 35 | 36 | style.configure( 37 | "PomodoroButton.TButton", 38 | background=COLOUR_SECONDARY, 39 | foreground=COLOUR_LIGHT_TEXT, 40 | ) 41 | 42 | style.map( 43 | "PomodoroButton.TButton", 44 | background=[("active", COLOUR_PRIMARY), ("disabled", COLOUR_LIGHT_TEXT)] 45 | ) 46 | 47 | # Main app window is a tk widget, so background is set directly 48 | self["background"] = COLOUR_PRIMARY 49 | 50 | self.title("Pomodoro Timer") 51 | self.columnconfigure(0, weight=1) 52 | self.rowconfigure(1, weight=1) 53 | 54 | self.pomodoro = tk.StringVar(value=25) 55 | self.long_break = tk.StringVar(value=15) 56 | self.short_break = tk.StringVar(value=5) 57 | self.timer_order = ["Pomodoro", "Short Break", "Pomodoro", "Short Break", "Pomodoro", "Long Break"] 58 | self.timer_schedule = deque(self.timer_order) 59 | 60 | container = ttk.Frame(self) 61 | container.grid() 62 | container.columnconfigure(0, weight=1) 63 | 64 | self.frames = {} 65 | 66 | settings_frame = Settings(container, self, lambda: self.show_frame(Timer)) 67 | timer_frame = Timer(container, self, lambda: self.show_frame(Settings)) 68 | settings_frame.grid(row=0, column=0, sticky="NESW") 69 | timer_frame.grid(row=0, column=0, sticky="NESW") 70 | 71 | self.frames[Settings] = settings_frame 72 | self.frames[Timer] = timer_frame 73 | 74 | self.show_frame(Timer) 75 | 76 | def show_frame(self, container): 77 | frame = self.frames[container] 78 | frame.tkraise() 79 | 80 | app = PomodoroTimer() 81 | app.mainloop() -------------------------------------------------------------------------------- /section08/lectures/11_styling_our_app/frames/__init__.py: -------------------------------------------------------------------------------- 1 | from frames.timer import Timer 2 | from frames.settings import Settings 3 | -------------------------------------------------------------------------------- /section08/lectures/11_styling_our_app/frames/settings.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class Settings(ttk.Frame): 6 | def __init__(self, parent, controller, show_timer): 7 | super().__init__(parent) 8 | 9 | self["style"] = "Background.TFrame" 10 | 11 | self.columnconfigure(0, weight=1) 12 | self.rowconfigure(2, weight=1) 13 | 14 | settings_container = ttk.Frame( 15 | self, 16 | padding="30 15 30 15", 17 | style="Background.TFrame" 18 | ) 19 | 20 | settings_container.grid(row=0, column=0, sticky="EW", padx=10, pady=10) 21 | 22 | settings_container.columnconfigure(0, weight=1) 23 | settings_container.rowconfigure(1, weight=1) 24 | 25 | pomodoro_label = ttk.Label( 26 | settings_container, 27 | text="Pomodoro: ", 28 | style="LightText.TLabel" 29 | ) 30 | pomodoro_label.grid(column=0, row=0, sticky="W") 31 | 32 | pomodoro_input = tk.Spinbox( 33 | settings_container, 34 | from_=0, 35 | to=120, 36 | increment=1, 37 | justify="center", 38 | textvariable=controller.pomodoro, 39 | width=10, 40 | ) 41 | pomodoro_input.grid(column=1, row=0, sticky="EW") 42 | pomodoro_input.focus() 43 | 44 | long_break_label = ttk.Label( 45 | settings_container, 46 | text="Long break time: ", 47 | style="LightText.TLabel" 48 | ) 49 | long_break_label.grid(column=0, row=1, sticky="W") 50 | 51 | long_break_input = tk.Spinbox( 52 | settings_container, 53 | from_=0, 54 | to=60, 55 | increment=1, 56 | justify="center", 57 | textvariable=controller.long_break, 58 | width=10, 59 | ) 60 | long_break_input.grid(column=1, row=1, sticky="EW") 61 | 62 | short_break_label = ttk.Label( 63 | settings_container, 64 | text="Short break time: ", 65 | style="LightText.TLabel" 66 | ) 67 | short_break_label.grid(column=0, row=2, sticky="W") 68 | 69 | short_break_input = tk.Spinbox( 70 | settings_container, 71 | from_=0, 72 | to=30, 73 | increment=1, 74 | justify="center", 75 | textvariable=controller.short_break, 76 | width=10, 77 | ) 78 | short_break_input.grid(column=1, row=2, sticky="EW") 79 | 80 | for child in settings_container.winfo_children(): 81 | child.grid_configure(padx=5, pady=5) 82 | 83 | button_container = ttk.Frame(self, style="Background.TFrame") 84 | button_container.grid(sticky="EW", padx=10) 85 | button_container.columnconfigure(0, weight=1) 86 | 87 | timer_button = ttk.Button( 88 | button_container, 89 | text="← Back", 90 | command=show_timer, 91 | style="PomodoroButton.TButton", 92 | cursor="hand2" # hand1 in some systems 93 | ) 94 | 95 | timer_button.grid(column=0, row=0, sticky="EW", padx=2) 96 | -------------------------------------------------------------------------------- /section08/pomodoro_timer/app.py: -------------------------------------------------------------------------------- 1 | from tkinter import ttk 2 | import tkinter as tk 3 | from collections import deque 4 | from frames import Settings, Timer 5 | from windows import set_dpi_awareness 6 | 7 | set_dpi_awareness() 8 | 9 | COLOUR_PRIMARY = "#2e3f4f" 10 | COLOUR_SECONDARY = "#293846" 11 | COLOUR_LIGHT_BACKGROUND = "#fff" 12 | COLOUR_LIGHT_TEXT = "#eee" 13 | COLOUR_DARK_TEXT = "#8095a8" 14 | 15 | 16 | class PomodoroTimer(tk.Tk): 17 | def __init__(self, *args, **kwargs): 18 | super().__init__(*args, **kwargs) 19 | 20 | style = ttk.Style() 21 | style.theme_use("clam") 22 | 23 | style.configure("Timer.TFrame", background=COLOUR_LIGHT_BACKGROUND) 24 | style.configure("Background.TFrame", background=COLOUR_PRIMARY) 25 | style.configure( 26 | "TimerText.TLabel", 27 | background=COLOUR_LIGHT_BACKGROUND, 28 | foreground=COLOUR_DARK_TEXT, 29 | font="Courier 46" 30 | ) 31 | 32 | style.configure( 33 | "LightText.TLabel", 34 | background=COLOUR_PRIMARY, 35 | foreground=COLOUR_LIGHT_TEXT, 36 | font=("TkDefaultFont", 11) 37 | ) 38 | 39 | style.configure( 40 | "PomodoroButton.TButton", 41 | background=[COLOUR_SECONDARY], 42 | foreground=COLOUR_LIGHT_TEXT, 43 | font=("TkDefaultFont", 11) 44 | ) 45 | 46 | style.map( 47 | "PomodoroButton.TButton", 48 | background=[("active", COLOUR_PRIMARY), ("disabled", COLOUR_LIGHT_TEXT)] 49 | ) 50 | 51 | # Main app window is a tk widget, so background is set directly 52 | self["background"] = COLOUR_PRIMARY 53 | 54 | self.title("Pomodoro Timer") 55 | self.columnconfigure(0, weight=1) 56 | self.rowconfigure(1, weight=1) 57 | 58 | self.pomodoro = tk.StringVar(value=25) 59 | self.long_break = tk.StringVar(value=15) 60 | self.short_break = tk.StringVar(value=5) 61 | self.timer_order = ["Pomodoro", "Short Break", "Pomodoro", "Short Break", "Pomodoro", "Long Break"] 62 | self.timer_schedule = deque(self.timer_order) 63 | 64 | container = ttk.Frame(self) 65 | container.grid() 66 | container.columnconfigure(0, weight=1) 67 | 68 | self.frames = {} 69 | 70 | settings_frame = Settings(container, self, lambda: self.show_frame(Timer)) 71 | timer_frame = Timer(container, self, lambda: self.show_frame(Settings)) 72 | settings_frame.grid(row=0, column=0, sticky="NESW") 73 | timer_frame.grid(row=0, column=0, sticky="NESW") 74 | 75 | self.frames[Settings] = settings_frame 76 | self.frames[Timer] = timer_frame 77 | 78 | self.show_frame(Timer) 79 | 80 | def show_frame(self, container): 81 | frame = self.frames[container] 82 | frame.tkraise() 83 | 84 | 85 | root = PomodoroTimer() 86 | root.mainloop() 87 | -------------------------------------------------------------------------------- /section08/pomodoro_timer/frames/__init__.py: -------------------------------------------------------------------------------- 1 | from frames.settings import Settings 2 | from frames.timer import Timer -------------------------------------------------------------------------------- /section08/pomodoro_timer/frames/settings.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class Settings(ttk.Frame): 6 | def __init__(self, parent, controller, show_timer): 7 | super().__init__(parent) 8 | 9 | self["style"] = "Background.TFrame" 10 | 11 | self.columnconfigure(0, weight=1) 12 | self.rowconfigure(2, weight=1) 13 | 14 | settings_container = ttk.Frame( 15 | self, 16 | padding="30 15 30 15", 17 | style="Background.TFrame" 18 | ) 19 | 20 | settings_container.grid(row=0, column=0, sticky="EW", padx=10, pady=10) 21 | 22 | settings_container.columnconfigure(0, weight=1) 23 | settings_container.rowconfigure(1, weight=1) 24 | 25 | pomodoro_label = ttk.Label( 26 | settings_container, 27 | text="Pomodoro: ", 28 | style="LightText.TLabel" 29 | ) 30 | pomodoro_label.grid(column=0, row=0, sticky="W") 31 | 32 | pomodoro_input = tk.Spinbox( 33 | settings_container, 34 | from_=0, 35 | to=120, 36 | increment=1, 37 | justify="center", 38 | textvariable=controller.pomodoro, 39 | width=10, 40 | ) 41 | pomodoro_input.grid(column=1, row=0, sticky="EW") 42 | pomodoro_input.focus() 43 | 44 | long_break_label = ttk.Label( 45 | settings_container, 46 | text="Long break time: ", 47 | style="LightText.TLabel" 48 | ) 49 | long_break_label.grid(column=0, row=1, sticky="W") 50 | 51 | long_break_input = tk.Spinbox( 52 | settings_container, 53 | from_=0, 54 | to=60, 55 | increment=1, 56 | justify="center", 57 | textvariable=controller.long_break, 58 | width=10, 59 | ) 60 | long_break_input.grid(column=1, row=1, sticky="EW") 61 | 62 | short_break_label = ttk.Label( 63 | settings_container, 64 | text="Short break time: ", 65 | style="LightText.TLabel" 66 | ) 67 | short_break_label.grid(column=0, row=2, sticky="W") 68 | 69 | short_break_input = tk.Spinbox( 70 | settings_container, 71 | from_=0, 72 | to=30, 73 | increment=1, 74 | justify="center", 75 | textvariable=controller.short_break, 76 | width=10, 77 | ) 78 | short_break_input.grid(column=1, row=2, sticky="EW") 79 | 80 | for child in settings_container.winfo_children(): 81 | child.grid_configure(padx=5, pady=5) 82 | 83 | button_container = ttk.Frame(self, style="Background.TFrame") 84 | button_container.grid(sticky="EW", padx=10) 85 | button_container.columnconfigure(0, weight=1) 86 | 87 | timer_button = ttk.Button( 88 | button_container, 89 | text="← Back", 90 | command=show_timer, 91 | style="PomodoroButton.TButton", 92 | cursor="hand2" # hand1 in some systems 93 | ) 94 | 95 | timer_button.grid(column=0, row=0, sticky="EW", padx=2) 96 | -------------------------------------------------------------------------------- /section08/pomodoro_timer/windows.py: -------------------------------------------------------------------------------- 1 | def set_dpi_awareness(): 2 | try: 3 | from ctypes import windll 4 | windll.shcore.SetProcessDpiAwareness(1) 5 | except: 6 | pass -------------------------------------------------------------------------------- /section09/lectures/(unused) PTK_09_08_responsive_breakpoints/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | black = "*" 11 | pillow = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /section09/lectures/(unused) PTK_09_08_responsive_breakpoints/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from frames.chat import Chat 3 | 4 | 5 | class Messenger(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.geometry("1200x500") 10 | self.minsize(800, 500) 11 | self.columnconfigure(0, weight=1) 12 | self.rowconfigure(0, weight=1) 13 | 14 | self.chat_frame = Chat(self) 15 | 16 | self.chat_frame.grid(row=0, column=0, sticky="NSEW") 17 | 18 | 19 | 20 | root = Messenger() 21 | root.mainloop() 22 | -------------------------------------------------------------------------------- /section09/lectures/(unused) PTK_09_08_responsive_breakpoints/frames/chat.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import requests 4 | from frames.message_window import MessageWindow 5 | 6 | messages = [{"message": "Hello, world", "date": 15498487}] 7 | message_labels = [] 8 | 9 | 10 | class Chat(ttk.Frame): 11 | def __init__(self, container, *args, **kwargs): 12 | super().__init__(container, *args, **kwargs) 13 | 14 | self.columnconfigure(0, weight=1) 15 | self.rowconfigure(0, weight=1) 16 | 17 | self.message_window = MessageWindow(self) 18 | self.message_window.grid(row=0, column=0, sticky="NSEW", pady=5) 19 | 20 | input_frame = ttk.Frame(self, padding=10) 21 | input_frame.grid(row=1, column=0, sticky="EW") 22 | 23 | message_fetch = ttk.Button( 24 | input_frame, 25 | text="Fetch", 26 | command=self.get_messages 27 | ) 28 | message_fetch.pack() 29 | self.message_window.update_message_widgets(messages, message_labels) 30 | 31 | def get_messages(self): 32 | global messages 33 | messages = requests.get("http://167.99.63.70/messages").json() 34 | self.message_window.update_message_widgets(messages, message_labels) 35 | self.after(150, lambda: self.message_window.yview_moveto(1.0)) 36 | -------------------------------------------------------------------------------- /section09/lectures/(unused) PTK_09_08_responsive_breakpoints/frames/message_window.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import tkinter as tk 3 | from tkinter import ttk 4 | from PIL import Image, ImageTk 5 | 6 | SCREEN_SIZE_TO_MESSAGE_WIDTH = { 7 | 1100: 900, 8 | 950: 700, 9 | 750: 550 10 | } 11 | 12 | 13 | class MessageWindow(tk.Canvas): 14 | def __init__(self, container, *args, **kwargs): 15 | super().__init__(container, *args, **kwargs, highlightthickness=0) 16 | 17 | self.messages_frame = ttk.Frame(container) 18 | self.messages_frame.columnconfigure(0, weight=1) 19 | 20 | self.scrollable_window = self.create_window((0, 0), window=self.messages_frame, anchor="nw") 21 | 22 | def configure_scroll_region(event): 23 | self.configure(scrollregion=self.bbox("all")) 24 | 25 | def configure_window_size(event): 26 | self.itemconfig(self.scrollable_window, width=self.winfo_width()) 27 | 28 | self.bind("", configure_window_size) 29 | self.messages_frame.bind("", configure_scroll_region) 30 | self.bind_all("", self._on_mousewheel) 31 | 32 | scrollbar = ttk.Scrollbar(container, orient="vertical", command=self.yview) 33 | scrollbar.grid(row=0, column=1, sticky="NS") 34 | 35 | self.configure(yscrollcommand=scrollbar.set) 36 | self.yview_moveto(1.0) 37 | 38 | # https://stackoverflow.com/a/17457843/1587271 39 | def _on_mousewheel(self, event): 40 | self.yview_scroll(-int(event.delta/120), "units") 41 | 42 | def update_message_widgets(self, messages, message_labels): 43 | existing_labels = [ 44 | (message["text"], time["text"]) for message, time in message_labels 45 | ] 46 | 47 | for message in messages: 48 | message_time = datetime.datetime.fromtimestamp(message["date"]).strftime( 49 | "%d-%m-%Y %H:%M:%S" 50 | ) 51 | 52 | if (message["message"], message_time) not in existing_labels: 53 | self._create_message_container(message["message"], message_time, message_labels) 54 | 55 | def _create_message_container(self, message_content, message_time, message_labels): 56 | container = ttk.Frame(self.messages_frame) 57 | container.columnconfigure(1, weight=1) 58 | container.grid(sticky="EW", padx=(10, 50), pady=10) 59 | 60 | def reconfigure_message_labels(event): 61 | closest_break_point = min(SCREEN_SIZE_TO_MESSAGE_WIDTH.keys(), key=lambda b: abs(b - container.winfo_width())) 62 | for label, _ in message_labels: 63 | if label.cget("wraplength") != SCREEN_SIZE_TO_MESSAGE_WIDTH[closest_break_point]: 64 | label.configure(wraplength=SCREEN_SIZE_TO_MESSAGE_WIDTH[closest_break_point]) 65 | 66 | container.bind("", reconfigure_message_labels) 67 | self._create_message_bubble(container, message_content, message_time, message_labels) 68 | 69 | def _create_message_bubble(self, container, message_content, message_time, message_labels): 70 | avatar_image = Image.open("./assets/male.png") 71 | avatar_photo = ImageTk.PhotoImage(avatar_image) 72 | 73 | avatar_label = tk.Label( 74 | container, 75 | image=avatar_photo 76 | ) 77 | 78 | avatar_label.image = avatar_photo 79 | avatar_label.grid( 80 | row=0, 81 | column=0, 82 | rowspan=2, 83 | sticky="NEW", 84 | padx=(0, 10), 85 | pady=(5, 0) 86 | ) 87 | 88 | time_label = ttk.Label( 89 | container, 90 | text=message_time, 91 | ) 92 | 93 | time_label.grid(row=0, column=1, sticky="NEW") 94 | 95 | message_label = tk.Label( 96 | container, 97 | text=message_content, 98 | wraplength=800, 99 | justify="left", 100 | anchor="w" 101 | ) 102 | 103 | message_label.grid(row=1, column=1, sticky="NEW") 104 | 105 | message_labels.append((message_label, time_label)) 106 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_01_app_overview/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | black = "*" 11 | pillow = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_01_app_overview/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import tkinter.font as font 4 | from frames.chat import Chat 5 | 6 | try: 7 | from ctypes import windll 8 | windll.shcore.SetProcessDpiAwareness(1) 9 | except: 10 | pass 11 | 12 | COLOUR_LIGHT_BACKGROUND_1 = "#fff" 13 | COLOUR_LIGHT_BACKGROUND_2 = "#f2f3f5" 14 | COLOUR_LIGHT_BACKGROUND_3 = "#e3e5e8" 15 | 16 | COLOUR_DARK_BACKGROUND_1 = "#36393f" 17 | COLOUR_DARK_BACKGROUND_2 = "#2f3136" 18 | COLOUR_DARK_BACKGROUND_3 = "#202225" 19 | 20 | COLOUR_LIGHT_TEXT = "#aaa" 21 | 22 | COLOUR_BUTTON_NORMAL = "#5fba7d" 23 | COLOUR_BUTTON_ACTIVE = "#58c77c" 24 | COLOUR_BUTTON_PRESSED = "#44e378" 25 | 26 | 27 | class Messenger(tk.Tk): 28 | def __init__(self, *args, **kwargs): 29 | super().__init__(*args, **kwargs) 30 | 31 | self.geometry("1400x500") 32 | self.minsize(1400, 500) 33 | self.columnconfigure(0, weight=1) 34 | self.rowconfigure(0, weight=1) 35 | 36 | self.chat_frame = Chat( 37 | self, 38 | background=COLOUR_LIGHT_BACKGROUND_3, 39 | style="Messages.TFrame" 40 | ) 41 | 42 | self.chat_frame.grid(row=0, column=0, sticky="NSEW") 43 | 44 | 45 | 46 | root = Messenger() 47 | root.tk.call('tk', 'scaling', 3.0) 48 | 49 | font.nametofont("TkDefaultFont").configure(size=14) 50 | 51 | style = ttk.Style(root) 52 | style.theme_use("clam") 53 | 54 | style.configure("Messages.TFrame", background=COLOUR_LIGHT_BACKGROUND_3) 55 | # style.configure("DarkMessages.TFrame", background=COLOUR_DARK_BACKGROUND_3) 56 | 57 | style.configure("Controls.TFrame", background=COLOUR_LIGHT_BACKGROUND_2) 58 | # style.configure("DarkControls.TFrame", background=COLOUR_DARK_BACKGROUND_2) 59 | 60 | style.configure("SendButton.TButton", borderwidth=0, background=COLOUR_BUTTON_NORMAL) 61 | style.map( 62 | "SendButton.TButton", 63 | background=[("pressed", COLOUR_BUTTON_PRESSED), ("active", COLOUR_BUTTON_ACTIVE)], 64 | ) 65 | 66 | style.configure( 67 | "FetchButton.TButton", background=COLOUR_LIGHT_BACKGROUND_1, borderwidth=0 68 | ) 69 | 70 | # style.configure( 71 | # "DarkFetchButton.TButton", background=COLOUR_DARK_BACKGROUND_1, borderwidth=0 72 | # ) 73 | 74 | style.configure("Message.TMessage", background=COLOUR_LIGHT_BACKGROUND_2, padding=5) 75 | # style.configure("DarkMessage.TMessage", background=COLOUR_DARK_BACKGROUND_2, padding=5) 76 | 77 | style.configure( 78 | "Time.TLabel", 79 | padding=5, 80 | background=COLOUR_LIGHT_BACKGROUND_1, 81 | foreground=COLOUR_LIGHT_TEXT, 82 | font=("TkDefaultFont", 8) 83 | ) 84 | # style.configure( 85 | # "DarkTime.TLabel", 86 | # padding=5, 87 | # background=COLOUR_DARK_BACKGROUND_1, 88 | # foreground=COLOUR_LIGHT_TEXT, 89 | # font=8 90 | # ) 91 | 92 | root.mainloop() 93 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_01_app_overview/frames/chat.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import requests 4 | from frames.message_window import MessageWindow 5 | 6 | messages = [{"message": "Hello, world", "date": 15498487}] 7 | message_labels = [] 8 | 9 | 10 | class Chat(ttk.Frame): 11 | def __init__(self, container, background, *args, **kwargs): 12 | super().__init__(container, *args, **kwargs) 13 | 14 | self.columnconfigure(0, weight=1) 15 | self.rowconfigure(0, weight=1) 16 | 17 | self.message_window = MessageWindow(self, background=background) 18 | self.message_window.grid(row=0, column=0, sticky="NSEW", pady=5) 19 | 20 | input_frame = ttk.Frame(self, style="Controls.TFrame", padding=10) 21 | input_frame.grid(row=1, column=0, columnspan=2, sticky="EW") 22 | 23 | self.message_input = tk.Text(input_frame, height=3) 24 | self.message_input.pack(expand=True, fill="both", side="left", padx=(0, 10)) 25 | 26 | message_submit = ttk.Button( 27 | input_frame, 28 | text="Send", 29 | style="SendButton.TButton", 30 | command=self.post_message, 31 | ) 32 | message_submit.pack() 33 | 34 | message_fetch = ttk.Button( 35 | input_frame, 36 | text="Fetch", 37 | style="FetchButton.TButton", 38 | command=self.get_messages 39 | ) 40 | message_fetch.pack() 41 | self.message_window.update_message_widgets(messages, message_labels) 42 | 43 | def post_message(self): 44 | body = self.message_input.get("1.0", "end").strip() 45 | requests.post("http://167.99.63.70/message", json={"message": body}) 46 | self.message_input.delete('1.0', "end") 47 | self.get_messages() 48 | 49 | def get_messages(self): 50 | global messages 51 | messages = requests.get("http://167.99.63.70/messages").json() 52 | self.message_window.update_message_widgets(messages, message_labels) 53 | self.after(150, lambda: self.message_window.yview_moveto(1.0)) 54 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_02_getting_messages_from_api/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | black = "*" 11 | pillow = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_02_getting_messages_from_api/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from frames.chat import Chat 3 | 4 | 5 | class Messenger(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.geometry("1200x500") 10 | self.columnconfigure(0, weight=1) 11 | self.rowconfigure(0, weight=1) 12 | 13 | self.chat_frame = Chat(self) 14 | 15 | self.chat_frame.grid(row=0, column=0, sticky="NSEW") 16 | 17 | 18 | 19 | root = Messenger() 20 | root.mainloop() 21 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_02_getting_messages_from_api/frames/chat.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import requests 4 | 5 | messages = [{"message": "Hello, world", "date": 15498487}] 6 | message_labels = [] 7 | 8 | 9 | class Chat(ttk.Frame): 10 | def __init__(self, container, **kwargs): 11 | super().__init__(container, **kwargs) 12 | 13 | self.columnconfigure(0, weight=1) 14 | self.rowconfigure(0, weight=1) 15 | 16 | input_frame = ttk.Frame(self, padding=10) 17 | input_frame.grid(row=1, column=0, sticky="EW") 18 | 19 | message_fetch = ttk.Button( 20 | input_frame, 21 | text="Fetch", 22 | command=self.get_messages 23 | ) 24 | message_fetch.pack() 25 | 26 | def get_messages(self): 27 | global messages 28 | messages = requests.get("http://167.99.63.70/messages").json() 29 | print(messages) 30 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_03_creating_labels_for_new_messages/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | black = "*" 11 | pillow = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_03_creating_labels_for_new_messages/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from frames.chat import Chat 3 | 4 | 5 | class Messenger(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.geometry("1200x500") 10 | self.columnconfigure(0, weight=1) 11 | self.rowconfigure(0, weight=1) 12 | 13 | self.chat_frame = Chat(self) 14 | 15 | self.chat_frame.grid(row=0, column=0, sticky="NSEW") 16 | 17 | 18 | 19 | root = Messenger() 20 | root.mainloop() 21 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_03_creating_labels_for_new_messages/frames/chat.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import requests 4 | 5 | messages = [{"message": "Hello, world", "date": 15498487}] 6 | message_labels = [] 7 | 8 | 9 | class Chat(ttk.Frame): 10 | def __init__(self, container, *args, **kwargs): 11 | super().__init__(container, *args, **kwargs) 12 | 13 | self.columnconfigure(0, weight=1) 14 | self.rowconfigure(0, weight=1) 15 | 16 | self.messages_frame = ttk.Frame(self) 17 | self.messages_frame.grid(row=0, column=0, sticky="NSEW", pady=5) 18 | 19 | input_frame = ttk.Frame(self, padding=10) 20 | input_frame.grid(row=1, column=0, sticky="EW") 21 | 22 | message_fetch = ttk.Button( 23 | input_frame, 24 | text="Fetch", 25 | command=self.get_messages 26 | ) 27 | message_fetch.pack() 28 | 29 | def get_messages(self): 30 | global messages 31 | messages = requests.get("http://167.99.63.70/messages").json() 32 | self.update_message_widgets() 33 | 34 | def update_message_widgets(self): 35 | existing_labels = [ 36 | message["text"] for message in message_labels 37 | ] 38 | 39 | for message in messages: 40 | if message["message"] not in existing_labels: 41 | new_message = ttk.Label( 42 | self.messages_frame, 43 | text=message["message"], 44 | anchor="w", 45 | justify="left" 46 | ) 47 | 48 | new_message.grid(sticky="NSEW") 49 | 50 | message_labels.append(new_message) 51 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_04_showing_the_message_date/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | black = "*" 11 | pillow = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_04_showing_the_message_date/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from frames.chat import Chat 3 | 4 | 5 | class Messenger(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.geometry("1200x500") 10 | self.columnconfigure(0, weight=1) 11 | self.rowconfigure(0, weight=1) 12 | 13 | self.chat_frame = Chat(self) 14 | 15 | self.chat_frame.grid(row=0, column=0, sticky="NSEW") 16 | 17 | 18 | 19 | root = Messenger() 20 | root.mainloop() 21 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_04_showing_the_message_date/frames/chat.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import requests 4 | import datetime 5 | 6 | messages = [{"message": "Hello, world", "date": 15498487}] 7 | message_labels = [] 8 | 9 | 10 | class Chat(ttk.Frame): 11 | def __init__(self, container, *args, **kwargs): 12 | super().__init__(container, *args, **kwargs) 13 | 14 | self.columnconfigure(0, weight=1) 15 | self.rowconfigure(0, weight=1) 16 | 17 | self.messages_frame = ttk.Frame(self) 18 | self.messages_frame.grid(row=0, column=0, sticky="NSEW", pady=5) 19 | 20 | input_frame = ttk.Frame(self, padding=10) 21 | input_frame.grid(row=1, column=0, sticky="EW") 22 | 23 | message_fetch = ttk.Button( 24 | input_frame, 25 | text="Fetch", 26 | command=self.get_messages 27 | ) 28 | message_fetch.pack() 29 | 30 | def get_messages(self): 31 | global messages 32 | messages = requests.get("http://167.99.63.70/messages").json() 33 | self.update_message_widgets() 34 | 35 | def update_message_widgets(self): 36 | existing_labels = [ 37 | (message["text"], time["text"]) for message, time in message_labels 38 | ] 39 | 40 | for message in messages: 41 | message_time = datetime.datetime.fromtimestamp(message["date"]).strftime( 42 | "%d-%m-%Y %H:%M:%S" 43 | ) 44 | 45 | if (message["message"], message_time) not in existing_labels: 46 | container = ttk.Frame(self.messages_frame) 47 | container.columnconfigure(1, weight=1) 48 | container.grid(sticky="EW", padx=(10, 50), pady=10) 49 | 50 | time_label = ttk.Label( 51 | container, 52 | text=message_time, 53 | ) 54 | 55 | time_label.grid(row=0, column=0, sticky="NEW") 56 | 57 | new_message = ttk.Label( 58 | container, 59 | text=message["message"], 60 | anchor="w", 61 | justify="left" 62 | ) 63 | 64 | new_message.grid(row=1, column=0, sticky="NSEW") 65 | 66 | message_labels.append((new_message, time_label)) 67 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_05_adding_sample_user_avatar/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | black = "*" 11 | pillow = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_05_adding_sample_user_avatar/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from frames.chat import Chat 3 | 4 | 5 | class Messenger(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.geometry("1200x500") 10 | self.columnconfigure(0, weight=1) 11 | self.rowconfigure(0, weight=1) 12 | 13 | self.chat_frame = Chat(self) 14 | 15 | self.chat_frame.grid(row=0, column=0, sticky="NSEW") 16 | 17 | 18 | 19 | root = Messenger() 20 | root.mainloop() 21 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_05_adding_sample_user_avatar/frames/chat.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import requests 4 | import datetime 5 | from PIL import Image, ImageTk 6 | 7 | messages = [{"message": "Hello, world", "date": 15498487}] 8 | message_labels = [] 9 | 10 | 11 | class Chat(ttk.Frame): 12 | def __init__(self, container, *args, **kwargs): 13 | super().__init__(container, *args, **kwargs) 14 | 15 | self.columnconfigure(0, weight=1) 16 | self.rowconfigure(0, weight=1) 17 | 18 | self.messages_frame = ttk.Frame(self) 19 | self.messages_frame.grid(row=0, column=0, sticky="NSEW", pady=5) 20 | 21 | input_frame = ttk.Frame(self, padding=10) 22 | input_frame.grid(row=1, column=0, sticky="EW") 23 | 24 | message_fetch = ttk.Button( 25 | input_frame, 26 | text="Fetch", 27 | command=self.get_messages 28 | ) 29 | message_fetch.pack() 30 | 31 | def get_messages(self): 32 | global messages 33 | messages = requests.get("http://167.99.63.70/messages").json() 34 | self.update_message_widgets() 35 | 36 | def update_message_widgets(self): 37 | existing_labels = [ 38 | (message["text"], time["text"]) for message, time in message_labels 39 | ] 40 | 41 | for message in messages: 42 | message_time = datetime.datetime.fromtimestamp(message["date"]).strftime( 43 | "%d-%m-%Y %H:%M:%S" 44 | ) 45 | 46 | if (message["message"], message_time) not in existing_labels: 47 | self._create_message_container(message["message"], message_time, message_labels) 48 | 49 | def _create_message_container(self, message_content, message_time, message_labels): 50 | container = ttk.Frame(self.messages_frame) 51 | container.columnconfigure(1, weight=1) 52 | container.grid(sticky="EW", padx=(10, 50), pady=10) 53 | 54 | self._create_message_bubble(container, message_content, message_time, message_labels) 55 | 56 | def _create_message_bubble(self, container, message_content, message_time, message_labels): 57 | avatar_image = Image.open("./assets/male.png") 58 | avatar_photo = ImageTk.PhotoImage(avatar_image) 59 | 60 | avatar_label = tk.Label( 61 | container, 62 | image=avatar_photo 63 | ) 64 | 65 | avatar_label.image = avatar_photo 66 | avatar_label.grid( 67 | row=0, 68 | column=0, 69 | rowspan=2, 70 | sticky="NEW", 71 | padx=(0, 10), 72 | pady=(5, 0) 73 | ) 74 | 75 | time_label = ttk.Label( 76 | container, 77 | text=message_time, 78 | ) 79 | 80 | time_label.grid(row=0, column=1, sticky="NEW") 81 | 82 | message_label = ttk.Label( 83 | container, 84 | text=message_content, 85 | anchor="w", 86 | justify="left" 87 | ) 88 | 89 | message_label.grid(row=1, column=1, sticky="NSEW") 90 | 91 | message_labels.append((message_label, time_label)) 92 | 93 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_06_how_to_scroll_frame_tkinter/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | black = "*" 11 | pillow = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_06_how_to_scroll_frame_tkinter/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from frames.chat import Chat 3 | 4 | 5 | class Messenger(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.geometry("1200x500") 10 | self.columnconfigure(0, weight=1) 11 | self.rowconfigure(0, weight=1) 12 | 13 | self.chat_frame = Chat(self) 14 | 15 | self.chat_frame.grid(row=0, column=0, sticky="NSEW") 16 | 17 | 18 | 19 | root = Messenger() 20 | root.mainloop() 21 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_06_how_to_scroll_frame_tkinter/frames/chat.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import requests 4 | from frames.message_window import MessageWindow 5 | 6 | messages = [{"message": "Hello, world", "date": 15498487}] 7 | message_labels = [] 8 | 9 | 10 | class Chat(ttk.Frame): 11 | def __init__(self, container, *args, **kwargs): 12 | super().__init__(container, *args, **kwargs) 13 | 14 | self.columnconfigure(0, weight=1) 15 | self.rowconfigure(0, weight=1) 16 | 17 | self.message_window = MessageWindow(self) 18 | self.message_window.grid(row=0, column=0, sticky="NSEW", pady=5) 19 | 20 | input_frame = ttk.Frame(self, padding=10) 21 | input_frame.grid(row=1, column=0, sticky="EW") 22 | 23 | message_fetch = ttk.Button( 24 | input_frame, 25 | text="Fetch", 26 | command=self.get_messages 27 | ) 28 | message_fetch.pack() 29 | self.message_window.update_message_widgets(messages, message_labels) 30 | 31 | def get_messages(self): 32 | global messages 33 | messages = requests.get("http://167.99.63.70/messages").json() 34 | self.message_window.update_message_widgets(messages, message_labels) 35 | self.after(150, lambda: self.message_window.yview_moveto(1.0)) 36 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_06_how_to_scroll_frame_tkinter/frames/message_window.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import tkinter as tk 3 | from tkinter import ttk 4 | from PIL import Image, ImageTk 5 | 6 | 7 | class MessageWindow(tk.Canvas): 8 | def __init__(self, container, *args, **kwargs): 9 | super().__init__(container, *args, **kwargs, highlightthickness=0) 10 | 11 | self.messages_frame = ttk.Frame(container) 12 | self.messages_frame.columnconfigure(0, weight=1) 13 | 14 | self.scrollable_window = self.create_window((0, 0), window=self.messages_frame, anchor="nw") 15 | 16 | def configure_scroll_region(event): 17 | self.configure(scrollregion=self.bbox("all")) 18 | 19 | def configure_window_size(event): 20 | self.itemconfig(self.scrollable_window, width=self.winfo_width()) 21 | 22 | self.bind("", configure_window_size) 23 | self.messages_frame.bind("", configure_scroll_region) 24 | self.bind_all("", self._on_mousewheel) 25 | 26 | scrollbar = ttk.Scrollbar(container, orient="vertical", command=self.yview) 27 | scrollbar.grid(row=0, column=1, sticky="NS") 28 | 29 | self.configure(yscrollcommand=scrollbar.set) 30 | self.yview_moveto(1.0) 31 | 32 | # https://stackoverflow.com/a/17457843/1587271 33 | def _on_mousewheel(self, event): 34 | self.yview_scroll(-int(event.delta/120), "units") 35 | 36 | def update_message_widgets(self, messages, message_labels): 37 | existing_labels = [ 38 | (message["text"], time["text"]) for message, time in message_labels 39 | ] 40 | 41 | for message in messages: 42 | message_time = datetime.datetime.fromtimestamp(message["date"]).strftime( 43 | "%d-%m-%Y %H:%M:%S" 44 | ) 45 | 46 | if (message["message"], message_time) not in existing_labels: 47 | self._create_message_container(message["message"], message_time, message_labels) 48 | 49 | def _create_message_container(self, message_content, message_time, message_labels): 50 | container = ttk.Frame(self.messages_frame) 51 | container.columnconfigure(1, weight=1) 52 | container.grid(sticky="EW", padx=(10, 50), pady=10) 53 | 54 | self._create_message_bubble(container, message_content, message_time, message_labels) 55 | 56 | def _create_message_bubble(self, container, message_content, message_time, message_labels): 57 | avatar_image = Image.open("./assets/male.png") 58 | avatar_photo = ImageTk.PhotoImage(avatar_image) 59 | 60 | avatar_label = tk.Label( 61 | container, 62 | image=avatar_photo 63 | ) 64 | 65 | avatar_label.image = avatar_photo 66 | avatar_label.grid( 67 | row=0, 68 | column=0, 69 | rowspan=2, 70 | sticky="NEW", 71 | padx=(0, 10), 72 | pady=(5, 0) 73 | ) 74 | 75 | time_label = ttk.Label( 76 | container, 77 | text=message_time, 78 | ) 79 | 80 | time_label.grid(row=0, column=1, sticky="NEW") 81 | 82 | message_label = tk.Label( 83 | container, 84 | text=message_content, 85 | wraplength=800, 86 | justify="left", 87 | anchor="w" 88 | ) 89 | 90 | message_label.grid(row=1, column=1, sticky="NEW") 91 | 92 | message_labels.append((message_label, time_label)) 93 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_08_handle_resizing_and_wrapping_labels/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | black = "*" 11 | pillow = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_08_handle_resizing_and_wrapping_labels/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from frames.chat import Chat 3 | 4 | 5 | class Messenger(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.geometry("1200x500") 10 | self.minsize(800, 500) 11 | self.columnconfigure(0, weight=1) 12 | self.rowconfigure(0, weight=1) 13 | 14 | self.chat_frame = Chat(self) 15 | 16 | self.chat_frame.grid(row=0, column=0, sticky="NSEW") 17 | 18 | 19 | 20 | root = Messenger() 21 | root.mainloop() 22 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_08_handle_resizing_and_wrapping_labels/frames/chat.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import requests 4 | from frames.message_window import MessageWindow 5 | 6 | messages = [{"message": "Hello, world", "date": 15498487}] 7 | message_labels = [] 8 | 9 | 10 | class Chat(ttk.Frame): 11 | def __init__(self, container, *args, **kwargs): 12 | super().__init__(container, *args, **kwargs) 13 | 14 | self.columnconfigure(0, weight=1) 15 | self.rowconfigure(0, weight=1) 16 | 17 | self.message_window = MessageWindow(self) 18 | self.message_window.grid(row=0, column=0, sticky="NSEW", pady=5) 19 | 20 | input_frame = ttk.Frame(self, padding=10) 21 | input_frame.grid(row=1, column=0, sticky="EW") 22 | 23 | message_fetch = ttk.Button( 24 | input_frame, 25 | text="Fetch", 26 | command=self.get_messages 27 | ) 28 | message_fetch.pack() 29 | self.message_window.update_message_widgets(messages, message_labels) 30 | 31 | def get_messages(self): 32 | global messages 33 | messages = requests.get("http://167.99.63.70/messages").json() 34 | self.message_window.update_message_widgets(messages, message_labels) 35 | self.after(150, lambda: self.message_window.yview_moveto(1.0)) 36 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_08_handle_resizing_and_wrapping_labels/frames/message_window.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import tkinter as tk 3 | from tkinter import ttk 4 | from PIL import Image, ImageTk 5 | 6 | 7 | MAX_MESSAGE_WIDTH = 800 8 | 9 | 10 | class MessageWindow(tk.Canvas): 11 | def __init__(self, container, *args, **kwargs): 12 | super().__init__(container, *args, **kwargs, highlightthickness=0) 13 | 14 | self.messages_frame = ttk.Frame(container) 15 | self.messages_frame.columnconfigure(0, weight=1) 16 | 17 | self.scrollable_window = self.create_window((0, 0), window=self.messages_frame, anchor="nw", width=self.winfo_width()) 18 | 19 | def configure_scroll_region(event): 20 | self.configure(scrollregion=self.bbox("all")) 21 | 22 | def configure_window_size(event): 23 | self.itemconfig(self.scrollable_window, width=self.winfo_width()) 24 | 25 | self.bind("", configure_window_size) 26 | self.messages_frame.bind("", configure_scroll_region) 27 | self.bind_all("", self._on_mousewheel) 28 | 29 | scrollbar = ttk.Scrollbar(container, orient="vertical", command=self.yview) 30 | scrollbar.grid(row=0, column=1, sticky="NS") 31 | 32 | self.configure(yscrollcommand=scrollbar.set) 33 | self.yview_moveto(1.0) 34 | 35 | # https://stackoverflow.com/a/17457843/1587271 36 | def _on_mousewheel(self, event): 37 | self.yview_scroll(-int(event.delta/120), "units") 38 | 39 | def update_message_widgets(self, messages, message_labels): 40 | existing_labels = [ 41 | (message["text"], time["text"]) for message, time in message_labels 42 | ] 43 | 44 | for message in messages: 45 | message_time = datetime.datetime.fromtimestamp(message["date"]).strftime( 46 | "%d-%m-%Y %H:%M:%S" 47 | ) 48 | 49 | if (message["message"], message_time) not in existing_labels: 50 | self._create_message_container(message["message"], message_time, message_labels) 51 | 52 | def _create_message_container(self, message_content, message_time, message_labels): 53 | container = ttk.Frame(self.messages_frame) 54 | container.columnconfigure(1, weight=1) 55 | container.grid(sticky="EW", padx=(10, 50), pady=10) 56 | 57 | def reconfigure_message_labels(event): 58 | for label, _ in message_labels: 59 | label.configure(wraplength=min(container.winfo_width() - 130, MAX_MESSAGE_WIDTH)) # image width, frame padx, and image padx 60 | self.messages_frame.update() 61 | 62 | container.bind("", reconfigure_message_labels) 63 | self._create_message_bubble(container, message_content, message_time, message_labels) 64 | 65 | def _create_message_bubble(self, container, message_content, message_time, message_labels): 66 | avatar_image = Image.open("./assets/male.png") 67 | avatar_photo = ImageTk.PhotoImage(avatar_image) 68 | 69 | avatar_label = tk.Label( 70 | container, 71 | image=avatar_photo 72 | ) 73 | 74 | avatar_label.image = avatar_photo 75 | avatar_label.grid( 76 | row=0, 77 | column=0, 78 | rowspan=2, 79 | sticky="NEW", 80 | padx=(0, 10), 81 | pady=(5, 0) 82 | ) 83 | 84 | time_label = ttk.Label( 85 | container, 86 | text=message_time, 87 | ) 88 | 89 | time_label.grid(row=0, column=1, sticky="NEW") 90 | 91 | message_label = tk.Label( 92 | container, 93 | text=message_content, 94 | wraplength=800, 95 | justify="left", 96 | anchor="w" 97 | ) 98 | 99 | message_label.grid(row=1, column=1, sticky="NEW") 100 | 101 | message_labels.append((message_label, time_label)) 102 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_09_sending_messages/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | black = "*" 11 | pillow = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_09_sending_messages/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from frames.chat import Chat 3 | 4 | 5 | class Messenger(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.geometry("1200x500") 10 | self.minsize(800, 500) 11 | self.columnconfigure(0, weight=1) 12 | self.rowconfigure(0, weight=1) 13 | 14 | self.chat_frame = Chat(self) 15 | 16 | self.chat_frame.grid(row=0, column=0, sticky="NSEW") 17 | 18 | 19 | 20 | root = Messenger() 21 | root.mainloop() 22 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_09_sending_messages/frames/chat.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import requests 4 | from frames.message_window import MessageWindow 5 | 6 | messages = [{"message": "Hello, world", "date": 15498487}] 7 | message_labels = [] 8 | 9 | 10 | class Chat(ttk.Frame): 11 | def __init__(self, container, *args, **kwargs): 12 | super().__init__(container, *args, **kwargs) 13 | 14 | self.columnconfigure(0, weight=1) 15 | self.rowconfigure(0, weight=1) 16 | 17 | self.message_window = MessageWindow(self) 18 | self.message_window.grid(row=0, column=0, sticky="NSEW", pady=5) 19 | 20 | input_frame = ttk.Frame(self, padding=10) 21 | input_frame.grid(row=1, column=0, sticky="EW") 22 | 23 | self.message_input = tk.Text(input_frame, height=3) 24 | self.message_input.pack(expand=True, fill="both", side="left", padx=(0, 10)) 25 | 26 | message_submit = ttk.Button( 27 | input_frame, 28 | text="Send", 29 | command=self.post_message 30 | ) 31 | message_submit.pack() 32 | 33 | message_fetch = ttk.Button( 34 | input_frame, 35 | text="Fetch", 36 | command=self.get_messages 37 | ) 38 | message_fetch.pack() 39 | self.message_window.update_message_widgets(messages, message_labels) 40 | 41 | def post_message(self): 42 | body = self.message_input.get("1.0", "end").strip() 43 | requests.post("http://167.99.63.70/message", json={"message": body}) 44 | self.message_input.delete('1.0', "end") 45 | self.get_messages() 46 | 47 | def get_messages(self): 48 | global messages 49 | messages = requests.get("http://167.99.63.70/messages").json() 50 | self.message_window.update_message_widgets(messages, message_labels) 51 | self.after(150, lambda: self.message_window.yview_moveto(1.0)) 52 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_09_sending_messages/frames/message_window.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import tkinter as tk 3 | from tkinter import ttk 4 | from PIL import Image, ImageTk 5 | 6 | SCREEN_SIZE_TO_MESSAGE_WIDTH = { 7 | 1100: 900, 8 | 950: 700, 9 | 750: 550 10 | } 11 | 12 | 13 | class MessageWindow(tk.Canvas): 14 | def __init__(self, container, *args, **kwargs): 15 | super().__init__(container, *args, **kwargs, highlightthickness=0) 16 | 17 | self.messages_frame = ttk.Frame(container) 18 | self.messages_frame.columnconfigure(0, weight=1) 19 | 20 | self.scrollable_window = self.create_window((0, 0), window=self.messages_frame, anchor="nw", width=self.winfo_width()) 21 | 22 | def configure_scroll_region(event): 23 | self.configure(scrollregion=self.bbox("all")) 24 | 25 | def configure_window_size(event): 26 | self.itemconfig(self.scrollable_window, width=self.winfo_width()) 27 | 28 | self.bind("", configure_window_size) 29 | self.messages_frame.bind("", configure_scroll_region) 30 | self.bind_all("", self._on_mousewheel) 31 | 32 | scrollbar = ttk.Scrollbar(container, orient="vertical", command=self.yview) 33 | scrollbar.grid(row=0, column=1, sticky="NS") 34 | 35 | self.configure(yscrollcommand=scrollbar.set) 36 | self.yview_moveto(1.0) 37 | 38 | # https://stackoverflow.com/a/17457843/1587271 39 | def _on_mousewheel(self, event): 40 | self.yview_scroll(-int(event.delta/120), "units") 41 | 42 | def update_message_widgets(self, messages, message_labels): 43 | existing_labels = [ 44 | (message["text"], time["text"]) for message, time in message_labels 45 | ] 46 | 47 | for message in messages: 48 | message_time = datetime.datetime.fromtimestamp(message["date"]).strftime( 49 | "%d-%m-%Y %H:%M:%S" 50 | ) 51 | 52 | if (message["message"], message_time) not in existing_labels: 53 | self._create_message_container(message["message"], message_time, message_labels) 54 | 55 | def _create_message_container(self, message_content, message_time, message_labels): 56 | container = ttk.Frame(self.messages_frame) 57 | container.columnconfigure(1, weight=1) 58 | container.grid(sticky="EW", padx=(10, 50), pady=10) 59 | 60 | def reconfigure_message_labels(event): 61 | closest_break_point = min(SCREEN_SIZE_TO_MESSAGE_WIDTH.keys(), key=lambda b: abs(b - container.winfo_width())) 62 | for label, _ in message_labels: 63 | if label.winfo_width() < closest_break_point: 64 | label.configure(wraplength=SCREEN_SIZE_TO_MESSAGE_WIDTH[closest_break_point]) 65 | self.messages_frame.update() 66 | 67 | container.bind("", reconfigure_message_labels) 68 | self._create_message_bubble(container, message_content, message_time, message_labels) 69 | 70 | def _create_message_bubble(self, container, message_content, message_time, message_labels): 71 | avatar_image = Image.open("./assets/male.png") 72 | avatar_photo = ImageTk.PhotoImage(avatar_image) 73 | 74 | avatar_label = tk.Label( 75 | container, 76 | image=avatar_photo 77 | ) 78 | 79 | avatar_label.image = avatar_photo 80 | avatar_label.grid( 81 | row=0, 82 | column=0, 83 | rowspan=2, 84 | sticky="NEW", 85 | padx=(0, 10), 86 | pady=(5, 0) 87 | ) 88 | 89 | time_label = ttk.Label( 90 | container, 91 | text=message_time, 92 | ) 93 | 94 | time_label.grid(row=0, column=1, sticky="NEW") 95 | 96 | message_label = tk.Label( 97 | container, 98 | text=message_content, 99 | wraplength=800, 100 | justify="left", 101 | anchor="w" 102 | ) 103 | 104 | message_label.grid(row=1, column=1, sticky="NEW") 105 | 106 | message_labels.append((message_label, time_label)) 107 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_10_styling_our_app/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | black = "*" 11 | pillow = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_10_styling_our_app/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import tkinter.font as font 4 | from frames.chat import Chat 5 | 6 | COLOUR_LIGHT_BACKGROUND_1 = "#fff" 7 | COLOUR_LIGHT_BACKGROUND_2 = "#f2f3f5" 8 | COLOUR_LIGHT_BACKGROUND_3 = "#e3e5e8" 9 | 10 | COLOUR_LIGHT_TEXT = "#aaa" 11 | 12 | COLOUR_BUTTON_NORMAL = "#5fba7d" 13 | COLOUR_BUTTON_ACTIVE = "#58c77c" 14 | COLOUR_BUTTON_PRESSED = "#44e378" 15 | 16 | 17 | class Messenger(tk.Tk): 18 | def __init__(self, *args, **kwargs): 19 | super().__init__(*args, **kwargs) 20 | 21 | self.geometry("1200x500") 22 | self.minsize(800, 500) 23 | self.columnconfigure(0, weight=1) 24 | self.rowconfigure(0, weight=1) 25 | 26 | self.chat_frame = Chat( 27 | self, 28 | background=COLOUR_LIGHT_BACKGROUND_3, 29 | style="Messages.TFrame" 30 | ) 31 | 32 | self.chat_frame.grid(row=0, column=0, sticky="NSEW") 33 | 34 | 35 | 36 | root = Messenger() 37 | 38 | font.nametofont("TkDefaultFont").configure(size=14) 39 | 40 | style = ttk.Style(root) 41 | style.theme_use("clam") 42 | 43 | style.configure("Messages.TFrame", background=COLOUR_LIGHT_BACKGROUND_3) 44 | 45 | style.configure("Controls.TFrame", background=COLOUR_LIGHT_BACKGROUND_2) 46 | 47 | style.configure("SendButton.TButton", borderwidth=0, background=COLOUR_BUTTON_NORMAL) 48 | style.map( 49 | "SendButton.TButton", 50 | background=[("pressed", COLOUR_BUTTON_PRESSED), ("active", COLOUR_BUTTON_ACTIVE)], 51 | ) 52 | 53 | style.configure( 54 | "FetchButton.TButton", background=COLOUR_LIGHT_BACKGROUND_1, borderwidth=0 55 | ) 56 | 57 | style.configure( 58 | "Time.TLabel", 59 | padding=5, 60 | background=COLOUR_LIGHT_BACKGROUND_1, 61 | foreground=COLOUR_LIGHT_TEXT, 62 | font=8 63 | ) 64 | 65 | style.configure("Avatar.TLabel", background=COLOUR_LIGHT_BACKGROUND_3) 66 | style.configure("Message.TLabel", background=COLOUR_LIGHT_BACKGROUND_2) 67 | 68 | root.mainloop() 69 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_10_styling_our_app/frames/chat.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import requests 4 | from frames.message_window import MessageWindow 5 | 6 | messages = [{"message": "Hello, world", "date": 15498487}] 7 | message_labels = [] 8 | 9 | 10 | class Chat(ttk.Frame): 11 | def __init__(self, container, background, *args, **kwargs): 12 | super().__init__(container, *args, **kwargs) 13 | 14 | self.columnconfigure(0, weight=1) 15 | self.rowconfigure(0, weight=1) 16 | 17 | self.message_window = MessageWindow(self, background=background) 18 | self.message_window.grid(row=0, column=0, sticky="NSEW", pady=5) 19 | 20 | input_frame = ttk.Frame(self, style="Controls.TFrame", padding=10) 21 | input_frame.grid(row=1, column=0, sticky="EW") 22 | 23 | self.message_input = tk.Text(input_frame, height=3) 24 | self.message_input.pack(expand=True, fill="both", side="left", padx=(0, 10)) 25 | 26 | message_submit = ttk.Button( 27 | input_frame, 28 | text="Send", 29 | style="SendButton.TButton", 30 | command=self.post_message 31 | ) 32 | message_submit.pack() 33 | 34 | message_fetch = ttk.Button( 35 | input_frame, 36 | text="Fetch", 37 | style="FetchButton.TButton", 38 | command=self.get_messages 39 | ) 40 | message_fetch.pack() 41 | self.message_window.update_message_widgets(messages, message_labels) 42 | 43 | def post_message(self): 44 | body = self.message_input.get("1.0", "end").strip() 45 | requests.post("http://167.99.63.70/message", json={"message": body}) 46 | self.message_input.delete('1.0', "end") 47 | self.get_messages() 48 | 49 | def get_messages(self): 50 | global messages 51 | messages = requests.get("http://167.99.63.70/messages").json() 52 | self.message_window.update_message_widgets(messages, message_labels) 53 | self.after(150, lambda: self.message_window.yview_moveto(1.0)) 54 | -------------------------------------------------------------------------------- /section09/lectures/PTK_09_10_styling_our_app/frames/message_window.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import tkinter as tk 3 | from tkinter import ttk 4 | from PIL import Image, ImageTk 5 | 6 | SCREEN_SIZE_TO_MESSAGE_WIDTH = { 7 | 1100: 900, 8 | 950: 700, 9 | 750: 550 10 | } 11 | 12 | 13 | class MessageWindow(tk.Canvas): 14 | def __init__(self, container, *args, **kwargs): 15 | super().__init__(container, *args, **kwargs, highlightthickness=0) 16 | 17 | self.messages_frame = ttk.Frame(container, style="Messages.TFrame") 18 | self.messages_frame.columnconfigure(0, weight=1) 19 | 20 | self.scrollable_window = self.create_window((0, 0), window=self.messages_frame, anchor="nw", width=self.winfo_width()) 21 | 22 | def configure_scroll_region(event): 23 | self.configure(scrollregion=self.bbox("all")) 24 | 25 | def configure_window_size(event): 26 | self.itemconfig(self.scrollable_window, width=self.winfo_width()) 27 | 28 | self.bind("", configure_window_size) 29 | self.messages_frame.bind("", configure_scroll_region) 30 | self.bind_all("", self._on_mousewheel) 31 | 32 | scrollbar = ttk.Scrollbar(container, orient="vertical", command=self.yview) 33 | scrollbar.grid(row=0, column=1, sticky="NS") 34 | 35 | self.configure(yscrollcommand=scrollbar.set) 36 | self.yview_moveto(1.0) 37 | 38 | # https://stackoverflow.com/a/17457843/1587271 39 | def _on_mousewheel(self, event): 40 | self.yview_scroll(-int(event.delta/120), "units") 41 | 42 | def update_message_widgets(self, messages, message_labels): 43 | existing_labels = [ 44 | (message["text"], time["text"]) for message, time in message_labels 45 | ] 46 | 47 | for message in messages: 48 | message_time = datetime.datetime.fromtimestamp(message["date"]).strftime( 49 | "%d-%m-%Y %H:%M:%S" 50 | ) 51 | 52 | if (message["message"], message_time) not in existing_labels: 53 | self._create_message_container(message["message"], message_time, message_labels) 54 | 55 | def _create_message_container(self, message_content, message_time, message_labels): 56 | container = ttk.Frame(self.messages_frame, style="Messages.TFrame") 57 | container.columnconfigure(1, weight=1) 58 | container.grid(sticky="EW", padx=(10, 50), pady=10) 59 | 60 | def reconfigure_message_labels(event): 61 | closest_break_point = min(SCREEN_SIZE_TO_MESSAGE_WIDTH.keys(), key=lambda b: abs(b - container.winfo_width())) 62 | for label, _ in message_labels: 63 | if label.winfo_width() < closest_break_point: 64 | label.configure(wraplength=SCREEN_SIZE_TO_MESSAGE_WIDTH[closest_break_point]) 65 | self.messages_frame.update() 66 | 67 | container.bind("", reconfigure_message_labels) 68 | self._create_message_bubble(container, message_content, message_time, message_labels) 69 | 70 | def _create_message_bubble(self, container, message_content, message_time, message_labels): 71 | avatar_image = Image.open("./assets/male.png") 72 | avatar_photo = ImageTk.PhotoImage(avatar_image) 73 | 74 | avatar_label = ttk.Label( 75 | container, 76 | image=avatar_photo, 77 | style="Avatar.TLabel" 78 | ) 79 | 80 | avatar_label.image = avatar_photo 81 | avatar_label.grid( 82 | row=0, 83 | column=0, 84 | rowspan=2, 85 | sticky="NEW", 86 | padx=(0, 10), 87 | pady=(5, 0) 88 | ) 89 | 90 | time_label = ttk.Label( 91 | container, 92 | text=message_time, 93 | style="Time.TLabel" 94 | ) 95 | 96 | time_label.grid(row=0, column=1, sticky="NEW") 97 | 98 | message_label = ttk.Label( 99 | container, 100 | text=message_content, 101 | wraplength=800, 102 | justify="left", 103 | anchor="w", 104 | style="Message.TLabel" 105 | ) 106 | 107 | message_label.grid(row=1, column=1, sticky="NEW") 108 | 109 | message_labels.append((message_label, time_label)) 110 | -------------------------------------------------------------------------------- /section10/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | pillow = "*" 10 | pyinstaller = "*" 11 | 12 | [requires] 13 | python_version = "3.7" 14 | -------------------------------------------------------------------------------- /section10/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "017156006223ce9fb829ee1b9e0513b4c1d90142c17a2dc543b5e1bb8ad135b3" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "altgraph": { 20 | "hashes": [ 21 | "sha256:d6814989f242b2b43025cba7161fc1b8fb487a62cd49c49245d6fd01c18ac997", 22 | "sha256:ddf5320017147ba7b810198e0b6619bd7b5563aa034da388cea8546b877f9b0c" 23 | ], 24 | "version": "==0.16.1" 25 | }, 26 | "future": { 27 | "hashes": [ 28 | "sha256:20eed10e1366e5d35b1f55c19c4825947ae1cc214dc891ec309790eb3f752141", 29 | "sha256:858e38522e8fd0d3ce8f0c1feaf0603358e366d5403209674c7b617fa0c24093" 30 | ], 31 | "version": "==0.18.1" 32 | }, 33 | "pefile": { 34 | "hashes": [ 35 | "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645" 36 | ], 37 | "version": "==2019.4.18" 38 | }, 39 | "pillow": { 40 | "hashes": [ 41 | "sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031", 42 | "sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71", 43 | "sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c", 44 | "sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340", 45 | "sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa", 46 | "sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b", 47 | "sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573", 48 | "sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e", 49 | "sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab", 50 | "sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9", 51 | "sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e", 52 | "sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291", 53 | "sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12", 54 | "sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871", 55 | "sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281", 56 | "sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08", 57 | "sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41", 58 | "sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2", 59 | "sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5", 60 | "sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb", 61 | "sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547", 62 | "sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75", 63 | "sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9", 64 | "sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1", 65 | "sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a", 66 | "sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96", 67 | "sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132", 68 | "sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a", 69 | "sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5", 70 | "sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0" 71 | ], 72 | "index": "pypi", 73 | "version": "==6.2.1" 74 | }, 75 | "pyinstaller": { 76 | "hashes": [ 77 | "sha256:ee7504022d1332a3324250faf2135ea56ac71fdb6309cff8cd235de26b1d0a96" 78 | ], 79 | "index": "pypi", 80 | "version": "==3.5" 81 | }, 82 | "pywin32-ctypes": { 83 | "hashes": [ 84 | "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", 85 | "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" 86 | ], 87 | "version": "==0.2.0" 88 | } 89 | }, 90 | "develop": {} 91 | } 92 | -------------------------------------------------------------------------------- /section10/assets/food.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/gui-development-tkinter-course/90a3979615997223f308832ce842e002cd571be4/section10/assets/food.png -------------------------------------------------------------------------------- /section10/assets/snake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/gui-development-tkinter-course/90a3979615997223f308832ce842e002cd571be4/section10/assets/snake.png -------------------------------------------------------------------------------- /section11/README.md: -------------------------------------------------------------------------------- 1 | # Packaging and Distributing Python Executables 2 | 3 | If you want to distribute your Tkinter applications, we recommend using PyInstaller. 4 | 5 | - [Their website](http://www.pyinstaller.org/); 6 | - [Their documentation](https://pyinstaller.readthedocs.io/en/stable/); 7 | - [Their GitHub](https://github.com/pyinstaller/pyinstaller); 8 | - [Their FAQ](https://github.com/pyinstaller/pyinstaller/wiki/FAQ). 9 | 10 | The gist of it is: PyInstaller will let you turn your Python applications into an executable for the platform that makes the executable. 11 | 12 | That means that, if you build your application on Windows, it'll produce a `.exe`. If you run it on Mac, it'll produce a `.app`, and so on. 13 | 14 | To install PyInstaller, just `pip install pyinstaller`. Make sure to activate your virtual environment first. 15 | 16 | Then, you can build your Tkinter applications and turn them into executables with this command: 17 | 18 | ``` 19 | pyinstaller app.py --onefile 20 | ``` 21 | 22 | This will produce two folders: `build` and `dist`. Inside the latter, you'll find your executable. You can share and distribute this with other people, even if they don't have Python in their system! 23 | 24 | ## Including data files 25 | 26 | To include a data folder, you'll need to add an option to the `pyinstaller` call: 27 | 28 | ``` 29 | pyinstaller app.py --onefile --add-data='assets;assets' 30 | ``` 31 | 32 | Note that in Mac and Linux, you'll need to do `--add-data='assets/*.png:assets'`, with a colon instead of a semicolon. 33 | 34 | That copies the local folder called "assets", and links it to a folder also called "assets" in the bundle. -------------------------------------------------------------------------------- /section11/assets/food.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/gui-development-tkinter-course/90a3979615997223f308832ce842e002cd571be4/section11/assets/food.png -------------------------------------------------------------------------------- /section11/assets/snake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/gui-development-tkinter-course/90a3979615997223f308832ce842e002cd571be4/section11/assets/snake.png -------------------------------------------------------------------------------- /section11/lectures/PTK_11_01_installation_and_documentation/README.md: -------------------------------------------------------------------------------- 1 | # Installation and Documentation 2 | 3 | Install pyinstaller by running `pipenv install pyinstaller`. 4 | 5 | Documentation is available here: 6 | 7 | - [PyInstaller documentation](https://pyinstaller.readthedocs.io/en/stable/operating-mode.html) 8 | 9 | ## What it does 10 | 11 | PyInstaller lets you build an executable that you can double-click and run without needing to install Python first. 12 | 13 | Thus, it's great for sharing your applications with other people, especially non-developers. 14 | 15 | When you run PyInstaller, it creates an executable for the Operating System that ran it. So if you run PyInstaller on Windows, it'll create a `.exe`. If you run it on Mac, it creates a `.app`. 16 | 17 | To quote their official documentation: 18 | 19 | > PyInstaller reads a Python script written by you. It analyzes your code to discover every other module and library your script needs in order to execute. Then it collects copies of all those files – including the active Python interpreter! – and puts them with your script in a single folder, or optionally in a single executable file. -------------------------------------------------------------------------------- /section11/lectures/PTK_11_02_two_ways_to_bundle/README.md: -------------------------------------------------------------------------------- 1 | # Two ways to bundle with PyInstaller 2 | 3 | There are two ways to bundle: [one folder](https://pyinstaller.readthedocs.io/en/stable/operating-mode.html#bundling-to-one-folder), and [one file](https://pyinstaller.readthedocs.io/en/stable/operating-mode.html#bundling-to-one-file). 4 | 5 | The official documentation has a great explanation of what goes on in each. Click the links above to read through it! 6 | -------------------------------------------------------------------------------- /section11/lectures/PTK_11_06_hiding_console_window/README.md: -------------------------------------------------------------------------------- 1 | # Hiding the console window 2 | 3 | By default, a console window is created for you. 4 | 5 | If you want to not do that, use a `.pyw` extension instead of `.py` for your app, before running PyInstaller. 6 | 7 | Do note that doing this will prevent you from seeing errors shown in the console! 8 | 9 | ## The `--windowed` option 10 | 11 | Alternatively, and arguably a better option, is to provide the `--windowed` option as well as `--onefile` or `--onefolder`. 12 | 13 | If there is an error while launching the application when using windowed mode, you'll get an error modal popup on your operating system. -------------------------------------------------------------------------------- /section11/lectures/PTK_11_07_when_things_go_wrong/README.md: -------------------------------------------------------------------------------- 1 | # When things go wrong 2 | 3 | PyInstaller has a lengthy section of their documentation detailing what to do when things go wrong: https://pyinstaller.readthedocs.io/en/stable/when-things-go-wrong.html 4 | 5 | In addition to that, I'd like to give you some practical tips. 6 | 7 | ## Keeping the Windows console when an error happens 8 | 9 | By default, the Windows console will automatically close when the application crashes. This can make it really difficult to see an error that's printed out there. 10 | 11 | The first time you encounter an error, you should surround your entire code with a try-except block. In there, print the traceback and also include an `input()` statement so that the Windows console does not close until you enter something (to satisfy the `input()` function). 12 | 13 | Like this: 14 | 15 | ``` 16 | try: 17 | root = tk.Tk() 18 | root.title("Snake") 19 | root.resizable(False, False) 20 | 21 | board = Snake() 22 | board.pack() 23 | 24 | 25 | root.mainloop() 26 | except: 27 | import traceback 28 | traceback.print_exc() 29 | input("Press Enter to end...") 30 | ``` -------------------------------------------------------------------------------- /section11/lectures/PTK_11_08_building_for_multiple_platforms/README.md: -------------------------------------------------------------------------------- 1 | # Building for multiple platforms 2 | 3 | As we've mentioned before, to build an executable for a platform, PyInstaller must be ran on that platform. 4 | 5 | No way around it, so you must get access to a target platform Operating System to build. 6 | 7 | You could do this using a Continuous Integration solution like AppVeyor or CircleCI. In them, you can specify the Operating System that will run your code. Do note that you may have to pay for some of this functionality. 8 | 9 | Although we won't cover how, the general process goes as follows: 10 | 11 | - Create your AppVeyor or CircleCI account; 12 | - Link your GitHub repository with it so it knows when there are code changes; 13 | - Configure your AppVeyor or CircleCI project so that it uses your target operating system; 14 | - Configure your AppVeyor or CircleCI project so that it runs PyInstaller; 15 | - Make the resultant `dist` folder available somewhere for download by your users (an option is Amazon's S3). -------------------------------------------------------------------------------- /section11/lectures/PTK_11_08_building_for_multiple_platforms/README_further_learning.md: -------------------------------------------------------------------------------- 1 | # Further learning 2 | 3 | I strongly recommend reading through the official documentation, particularly these sections: 4 | 5 | - https://pyinstaller.readthedocs.io/en/stable/operating-mode.html 6 | - https://pyinstaller.readthedocs.io/en/stable/usage.html#options 7 | - https://pyinstaller.readthedocs.io/en/stable/usage.html#supporting-multiple-platforms 8 | - https://pyinstaller.readthedocs.io/en/stable/usage.html#platform-specific-notes 9 | - https://pyinstaller.readthedocs.io/en/stable/runtime-information.html 10 | - https://pyinstaller.readthedocs.io/en/stable/spec-files.html 11 | - https://pyinstaller.readthedocs.io/en/stable/when-things-go-wrong.html 12 | 13 | I know it's quite a lot, but it'll be worth it! 14 | 15 | In addition, remember the documentation has more sections on advanced topics that you can read if you think you may need those. 16 | 17 | Finally, there's also a [recipes page](https://github.com/pyinstaller/pyinstaller/wiki/Recipes) that contains code to do specific things with PyInstaller, such as getting an executable from a Django app, or one to do multiprocessing. These code samples and common issues will reduce the amount of work you have to do when trying to get something like this working. -------------------------------------------------------------------------------- /section11/windows.py: -------------------------------------------------------------------------------- 1 | def set_dpi_awareness(): 2 | try: 3 | from ctypes import windll 4 | windll.shcore.SetProcessDpiAwareness(1) 5 | except: 6 | pass -------------------------------------------------------------------------------- /test_projects/calculator/calc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/gui-development-tkinter-course/90a3979615997223f308832ce842e002cd571be4/test_projects/calculator/calc.gif -------------------------------------------------------------------------------- /test_projects/calculator/calculator_01.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | """ 5 | Right now the handle_click function simply takes a value specific to each button and prints it. 6 | This is intended as a demonstration of how to pass in variables to Button commands. 7 | In the standard case, they simply take a function name. 8 | """ 9 | 10 | 11 | def handle_click(x): 12 | print(x) 13 | 14 | 15 | root = tk.Tk() 16 | root.title("Calculator") 17 | 18 | buttons = ttk.Frame(root, padding="10") 19 | buttons.grid() 20 | 21 | """ 22 | Passing in a variable to a command can be easily achieved using a lambda function. 23 | 24 | TODO: Another option exists using partials. This may be better, but I need to do more research. 25 | TODO: I need to see if there is also a better way to deal with the amount of repetition in the following lines. 26 | """ 27 | ttk.Button(buttons, text="1").grid(column=0, row=0) 28 | ttk.Button(buttons, text="2").grid(column=1, row=0) 29 | ttk.Button(buttons, text="3").grid(column=2, row=0) 30 | ttk.Button(buttons, text="/").grid(column=3, row=0) 31 | 32 | ttk.Button(buttons, text="4").grid(column=0, row=1) 33 | ttk.Button(buttons, text="5").grid(column=1, row=1) 34 | ttk.Button(buttons, text="6").grid(column=2, row=1) 35 | ttk.Button(buttons, text="*").grid(column=3, row=1) 36 | 37 | ttk.Button(buttons, text="7").grid(column=0, row=2) 38 | ttk.Button(buttons, text="8").grid(column=1, row=2) 39 | ttk.Button(buttons, text="9").grid(column=2, row=2) 40 | ttk.Button(buttons, text="-").grid(column=3, row=2) 41 | 42 | ttk.Button(buttons, text=".").grid(column=0, row=3) 43 | ttk.Button(buttons, text="0").grid(column=1, row=3) 44 | ttk.Button(buttons, text="=").grid(column=2, row=3) 45 | ttk.Button(buttons, text="+").grid(column=3, row=3) 46 | 47 | """ 48 | winfo is short for window information. The winfo_children() method gets all children of a container. 49 | For each child, we get a reference like this: 50 | 51 | .!frame.!button 52 | 53 | which can be used to select a specific widget. As such, we can iterate over these widgets and call methods on them individually. 54 | Below we call the grid_configure method, allowing us to specify x and y padding for each button in buttons. 55 | """ 56 | for child in buttons.winfo_children(): 57 | child.grid_configure(padx=5, pady=5) 58 | child.configure(command=lambda button=child: handle_click(button["text"])) 59 | """ 60 | child.configure(command=lambda: handle_click(child['text'])) did not solve the repetition issue. 61 | All of the values for child['text'] end up being "+", as child in the first line of the for loop shares an extended 62 | scope with child inside the lambda function. 63 | The value of child['text'] does not get evaluated until the function is called, by which time, the value of child is 64 | the button containing the text content, "+". 65 | 66 | Fixed the issue by setting a default value for a new lambda argument called button. 67 | Default arguments are evaluated when a function is created, not when they are called, 68 | meaning we can preserve the value of each iteration of child after the loop completes. 69 | """ 70 | 71 | root.mainloop() 72 | -------------------------------------------------------------------------------- /test_projects/calculator/calculator_02.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | """ 5 | Right now the handle_click function simply takes a value specific to each button and prints it. 6 | This is intended as a demonstration of how to pass in variables to Button commands. 7 | In the standard case, they simply take a function name. 8 | """ 9 | 10 | 11 | def handle_click(x): 12 | print(x) 13 | 14 | 15 | root = tk.Tk() 16 | root.title("Calculator") 17 | 18 | buttons = ttk.Frame(root, padding="10") 19 | buttons.grid() 20 | 21 | row_0 = ttk.Frame(buttons) 22 | row_1 = ttk.Frame(buttons) 23 | row_2 = ttk.Frame(buttons) 24 | row_3 = ttk.Frame(buttons) 25 | 26 | """ 27 | Passing in a variable to a command can be easily achieved using a lambda function. 28 | 29 | TODO: Another option exists using partials. This may be better, but I need to do more research. 30 | 31 | Split the button grid into rows, allowing me to remove the row definition from each item, 32 | adding them to the for loop later in the app. 33 | """ 34 | ttk.Button(row_0, text="1", command=lambda: handle_click("1")).grid(column=0) 35 | ttk.Button(row_0, text="2", command=lambda: handle_click("2")).grid(column=1) 36 | ttk.Button(row_0, text="3", command=lambda: handle_click("3")).grid(column=2) 37 | ttk.Button(row_0, text="/", command=lambda: handle_click("/")).grid(column=3) 38 | 39 | ttk.Button(row_1, text="4", command=lambda: handle_click("4")).grid(column=0) 40 | ttk.Button(row_1, text="5", command=lambda: handle_click("5")).grid(column=1) 41 | ttk.Button(row_1, text="6", command=lambda: handle_click("6")).grid(column=2) 42 | ttk.Button(row_1, text="*", command=lambda: handle_click("*")).grid(column=3) 43 | 44 | ttk.Button(row_2, text="7", command=lambda: handle_click("7")).grid(column=0) 45 | ttk.Button(row_2, text="8", command=lambda: handle_click("8")).grid(column=1) 46 | ttk.Button(row_2, text="9", command=lambda: handle_click("9")).grid(column=2) 47 | ttk.Button(row_2, text="-", command=lambda: handle_click("-")).grid(column=3) 48 | 49 | ttk.Button(row_3, text=".", command=lambda: handle_click(".")).grid(column=0) 50 | ttk.Button(row_3, text="0", command=lambda: handle_click("0")).grid(column=1) 51 | ttk.Button(row_3, text="=", command=lambda: handle_click("=")).grid(column=2) 52 | ttk.Button(row_3, text="+", command=lambda: handle_click("+")).grid(column=3) 53 | 54 | """ 55 | winfo is short for window information. The winfo_children() method gets all children of a container. 56 | For each child, we get a reference like this: 57 | 58 | .!frame.!button 59 | 60 | which can be used to select a specific widget. As such, we can iterate over these widgets and call methods on them individually. 61 | Below we call the grid_configure method, allowing us to specify x and y padding for each button in buttons. 62 | """ 63 | for i, button_row in enumerate(buttons.winfo_children()): 64 | """ 65 | Defined the grid for each item in the outer for loop, removing the need for individual definitions above. 66 | Not 100% sure if this is better, as it does make it harder to see what is happening in the program. 67 | I'm tempted to say that a DRY approach to Tkinter is maybe not ideal, as removing the repetition massively complicates things. 68 | """ 69 | button_row.grid(row=i) 70 | 71 | for button in button_row.winfo_children(): 72 | # row=0 is necessary to stop them trying to move onto independent rows. 73 | button.grid_configure(padx=5, pady=5, row=0) 74 | 75 | root.mainloop() 76 | -------------------------------------------------------------------------------- /test_projects/calculator/calculator_03.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | """ 5 | Right now the handle_click function simply takes a value specific to each button and prints it. 6 | This is intended as a demonstration of how to pass in variables to Button commands. 7 | In the standard case, they simply take a function name. 8 | """ 9 | 10 | 11 | def handle_click(x): 12 | print(x) 13 | 14 | 15 | root = tk.Tk() 16 | root.title("Calculator") 17 | 18 | buttons = ttk.Frame(root, padding="10") 19 | buttons.grid() 20 | 21 | """ 22 | Passing in a variable to a command can be easily achieved using a lambda function. 23 | 24 | The create_button function seeks to bypass the awkward solution to using a lamdba function with command, found in calculator_01.py 25 | The argument to handle_click is now created when the button is created, allowing for an extensive reduction in the grid setup. 26 | 27 | A pair of nested for loops and a list comprehension are used to generate the 16 buttons. 28 | Each button is created by create_button, and positioned precisely using grid. 29 | 30 | The innermost for loop now covered every element in the buttons group, meaning that we can do away with the for loop 31 | featured in the previous calculator iterations. 32 | 33 | 34 | for button in button_row.winfo_children(): 35 | button.grid_configure(padx=5, pady=5, row=1) 36 | 37 | 38 | Note that the grid method cannot be called in the create_button function, but I have not found a concrete reason as to why yet. 39 | Notice also that the buttons object above required that the grid method be called after the object was created. 40 | This seems to be a common pattern. 41 | """ 42 | 43 | 44 | def create_button(text): 45 | return ttk.Button(buttons, text=text, command=lambda: handle_click(text)) 46 | 47 | 48 | texts = ( 49 | ["1", "2", "3", "/"], 50 | ["4", "5", "6", "*"], 51 | ["7", "8", "9", "-"], 52 | [".", "0", "=", "+"], 53 | ) 54 | 55 | for i, text_group in enumerate(texts): 56 | button_row = [create_button(text) for text in text_group] 57 | 58 | for j, button in enumerate(button_row): 59 | button.grid(column=j, row=i, padx=5, pady=5) 60 | 61 | root.mainloop() 62 | -------------------------------------------------------------------------------- /test_projects/calculator/calculator_04.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import os # Imported to allow for finding the calculator icon 4 | 5 | 6 | class CalculatorApp(tk.Tk): 7 | def __init__(self, *args, **kwargs): 8 | # Initialise Tkinter along with the MainApplication 9 | super().__init__(*args, **kwargs) 10 | 11 | # Create an encompassing Frame called container. 12 | container = ttk.Frame(self) 13 | container.grid(padx=10, pady=10) 14 | 15 | # Assign an instance of the Buttons class to the variable buttons 16 | buttons = Buttons(container, self) 17 | 18 | """ 19 | We can define a dictionary called frames which will allow us to store references to 20 | our various views in the application. This means we can change the view in response 21 | to some event. See the show_frame method below. 22 | """ 23 | self.frames = {} 24 | # Create an entry in the dictionary for the Buttons frame 25 | self.frames[Buttons] = buttons 26 | 27 | buttons.grid() 28 | 29 | # We use the show_frame method to set the current view to the Buttons Frame 30 | self.show_frame(Buttons) 31 | 32 | def show_frame(self, container): 33 | """ 34 | The show_frame method calls the tkraise method on a specific frame, bringing it to 35 | the topmost layer of a window. Frames are then stacked on top of one another, with the 36 | topmost Frame obscuring the view to the ones below. 37 | 38 | The show_frame method takes a container as an argument and searches the frames dictionary 39 | defined above for a key matching the name of that container. The value of that container 40 | is then assigned to the frame variable, and raised to the topmost level of the stack. 41 | """ 42 | 43 | frame = self.frames[container] 44 | frame.tkraise() 45 | 46 | 47 | class Buttons(ttk.Frame): 48 | def __init__(self, parent, controller): 49 | super().__init__(parent) 50 | texts = ( 51 | ["1", "2", "3", "/"], 52 | ["4", "5", "6", "*"], 53 | ["7", "8", "9", "-"], 54 | [".", "0", "=", "+"], 55 | ) 56 | 57 | for i, text_group in enumerate(texts): 58 | button_row = [self.create_button(text) for text in text_group] 59 | 60 | for j, button in enumerate(button_row): 61 | button.grid(column=j, row=i, padx=5, pady=5) 62 | 63 | def create_button(self, text): 64 | return ttk.Button(text=text, command=lambda: self.handle_click(text)) 65 | 66 | def handle_click(self, x): 67 | print(x) 68 | 69 | 70 | root = CalculatorApp() 71 | 72 | root.call("wm", "iconphoto", root._w, tk.PhotoImage("calc.gif")) 73 | root.mainloop() 74 | -------------------------------------------------------------------------------- /test_projects/currency_converter/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.6" 12 | -------------------------------------------------------------------------------- /test_projects/currency_converter/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "415dfdcb118dd9bdfef17671cb7dcd78dbd69b6ae7d4f39e8b44e71d60ca72e7" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": {} 20 | } 21 | -------------------------------------------------------------------------------- /test_projects/currency_converter/currency_converter_01.py: -------------------------------------------------------------------------------- 1 | # TODO: Add labels to the plot axes, and add a graph title 2 | 3 | from tkinter import ttk 4 | import tkinter as tk 5 | 6 | # Import animation to let us live update the plots. 7 | import matplotlib.animation as animation 8 | 9 | # Import matplotlib and define the backend to TkAgg to help it work with Tkinter. 10 | from matplotlib.figure import Figure 11 | 12 | # Gross import to grab the matplotlib canvas and the built in buttons to zoom and select, etc. 13 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk 14 | import matplotlib 15 | 16 | matplotlib.use("TkAgg") 17 | 18 | 19 | # Defines a matplotlib figure 20 | fig = Figure(figsize=(5, 5), dpi=100) 21 | 22 | # Convention seems to be to call these subplots a or ax 23 | ax = fig.add_subplot(111) 24 | 25 | """ 26 | The animate function takes in an interval and allows us to continuously replot a matplotlib figure. 27 | 28 | In this example, we grab some dummy data in csv format from dummyData.txt 29 | We extract the data to two lists: x_vals and y_vals, then call plot to actually display the data. 30 | 31 | ax.clear() here is a vital line, as it prevents us from painting over the same information every interval. 32 | We need a fresh canvas each time we plot, or it'll become a total mess. 33 | """ 34 | 35 | 36 | def animate(i): 37 | with open("dummyData.txt", "r") as file: 38 | lines = [line.strip() for line in file.readlines()] 39 | 40 | x_vals = [] 41 | y_vals = [] 42 | 43 | for line in lines: 44 | x, y = line.split(",") 45 | x_vals.append(int(x)) 46 | y_vals.append(int(y)) 47 | 48 | ax.clear() 49 | ax.plot(x_vals, y_vals) 50 | 51 | """ 52 | Tried this shorter alternative, but the fact that the values are strings keeps the y axis from being ordered. 53 | Leaving it here, because it highlights a potential error students might run into. 54 | 55 | lines = [line.split(",") for line in lines] 56 | x, y = zip(*(line for line in lines)) 57 | 58 | ax.clear() 59 | ax.plot(x, y) 60 | """ 61 | 62 | 63 | class CurrencyConverter(tk.Tk): 64 | def __init__(self, *args, **kwargs): 65 | super().__init__(*args, **kwargs) 66 | 67 | self.title("Currency Converter") 68 | 69 | container = ttk.Frame(self) 70 | container.grid(padx=10, pady=10, sticky="EW") 71 | 72 | self.frames = {} 73 | 74 | # The comma below is important as we need an iterable object for for. Without it, it tries to iterate over the class. 75 | for F in (DummyData,): 76 | frame = F(container, self) 77 | self.frames[F] = frame 78 | frame.grid(row=0, column=0, sticky="NSEW") 79 | 80 | self.show_frame(DummyData) 81 | 82 | def show_frame(self, container): 83 | frame = self.frames[container] 84 | frame.tkraise() 85 | 86 | 87 | class DummyData(ttk.Frame): 88 | def __init__(self, parent, controller): 89 | super().__init__(parent) 90 | 91 | # Define a canvas, passing in some figure object. 92 | canvas = FigureCanvasTkAgg(fig, self) 93 | 94 | # The show method paints our canvas. Until we actually call show, all the plotting is done in the backend. 95 | canvas.draw() 96 | canvas.get_tk_widget().pack() 97 | 98 | # This toolbar gives us the standard matplotlib buttons below our plot. 99 | toolbar = NavigationToolbar2Tk(canvas, self) 100 | toolbar.update() 101 | canvas._tkcanvas.pack() 102 | 103 | 104 | root = CurrencyConverter() 105 | 106 | # Below we define the animation function behaviour. I needs to be assigned to something, but it doesn't appear to matter what. 107 | # Convention seems to be using ani or anim as a variable name. 108 | ani = animation.FuncAnimation(fig, animate, interval=1000) 109 | root.mainloop() 110 | -------------------------------------------------------------------------------- /test_projects/currency_converter/currency_converter_02.py: -------------------------------------------------------------------------------- 1 | # TODO: Add labels to the plot axes, and add a graph title 2 | 3 | import json 4 | import requests 5 | from tkinter import ttk 6 | import tkinter as tk 7 | import matplotlib.animation as animation 8 | from matplotlib.figure import Figure 9 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk 10 | import matplotlib 11 | 12 | matplotlib.use("TkAgg") 13 | 14 | 15 | """ 16 | Grab historical USD value data from free.currencyconverterapi.com 17 | Data is limited to a span of 8 days. Values are an average for the day. 18 | 19 | The format of the data is as follows: 20 | 21 | { 22 | "_": { 23 | "": , 24 | "": 25 | ... 26 | }, 27 | "_": { 28 | "": , 29 | "": 30 | ... 31 | } 32 | } 33 | 34 | The API is relatively simple. Docs can be found here: https://www.currencyconverterapi.com/docs 35 | """ 36 | 37 | url = "https://free.currencyconverterapi.com/api/v6/convert?q=USD_GBP&compact=ultra&date=2019-01-01&endDate=2019-01-09&apiKey=443eaf8aa1f431564727" 38 | 39 | fig = Figure(figsize=(10, 5), dpi=100) 40 | a = fig.add_subplot(111) 41 | 42 | 43 | def animate(i): 44 | data = requests.get(url) 45 | json_data = data.json() 46 | 47 | dates, rates = zip(*json_data["USD_GBP"].items()) 48 | 49 | a.clear() 50 | a.plot(dates, rates) 51 | 52 | 53 | class CurrencyConverter(tk.Tk): 54 | def __init__(self, *args, **kwargs): 55 | super().__init__(*args, **kwargs) 56 | 57 | self.title("Currency Converter") 58 | 59 | container = ttk.Frame(self) 60 | container.grid(padx=10, pady=10, sticky="EW") 61 | 62 | self.frames = {} 63 | 64 | for F in (Home, HistoricalData): 65 | frame = F(container, self) 66 | self.frames[F] = frame 67 | frame.grid(row=0, column=0, sticky="NESW") 68 | 69 | self.show_frame(Home) 70 | 71 | def show_frame(self, container): 72 | frame = self.frames[container] 73 | frame.tkraise() 74 | 75 | 76 | class Home(ttk.Frame): 77 | def __init__(self, parent, controller): 78 | super().__init__(parent) 79 | 80 | button = ttk.Button( 81 | self, 82 | text="Historical Data", 83 | command=lambda: controller.show_frame(HistoricalData), 84 | ) 85 | button.pack() 86 | 87 | 88 | class HistoricalData(ttk.Frame): 89 | def __init__(self, parent, controller): 90 | super().__init__(parent) 91 | 92 | canvas = FigureCanvasTkAgg(fig, self) 93 | canvas.draw() 94 | canvas.get_tk_widget().pack() 95 | 96 | toolbar = NavigationToolbar2Tk(canvas, self) 97 | toolbar.update() 98 | canvas._tkcanvas.pack() 99 | 100 | 101 | root = CurrencyConverter() 102 | # Request every 60 seconds, since we are strictly limited by the API 103 | ani = animation.FuncAnimation(fig, animate, interval=60000) 104 | root.mainloop() 105 | -------------------------------------------------------------------------------- /test_projects/currency_converter/dummyData.txt: -------------------------------------------------------------------------------- 1 | 1, 6 2 | 2, 7 3 | 3, 3 4 | 4, 4 5 | 5, 7 6 | 6, 1 7 | 7, 3 8 | 8, 2 9 | 9, 4 10 | 10, 6 11 | 11, 3 12 | 12, 4 -------------------------------------------------------------------------------- /test_projects/distance_converter/distance_converter_01.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | """ 5 | While calculate_feet doesn't use any arguments for its main logic, we later specify 6 | that when a user uses the return key, this function will run. If we do not allow 7 | for positional arguments, pressing return will give use an error: 8 | 9 | TypeError: calculate_feet() takes 0 positional arguments but 1 was given 10 | 11 | In our case, the argument passed in is a KeyPress event object: 12 | 13 | 14 | """ 15 | 16 | 17 | def calculate_feet(*args): 18 | try: 19 | """ 20 | The get() method is used to fetch the value of a StringVar() instance. 21 | 22 | The set() method is the counterpart of get() and sets the value of 23 | a StringVar() instance. 24 | """ 25 | value = float(metres_value.get()) 26 | feet_value.set(value * 3.28084) 27 | except ValueError: 28 | # For now we're just going to ignore faulty inputs to the converter 29 | pass 30 | 31 | 32 | root = tk.Tk() 33 | root.title("Distance Converter") 34 | 35 | main = ttk.Frame(root, padding="30 15 30 15") 36 | main.grid() 37 | 38 | """ 39 | We configure root to have a single row and column, with weight (importance / priority) set to 1. 40 | This means that as the window expands, priority is given to this row to expand, 41 | allowing it to stretch to the window size in both directions. 42 | """ 43 | root.columnconfigure(0, weight=1) 44 | root.rowconfigure(0, weight=1) 45 | 46 | 47 | # Here we create two instances of the StringVar() class, which is to track the content of widgets 48 | feet_value = tk.StringVar() 49 | metres_value = tk.StringVar() 50 | 51 | """ 52 | Below we use a few new properties, and a new widget type. 53 | 54 | The Entry widget is used to accept text input of some type, and works very much like 55 | 56 | 57 | 58 | in HTML. It's a single line box that accepts a string of characters. 59 | 60 | We also dfine a specific width for this input box. Width does not take a pixel value, 61 | but rather a character size. It therefore functions like ems. It's possible to define the 62 | character to use for these character based size measurements. Obviously for monospace fonts, 63 | the character chosen doesn't matter, but for variable width fonts, it can make a major difference: 64 | 65 | font.measure("m") 66 | 67 | A final point of note is the textvariable keyword argument used with Entry. This associates a variable 68 | with the content of the Entry field. As such, the value of metres_value will update automatically 69 | as the content of metres_input gets updated. 70 | """ 71 | metres_input = ttk.Entry(main, width=10, textvariable=metres_value) 72 | 73 | """ 74 | The sticky property below anchors a widget to the edges of its container. Compass directions 75 | are used to specify this behaviour. You can pass in a tuple of values to anchor a widget to more than 76 | one edge. 77 | """ 78 | metres_input.grid(column=2, row=1, sticky="EW") 79 | 80 | 81 | """ 82 | Once again we specify a textvariable argument, but for Label it behaves differently to input. 83 | For a label, the text content of that lable updates when the value of the associated variable 84 | changes. As such, when the value of feet_value changes, the text content of this Label will be 85 | instantly updated. 86 | """ 87 | ttk.Label(main, textvariable=feet_value).grid(column=2, row=2, sticky="EW") 88 | ttk.Button(main, text="Calculate", command=calculate_feet).grid( 89 | column=1, row=3, columnspan=2, sticky="EW" 90 | ) 91 | 92 | ttk.Label(main, text="metres").grid(column=1, row=1, sticky="W", ipadx=5) 93 | ttk.Label(main, text="feet").grid(column=1, row=2, sticky="W", ipadx=5) 94 | 95 | for child in main.winfo_children(): 96 | child.grid_configure(padx=5, pady=5) 97 | 98 | # We can specify a default focus using the focus method on a widget. 99 | metres_input.focus() 100 | 101 | """ 102 | Below we use the bind method to call the calculate_feet function in response to some even. 103 | In this instance, the event is pressing the return key on the keyboard. 104 | 105 | Note that bind is attached to a specific element, so it's possible to trigger behaviour only for 106 | certain frames. By binding to root, this event will be triggered for all frames, since they are 107 | all children of root. 108 | """ 109 | root.bind("", calculate_feet) 110 | # This bind is for the enter key on a numpad 111 | root.bind("", calculate_feet) 112 | 113 | root.mainloop() 114 | -------------------------------------------------------------------------------- /test_projects/distance_converter/distance_converter_02.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | class DistanceConverter(tk.Tk): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | 9 | self.title("Distance Calculator") 10 | self.frames = {} 11 | 12 | container = ttk.Frame(self) 13 | container.grid(padx=10, pady=10, sticky="EW") 14 | 15 | for F in (MetresToFeet, FeetToMetres): 16 | frame = F(container, self) 17 | self.frames[F] = frame 18 | frame.grid(row=0, column=0, sticky="NSEW") 19 | 20 | self.show_frame(MetresToFeet) 21 | 22 | def show_frame(self, container): 23 | frame = self.frames[container] 24 | frame.tkraise() 25 | 26 | 27 | class MetresToFeet(ttk.Frame): 28 | def __init__(self, parent, controller): 29 | super().__init__(parent) 30 | 31 | self.feet_value = tk.StringVar() 32 | self.metres_value = tk.StringVar() 33 | 34 | metres_label = ttk.Label(self, text="metres") 35 | metres_label.grid(column=1, row=1, sticky="W", ipadx=5) 36 | metres_input = ttk.Entry(self, width=10, textvariable=self.metres_value) 37 | metres_input.grid(column=2, row=1, sticky="EW") 38 | metres_input.focus() 39 | 40 | feet_label = ttk.Label(self, text="feet") 41 | feet_label.grid(column=1, row=2, sticky="W", ipadx=5) 42 | feet_display = ttk.Label(self, textvariable=self.feet_value) 43 | feet_display.grid(column=2, row=2, sticky="EW") 44 | 45 | calculate_button = ttk.Button( 46 | self, text="Calculate", command=self.calculate_feet 47 | ) 48 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 49 | 50 | switch_page_button = ttk.Button( 51 | self, 52 | text="Switch to feet conversion", 53 | command=lambda: controller.show_frame(FeetToMetres), 54 | ) 55 | switch_page_button.grid(column=1, row=4, columnspan=2, sticky="EW") 56 | 57 | for child in self.winfo_children(): 58 | child.grid_configure(padx=5, pady=5) 59 | 60 | def calculate_feet(self, *args): 61 | try: 62 | value = float(self.metres_value.get()) 63 | self.feet_value.set("%.3f" % (value * 3.28084)) 64 | except ValueError: 65 | pass 66 | 67 | 68 | class FeetToMetres(ttk.Frame): 69 | def __init__(self, parent, controller): 70 | super().__init__(parent) 71 | 72 | self.feet_value = tk.StringVar() 73 | self.metres_value = tk.StringVar() 74 | 75 | feet_label = ttk.Label(self, text="feet") 76 | feet_label.grid(column=1, row=1, sticky="W", ipadx=5) 77 | feet_input = ttk.Entry(self, width=10, textvariable=self.feet_value) 78 | feet_input.grid(column=2, row=1, sticky="EW") 79 | feet_input.focus() 80 | 81 | metres_label = ttk.Label(self, text="metres") 82 | metres_label.grid(column=1, row=2, sticky="W", ipadx=5) 83 | metres_display = ttk.Label(self, textvariable=self.metres_value) 84 | metres_display.grid(column=2, row=2, sticky="EW") 85 | 86 | calculate_button = ttk.Button( 87 | self, text="Calculate", command=self.calculate_metres 88 | ) 89 | calculate_button.grid(column=1, row=3, columnspan=2, sticky="EW") 90 | 91 | switch_page_button = ttk.Button( 92 | self, 93 | text="Switch to metres conversion", 94 | command=lambda: controller.show_frame(MetresToFeet), 95 | ) 96 | switch_page_button.grid(column=1, row=4, columnspan=2, sticky="EW") 97 | 98 | for child in self.winfo_children(): 99 | child.grid_configure(padx=5, pady=5) 100 | 101 | def calculate_metres(self, *args): 102 | try: 103 | value = float(self.feet_value.get()) 104 | self.metres_value.set("%.3f" % (value / 3.28084)) 105 | except ValueError: 106 | pass 107 | 108 | 109 | root = DistanceConverter() 110 | root.mainloop() 111 | --------------------------------------------------------------------------------