Skip to content

Εργαστήριο 5 στην Python

Θέματα που εξετάζονται στο εργαστήριο: Γραφικά περιβάλλοντα διεπαφής με το tkinter1, APIs, matplotlib, MVC (Model View Controller)

Εξάσκηση (εκφωνήσεις και λύσεις ασκήσεων)

Άσκηση E5A1 - Δημιουργήστε ένα πρόγραμμα το οποίο να υλοποιεί μια λίστα εργασιών (todo list) μέσω ενός γραφικού περιβάλλοντος διεπαφής. Ο χρήστης να μπορεί να εισάγει εργασίες στη λίστα και να διαγράφει εργασίες από τη λίστα. Να μην επιτρέπεται η εισαγωγή της ίδιας εργασίας πάνω από μια φορά στη λίστα εργασιών.

Λύση άσκησης E5A1

5_1_sol.py
import tkinter as tk
from tkinter import ttk  # ttk = themed widgets
from tkinter.messagebox import showwarning


def add_task():
    task = entry_task.get()
    if task == "":
        showwarning(title="Σφάλμα!", message="Θα πρέπει να εισάγετε μια εργασία")
    elif task in tasks.get():
        showwarning(title="Σφάλμα!", message="H εργασία ήδη υπάρχει στη λίστα")
    else:
        listbox_tasks.insert(tk.END, task)
        entry_task.delete(0, tk.END)


def delete_task():
    try:
        task_index = listbox_tasks.curselection()[0]  # επιλογή εργασίας
        listbox_tasks.delete(task_index)
    except:
        showwarning(title="Σφάλμα!", message="Θα πρέπει να επιλέξετε μια εργασία")


#  δημιουργία GUI
root = tk.Tk()  # δημιουργία παραθύρου
root.title("Λίστα εργασιών")
tasks = tk.Variable(value=[])

frame_tasks = ttk.Frame(root)  # δημιουργία frame για το listbox και το scrollbar
frame_tasks.pack()  # τοποθέτηση frame στο παράθυρο

listbox_tasks = tk.Listbox(
    frame_tasks, height=10, width=50, listvariable=tasks
)  # δημιουργία listbox
listbox_tasks.pack(side=tk.LEFT)  # τοποθέτηση listbox στο frame

scrollbar_tasks = ttk.Scrollbar(frame_tasks)  # δημιουργία scrollbar για το listbox
scrollbar_tasks.pack(side=tk.RIGHT, fill=tk.Y)  # τοποθέτηση scrollbar στο frame

listbox_tasks.config(yscrollcommand=scrollbar_tasks.set)  # σύνδεση listbox με scrollbar
scrollbar_tasks.config(command=listbox_tasks.yview)  # σύνδεση scrollbar με listbox

entry_task = ttk.Entry(root)
entry_task.pack(
    fill="x", expand=True
)  # fill="x" για να γεμίζει το πλάτος του παραθύρου

button_add_task = ttk.Button(root, text="Νέα εργασία")
button_add_task["command"] = add_task  # ή button_add_task.config(command=add_task)
button_add_task.pack(fill="x", expand=True)

button_delete_task = ttk.Button(root, text="Διαγραφή εργασίας", command=delete_task)
button_delete_task.pack(fill="x", expand=True)

root.eval("tk::PlaceWindow . center")  # τοποθέτηση παραθύρου στο κέντρο της οθόνης
root.mainloop()  # έναρξη βρόχου επεξεργασίας γραφικών
Παράδειγμα εκτέλεσης:
$ python 5_1_sol.py
TODO List GUI example

Λύση άσκησης E5A1 με OOP

5_1_sol_oop.py
import tkinter as tk
from tkinter import ttk  # themed widgets
from tkinter.messagebox import showwarning


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Λίστα εργασιών")
        self.frame_tasks = ttk.Frame(self)
        self.frame_tasks.pack()

        self.tasks = tk.Variable(value=[])
        self.listbox_tasks = tk.Listbox(
            self.frame_tasks, height=10, width=50, listvariable=self.tasks
        )
        self.listbox_tasks.pack(side=tk.LEFT)

        self.scrollbar_tasks = ttk.Scrollbar(self.frame_tasks)
        self.scrollbar_tasks.pack(side=tk.RIGHT, fill=tk.Y)

        # σύνδεση listbox με scrollbar
        self.listbox_tasks.config(yscrollcommand=self.scrollbar_tasks.set)
        self.scrollbar_tasks.config(command=self.listbox_tasks.yview)

        self.entry_task = ttk.Entry(self)
        self.entry_task.pack(fill="x", expand=True)

        self.button_add_task = ttk.Button(
            self, text="Νέα εργασία", command=self.add_task
        )
        self.button_add_task.pack(fill="x", expand=True)

        self.button_delete_task = ttk.Button(
            self, text="Διαγραφή εργασίας", command=self.delete_task
        )
        self.button_delete_task.pack(fill="x", expand=True)

    def add_task(self):
        task = self.entry_task.get()
        if task == "":
            showwarning(title="Σφάλμα!", message="Θα πρέπει να εισάγετε μια εργασία")
        elif task in self.tasks.get():
            showwarning(title="Σφάλμα!", message="H εργασία ήδη υπάρχει στη λίστα")
        else:
            self.listbox_tasks.insert(tk.END, task)
            self.entry_task.delete(0, tk.END)

    def delete_task(self):
        try:
            task_index = self.listbox_tasks.curselection()[0]
            self.listbox_tasks.delete(task_index)
        except:
            showwarning(title="Σφάλμα!", message="Θα πρέπει να επιλέξετε μια εργασία")


if __name__ == "__main__":
    app = App()
    app.mainloop()
Παράδειγμα εκτέλεσης:
$ python 5_1_sol_oop.py
TODO List GUI example OOP

Άσκηση E5A2 - Δημιουργήστε ένα πρόγραμμα που να διαχειρίζεται επαφές (contacts). Για κάθε επαφή να διατηρούνται οι πληροφορίες, επώνυμο, όνομα, τηλέφωνο. Να παρέχεται λειτουργικότητα CRUD (Create, Retrieve, Update, Delete). Τα δεδομένα να αποθηκεύονται σε αρχείο contacts.csv και να ανακαλούνται από αυτό κατά την εκκίνηση του προγράμματος.

Λύση άσκησης E5A2

5_2_sol.py
# Λύση από τον φοιτητή Ράντο Κωνσταντίνο
# του Τμήματος Πληροφορικής και Τηλεπικοινωνιών
# του Πανεπιστημίου Ιωαννίνων τον Απρίλιο του 2024

import csv
import tkinter as tk
from tkinter import messagebox, ttk


class ContactManager:
    def __init__(self, root):
        self.root = root
        self.root.title("Διαχείριση Επαφών")
        self.contacts = (
            self.load_contacts()
        )  # Φόρτωση των επαφών κατά την αρχικοποίηση.
        self.deleted_contacts = []
        self.create_widgets()
        self.root.eval(
            "tk::PlaceWindow . center"
        )  # Κεντραρισμα του παραθύρου στην οθόνη.

    def create_widgets(self):
        tree_frame = tk.Frame(self.root)
        tree_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        # Δημιουργία Treeview για την παρουσίαση των επαφών.
        self.tree = ttk.Treeview(
            tree_frame, columns=("lastname", "firstname", "phone"), show="headings"
        )
        self.tree.heading("lastname", text="Επώνυμο")
        self.tree.heading("firstname", text="Όνομα")
        self.tree.heading("phone", text="Τηλέφωνο")
        self.tree.column("lastname", width=120)
        self.tree.column("firstname", width=120)
        self.tree.column("phone", width=120)
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar = ttk.Scrollbar(
            tree_frame, orient=tk.VERTICAL, command=self.tree.yview
        )
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.configure(
            yscrollcommand=scrollbar.set
        )  # Σύνδεση του scrollbar με το treeview.

        btn_frame = tk.Frame(self.root)  # Πλαίσιο για τα κουμπιά.
        btn_frame.pack(side=tk.BOTTOM, fill=tk.X)

        # Κουμπιά για διαχείριση των επαφών.
        tk.Button(btn_frame, text="Προσθήκη Επαφής", command=self.add_contact).pack(
            side=tk.TOP, fill=tk.X
        )
        tk.Button(btn_frame, text="Επεξεργασία Επαφής", command=self.edit_contact).pack(
            side=tk.TOP, fill=tk.X
        )
        tk.Button(btn_frame, text="Διαγραφή Επαφής", command=self.delete_contact).pack(
            side=tk.TOP, fill=tk.X
        )
        tk.Button(
            btn_frame, text="Ανάκτηση Επαφής", command=self.show_retrieve_dialog
        ).pack(side=tk.TOP, fill=tk.X)
        tk.Button(btn_frame, text="Αποθήκευση Επαφών", command=self.save_contacts).pack(
            side=tk.TOP, fill=tk.X
        )

        self.display_contacts()  # Αρχική εμφάνιση επαφών καλώντας την display_contacts.

    def load_contacts(self):
        # Φόρτωση των επαφών από το CSV αρχείο.
        try:
            with open("contacts.csv", mode="r", newline="", encoding="utf-8") as file:
                reader = csv.reader(file)
                return sorted(
                    [tuple(contact) for contact in reader], key=lambda x: x[0].lower()
                )
        except FileNotFoundError:
            return []

    def save_contacts(self):
        # Αποθήκευση των επαφών στο CSV αρχείο.
        with open("contacts.csv", mode="w", newline="", encoding="utf-8") as file:
            writer = csv.writer(file)
            for contact in sorted(self.contacts, key=lambda x: x[0].lower()):
                writer.writerow(contact)
        messagebox.showinfo("Επιτυχία!", "Οι επαφές αποθηκεύτηκαν στο αρχείο.")

    def add_contact(self):
        self.create_dialog("Νέα Επαφή")

    def edit_contact(self):
        try:
            selected_item = self.tree.selection()[0]
            contact = self.tree.item(selected_item, "values")
            self.create_dialog("Επεξεργασία Επαφής", contact)
        except IndexError:
            messagebox.showerror("Σφάλμα!", "Δεν έχει επιλεγεί καμία επαφή")

    def create_dialog(self, title, contact=None):
        # Δημιουργία παραθύρου διαλόγου για εισαγωγή ή επεξεργασία επαφής.
        dialog = tk.Toplevel(self.root)
        dialog.title(title)
        dialog.geometry(
            "+{}+{}".format(self.root.winfo_x() + 50, self.root.winfo_y() + 50)
        )
        dialog.transient(self.root)
        dialog.grab_set()
        dialog.focus_set()

        tk.Label(dialog, text="Επώνυμο:").pack()
        last_name_entry = tk.Entry(dialog)
        last_name_entry.pack(fill=tk.X)
        last_name_entry.insert(0, contact[0] if contact else "")

        tk.Label(dialog, text="Όνομα:").pack()
        first_name_entry = tk.Entry(dialog)
        first_name_entry.pack(fill=tk.X)
        first_name_entry.insert(0, contact[1] if contact else "")

        tk.Label(dialog, text="Τηλέφωνο:").pack()
        phone_number_entry = tk.Entry(dialog)
        phone_number_entry.pack(fill=tk.X)
        phone_number_entry.insert(0, contact[2] if contact else "")

        def save_contact():
            # Αποθήκευση της νέας ή της τροποποιημένης επαφής.
            last_name = last_name_entry.get()
            first_name = first_name_entry.get()
            phone_number = phone_number_entry.get()
            if last_name and first_name and phone_number:
                new_contact = (last_name, first_name, phone_number)
                if contact:
                    idx = self.contacts.index(contact)
                    self.contacts[idx] = new_contact
                else:
                    self.contacts.append(new_contact)
                self.display_contacts()
                dialog.destroy()
            else:
                messagebox.showerror(
                    "Σφάλμα!",
                    "Παρακαλώ εισάγετε όλες τις λεπτομέρειες της επαφής",
                    parent=dialog,
                )

        tk.Button(dialog, text="Αποθήκευση", command=save_contact).pack()

    def display_contacts(self):
        # Εμφάνιση επαφών ταξινομημένες αλφαβητικά κατά επώνυμο.
        self.tree.delete(*self.tree.get_children())
        for contact in sorted(self.contacts, key=lambda x: x[0].lower()):
            self.tree.insert("", tk.END, values=contact)

    def delete_contact(self):
        # Διαγραφή της επιλεγμένης επαφής.
        try:
            selected_item = self.tree.selection()[0]
            contact = self.tree.item(selected_item, "values")
            self.deleted_contacts.append(contact)
            self.contacts.remove(contact)
            self.tree.delete(selected_item)
        except IndexError:
            messagebox.showerror("Σφάλμα!", "Δεν έχει επιλεγεί καμία επαφή")

    def show_retrieve_dialog(self):
        # Διάλογος ανάκτησης διαγραμμένων επαφών.
        if not self.deleted_contacts:
            messagebox.showinfo(
                "Πληροφορία", "Δεν υπάρχουν διαγραμμένες επαφές για ανάκτηση."
            )
            return

        retrieve_window = tk.Toplevel(self.root)
        retrieve_window.title("Ανάκτηση Επαφής")
        retrieve_window.geometry(
            "300x300+450+450"
        )  # Θέση του παραθύρου, ώστε να μην συμπίπτει με το κεντρικό.

        list_frame = tk.Frame(retrieve_window)
        list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        listbox = tk.Listbox(list_frame)
        listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=listbox.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        listbox.configure(yscrollcommand=scrollbar.set)

        for idx, contact in enumerate(self.deleted_contacts):
            listbox.insert(tk.END, f"{idx+1}. {contact[0]} {contact[1]} - {contact[2]}")

        def retrieve_selected_contact():
            # Ανάκτηση της επιλεγμένης διαγραμμένης επαφής.
            selected_idx = listbox.curselection()
            if selected_idx:
                selected_contact = self.deleted_contacts.pop(selected_idx[0])
                self.contacts.append(selected_contact)
                self.display_contacts()
                retrieve_window.destroy()
            else:
                messagebox.showerror("Προσοχή!", "Δεν έχετε επιλέξει καμία επαφή")

        retrieve_button = tk.Button(
            retrieve_window,
            text="Ανάκτηση Επιλεγμένης Επαφής",
            command=retrieve_selected_contact,
        )
        retrieve_button.pack(side=tk.BOTTOM, fill=tk.X)


if __name__ == "__main__":
    root = tk.Tk()
    app = ContactManager(root)
    root.mainloop()
Παράδειγμα εκτέλεσης:
$ python 5_2_sol.py
CRUD

Άσκηση E5A3 - Δημιουργήστε ένα πρόγραμμα που να απεικονίζει σε ένα γράφημα τις θερμοκρασίες για τις 5 τελευταίες ημέρες στην Άρτα [39.1606, 20.9853]. Χρησιμοποιήστε το module matplotlib για τη σχεδίαση του γραφήματος και για τη λήψη των θερμοκρασιών το OpenWeathermap API (προσοχή πρέπει να λάβετε API key και να το συμπληρώσετε μέσα στον κώδικα).

Λύση άσκησης E5A3

5_3_sol.py
import calendar
import datetime
from tkinter import *

import requests
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure


def plot():
    api_key = "ΣΥΜΠΛΗΡΩΣΤΕ ΤΟ ΔΙΚΟ ΣΑΣ API ΚΛΕΙΔΙ ΕΔΩ"

    lat = latitude_entry.get()
    lon = longitude_entry.get()

    date_time = datetime.datetime.now() - datetime.timedelta(days=5)
    utc_time = calendar.timegm(date_time.utctimetuple())

    timezone = ""
    temperatures = []
    dates = []

    for _ in range(5):
        temp_date = str(date_time).split(" ")[0]
        temp_date = temp_date.split("-")
        dates.append(temp_date[2] + "/" + temp_date[1] + "/" + temp_date[0])

        weather_url = f"https://api.openweathermap.org/data/2.5/onecall/timemachine?lat={lat}&lon={lon}&units=metric&dt={utc_time}&appid={api_key}"
        response = requests.get(weather_url)
        weather_data = response.json()

        if timezone == "":
            timezone = weather_data["timezone"]

        for i in range(24):
            temp = int(weather_data["hourly"][i]["temp"])
            temperatures.append(temp)

        date_time = date_time + datetime.timedelta(days=1)
        utc_time = calendar.timegm(date_time.utctimetuple())

    temp_date = str(date_time).split(" ")[0]
    temp_date = temp_date.split("-")
    dates.append(temp_date[2] + "/" + temp_date[1] + "/" + temp_date[0])

    fig = Figure(figsize=(10, 5), dpi=100)
    plot1 = fig.add_subplot(111)
    plot1.plot(temperatures)
    plot1.set_title(f"Θερμοκρασία στις γεωγραφικές συντεταγμένες {lat=} {lon=}")
    plot1.set_ylabel("Θερμοκρασία (°C)")
    plot1.set_xticks([0, 24, 48, 72, 96, 120])
    plot1.set_xticklabels(dates)

    canvas = FigureCanvasTkAgg(fig, master=plot_frame)
    canvas.draw()
    canvas.get_tk_widget().pack()
    toolbar = NavigationToolbar2Tk(canvas, plot_frame)
    toolbar.update()
    canvas.get_tk_widget().pack()


window = Tk()
window.title("Διάγραμμα θερμοκρασιών για τις 5 τελευταίες ημέρες")
window.geometry("1000x500")

frame = Frame(window)
frame.pack()

plot_frame = Frame(window)
plot_frame.pack(side=BOTTOM)

Label(frame, text="Γεωγραφικό πλάτος (latitude)").grid(row=0, sticky="w")
Label(frame, text="Γεωγραφικό μήκος (longitude)").grid(row=1, sticky="w")

default_lat = StringVar(window, value="39.1606")
default_lon = StringVar(window, value="20.9853")

latitude_entry = Entry(frame, textvariable=default_lat)
longitude_entry = Entry(frame, textvariable=default_lon)

latitude_entry.grid(row=0, column=1, sticky="w")
longitude_entry.grid(row=1, column=1, sticky="w")

plot_button = Button(master=frame, command=plot, height=2, width=10, text="Plot")
plot_button.grid(row=2, column=0, columnspan=2, sticky="news")

if __name__ == "__main__":
    window.mainloop()
Παράδειγμα εκτέλεσης:
$ python 5_3_sol.py
Weather -5 days

Άσκηση E5A4 - Χρησιμοποιήστε το pattern MVC έτσι ώστε να αναπτύξετε μια εφαρμογή που να πραγματοποιεί πράξεις πρόσθεσης, αφαίρεσης, πολλαπλασιασμού και διαίρεσης με μιγαδικούς αριθμούς. Στο ρόλο του view να μπορεί να εναλλάσσεται γραφικό περιβάλλον (GUI=Graphical User Interface) και περιβάλλον κειμένου (TUI=Text User Interface).

Λύση άσκησης E5A4

5_4_sol.py
# Λύση από τον φοιτητή Γιάννη Ροδουσάκη
# του Τμήματος Πληροφορικής και Τηλεπικοινωνιών
# του Πανεπιστημίου Ιωαννίνων τον Απρίλιο του 2022


import sys
import tkinter as tk
from tkinter import messagebox


class Complex:
    def __init__(self, real: float, imag: float):
        self.real = real
        self.imag = imag

    # Υπερφόρτωση τελεστών για πράξεις μιγαδικών αριθμών
    def __add__(self, other_complex: "Complex"):
        real_new = self.real + other_complex.real
        imag_new = self.imag + other_complex.imag
        return Complex(real_new, imag_new)

    def __sub__(self, other_complex: "Complex"):
        real_new = self.real - other_complex.real
        imag_new = self.imag - other_complex.imag
        return Complex(real_new, imag_new)

    def __mul__(self, other_complex: "Complex"):
        real_new = self.real * other_complex.real - self.imag * other_complex.imag
        imag_new = self.imag * other_complex.real + self.real * other_complex.imag
        return Complex(real_new, imag_new)

    def __truediv__(self, other_complex: "Complex"):
        temp_real = self.real * other_complex.real + self.imag * other_complex.imag
        temp_imag = other_complex.real * self.imag - self.real * other_complex.imag
        denum = other_complex.real**2 + other_complex.imag**2
        real_new = temp_real / denum
        imag_new = temp_imag / denum
        return Complex(real_new, imag_new)

    def __str__(self):
        return f"({self.real}, {self.imag}i)"


class ViewGui(tk.Frame):
    def __init__(self, parent):
        super().__init__(parent)

        # 1η γραμμή -> Εισαγωγή πρώτου μιγαδικού αριθμού
        self.real1_label = tk.Label(self, text="Real 1: ")
        self.real1_label.grid(row=0, column=0)
        self.real1_var = tk.DoubleVar()
        self.real1_entry = tk.Entry(self, textvariable=self.real1_var, justify="right")
        self.real1_entry.grid(row=0, column=1)
        self.imag1_label = tk.Label(self, text="Imaginary 1: ")
        self.imag1_label.grid(row=0, column=2)
        self.imag1_var = tk.DoubleVar()
        self.imag1_entry = tk.Entry(self, textvariable=self.imag1_var, justify="right")
        self.imag1_entry.grid(row=0, column=3)

        # 2η γραμμή -> Εισαγωγή δεύτερου μιγαδικού αριθμού
        self.real2_label = tk.Label(self, text="Real 2: ")
        self.real2_label.grid(row=1, column=0)
        self.real2_var = tk.DoubleVar()
        self.real2_entry = tk.Entry(self, textvariable=self.real2_var, justify="right")
        self.real2_entry.grid(row=1, column=1)
        self.imag2_label = tk.Label(self, text="Imaginary 2: ")
        self.imag2_label.grid(row=1, column=2)
        self.imag2_var = tk.DoubleVar()
        self.imag2_entry = tk.Entry(self, textvariable=self.imag2_var, justify="right")
        self.imag2_entry.grid(row=1, column=3)

        # 3η γραμμή -> Κουμπιά εκτέλεσης πράξεων μιγαδικών αριθμών
        self.add_button = tk.Button(self, text="Add", command=self.add_clicked)
        self.add_button.grid(row=2, column=0, sticky=tk.W)
        self.substract_button = tk.Button(
            self, text="Substract", command=self.substract_clicked
        )
        self.substract_button.grid(row=2, column=1, sticky=tk.W)
        self.multiply_button = tk.Button(
            self, text="Multiply", command=self.multiply_clicked
        )
        self.multiply_button.grid(row=2, column=2, sticky=tk.W)
        self.divide_button = tk.Button(self, text="Divide", command=self.divide_clicked)
        self.divide_button.grid(row=2, column=3, sticky=tk.W)

        # 4η γραμμή -> Εμφάνιση αποτελέσματος πράξης μιγαδικών αριθμών
        self.real_result_label = tk.Label(self, text="")
        self.real_result_label.grid(row=3, column=0)
        self.imag_result_label = tk.Label(self, text="")
        self.imag_result_label.grid(row=3, column=1)

        self.controller = None

    # Συνάρτηση ορισμού controller
    def set_controller(self, controller):
        self.controller = controller

    # Συνάρτηση χειρισμού εισαγωγών
    def handle_input_event(self):
        self.complex1 = Complex(
            float(self.real1_entry.get()), float(self.imag1_entry.get())
        )
        self.complex2 = Complex(
            float(self.real2_entry.get()), float(self.imag2_entry.get())
        )

    # Συνάρτηση ενημέρωσης label αποτελέσματος μιγαδικού αριθμού
    def show_result(self, complex_result: Complex):
        # Μορφοποίηση αποτελέσματος για εμφάνιση 2 δεκαδικών ψηφίων
        complex_result.real = "{:.3f}".format(complex_result.real)
        complex_result.imag = "{:.3f}".format(complex_result.imag)
        # Ανανέωση label για εμφάνιση αποτελέσματος
        self.real_result_label["text"] = "Real Result: {}".format(
            str(complex_result.real)
        )
        self.imag_result_label["text"] = "Imaginary Result: {}".format(
            str(complex_result.imag)
        )

    # Συναρτήσεις χειρισμού event πράξεων μιγαδικών αριθμών
    def add_clicked(self):
        self.handle_input_event()
        if self.controller:
            self.controller.add(self.complex1, self.complex2)

    def substract_clicked(self):
        self.handle_input_event()
        if self.controller:
            self.controller.substract(self.complex1, self.complex2)

    def multiply_clicked(self):
        self.handle_input_event()
        if self.controller:
            self.controller.multiply(self.complex1, self.complex2)

    def divide_clicked(self):
        self.handle_input_event()
        if self.controller:
            self.controller.divide(self.complex1, self.complex2)


class ViewTui:
    def __init__(self):
        print("~" * 82)
        print("~" * 30 + "  Complex Calculator  " + "~" * 30)
        print("~" * 82 + "\n")

        print("Choose operation::")
        self.operation_selection = int(
            input("1.Addition  2.Substraction  3.Multiplication  4.Division \n5.Quit\n")
        )

        self.controller = Controller(Complex(1, 2), self)

        if self.operation_selection == 1:
            # check which function needs to be called
            self.add_clicked()
        elif self.operation_selection == 2:
            self.substract_clicked()
        elif self.operation_selection == 3:
            self.multiply_clicked()
        elif self.operation_selection == 4:
            self.divide_clicked()
        elif self.operation_selection == 5:
            print("Quitting ...")
        else:
            print("Wrong Input!!!")

    # Συνάρτηση χειρισμού εισαγωγής μιγαδικών αριθμών
    def handle_input_event(self):
        print("Input of 1st complex number:")
        print("Give real part of 1st complex number: ")
        self.real1 = float(input())
        print("Give imaginary part of 1st complex number: ")
        self.imag1 = float(input())

        print("Input of 2nd complex number:")
        print("Give real part of 2nd complex number: ")
        self.real2 = float(input())
        print("Give imaginary part of 2nd complex number: ")
        self.imag2 = float(input())

    def handle_modelling_stage(self):
        self.complex1 = Complex(self.real1, self.imag1)
        self.complex2 = Complex(self.real2, self.imag2)

    def show_result(self, complex_result: Complex):
        print(
            "Real Result: {:.3f}, Imaginary Result: {:.3f}".format(
                complex_result.real, complex_result.imag
            )
        )

    # Συναρτήσεις χειρισμού event πράξεων μιγαδικών αριθμών
    def add_clicked(self):
        self.handle_input_event()
        self.handle_modelling_stage()
        self.controller.add(self.complex1, self.complex2)

    def substract_clicked(self):
        self.handle_input_event()
        self.handle_modelling_stage()
        self.controller.substract(self.complex1, self.complex2)

    def multiply_clicked(self):
        self.handle_input_event()
        self.handle_modelling_stage()

        self.controller.multiply(self.complex1, self.complex2)

    def divide_clicked(self):
        self.handle_input_event()
        self.handle_modelling_stage()

        self.controller.divide(self.complex1, self.complex2)


class Controller:
    def __init__(self, model: Complex = None, view=None):
        self.model = model
        self.view = view

    # Συναρτήσεις εκτέλεσης πράξεων μιγαδικών αριθμών
    def add(self, complex1: Complex, complex2: Complex):
        self.model = complex1
        complex_result = self.model + complex2
        self.view.show_result(complex_result)

    def substract(self, complex1: Complex, complex2: Complex):
        self.model = complex1
        complex_result = self.model - complex2
        self.view.show_result(complex_result)

    def multiply(self, complex1: Complex, complex2: Complex):
        self.model = complex1
        complex_result = self.model * complex2
        self.view.show_result(complex_result)

    def divide(self, complex1: Complex, complex2: Complex):
        self.model = complex1

        # try catch, για GUI διεπαφή
        if sys.argv[1].upper() == "GUI":
            try:
                complex_result = self.model / complex2
                self.view.show_result(complex_result)
            except ZeroDivisionError:
                messagebox.showwarning(
                    title="Division Error",
                    message=" Can't divide by zero!!! \n Please input valid numbers!",
                )

        # try catch, για TUI διεπαφή
        if sys.argv[1].upper() == "TUI":
            try:
                complex_result = self.model / complex2
                self.view.show_result(complex_result)
            except ZeroDivisionError:
                print(" Can't divide by zero!!! \n Please input valid numbers!")


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Complex Calculator")
        # Δημιουργία μοντέλου
        model = Complex(1, 2)
        # Δημιουργία γραφικού περιβάλλοντος και τοποθέτηση στην οθόνη
        view_gui = ViewGui(self)
        view_gui.grid(row=0, column=0, padx=10, pady=10)
        # Δημιουργία controller
        controller_gui = Controller(model, view_gui)
        # Ορισμός του controller στο γραφικό περιβάλλον
        view_gui.set_controller(controller_gui)


if __name__ == "__main__":
    # try catch block, για αποφυγή IndexError exception, αν ο χρήστης δεν εισάγει τύπο διεπαφής
    try:
        if sys.argv[1].upper() == "GUI":
            app = App()
            app.mainloop()
        elif sys.argv[1].upper() == "TUI":
            while True:
                view = ViewTui()
                if view.operation_selection == 5:
                    break

        # Δημιουργία exception αν ο χρήστης εισάγει λάθος τύπο διεπαφής
        if sys.argv[1].upper() not in ["GUI", "TUI"]:
            raise Exception("Wrong interface command line parameter")
    except IndexError:
        print("Input interface type, as command line parameter")
Παράδειγμα εκτέλεσης για TUI:
$ python 5_4_sol.py TUI
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  Complex Calculator  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Choose operation::
1.Addition  2.Substraction  3.Multiplication  4.Division
5.Quit
3
Input of 1st complex number:
Give real part of 1st complex number:
2
Give imaginary part of 1st complex number:
4
Input of 2nd complex number:
Give real part of 2nd complex number:
3
Give imaginary part of 2nd complex number:
5
Real Result: -14.000, Imaginary Result: 22.000
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  Complex Calculator  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Choose operation::
1.Addition  2.Substraction  3.Multiplication  4.Division
5.Quit
5
Quitting ...

Παράδειγμα εκτέλεσης για GUI:

$ python 5_4_sol.py GUI
Calculator GUI

Επιπλέον εξάσκηση

Άσκηση E5A5 - Υλοποιήστε με το tkinter την 1η εργασία (Counter) από την ιστοσελίδα 7GUIs. Δηλαδή, δημιουργήστε ένα παράθυρο όπως το ακόλουθο και κάθε φορά που ο χρήστης πατά το πλήκτρο Count η τιμή στο πεδίο κειμένου που κατά την εκκίνηση έχει τιμή 0 να αυξάνεται κατά 1.

Counter

Λύση άσκησης E5A5

e5a5.py
import tkinter as tk


def increment():
    x = int(count_label["text"])
    x += 1
    count_label["text"] = str(x)


root = tk.Tk()
root.geometry("200x30")
root.title("Counter")
count_label = tk.Label(root, text="0", width=10, background="white")
count_label.pack(side=tk.LEFT)
increment_button = tk.Button(root, text="Count", width=10)
increment_button.pack(side=tk.RIGHT)
increment_button.config(command=increment)

root.eval("tk::PlaceWindow . center")
root.mainloop()
Παράδειγμα εκτέλεσης:
$ python e5a5.py
7GUIs task 1

Άσκηση Ε5Α6 - Υλοποιήστε με το tkinter τη 2η εργασία (Temperature Converter) από την ιστοσελίδα 7GUIs. Δηλαδή, δημιουργήστε ένα παράθυρο όπως το ακόλουθο:

Temperature converter

και κάθε φορά που ο χρήστης εισάγει θερμοκρασία είτε σε βαθμούς Κελσίου είτε σε βαθμούς Φάρεναϊτ να ενημερώνεται και η θερμοκρασία στην άλλη μονάδα μέτρησης. Εντοπίστε πληροφορίες για την StringVar4 για την trace_add5.

Λύση άσκησης E5A6

e5a6.py
import tkinter as tk

root = tk.Tk()
celsius_var = tk.StringVar()
celsius_var.set("0.0")
fahr_var = tk.StringVar()
fahr_var.set("32.0")


def celcius_callback(*args):
    if root.focus_get() != celsius_entry:
        return
    try:
        celcius_temperature = celsius_var.get()
        c = float(celcius_temperature)
        f = c * (9 / 5) + 32
        celsius_entry.config(background="white")
        fahr_var.set(f"{f:.1f}")
    except ValueError:
        celsius_entry.config(background="pink")


def fahr_callback(*args):
    if root.focus_get() != fahr_entry:
        return
    try:
        fahr_temperature = fahr_var.get()
        f = float(fahr_temperature)
        c = (f - 32) * 5 / 9
        fahr_entry.config(background="white")
        celsius_var.set(f"{c:.1f}")
    except ValueError:
        fahr_entry.config(background="pink")


root.geometry("350x30")
root.title("TempConv")

celsius_var.trace_add("write", celcius_callback)
fahr_var.trace_add("write", fahr_callback)
celsius_entry = tk.Entry(root, width=10, textvariable=celsius_var)
celsius_entry.grid(row=0, column=0)

label1 = tk.Label(root, text="Celsius = ")
label1.grid(row=0, column=1)
fahr_entry = tk.Entry(root, width=10, textvariable=fahr_var)
fahr_entry.grid(row=0, column=2)
label2 = tk.Label(root, text="Fahrenheit")
label2.grid(row=0, column=3)

root.eval("tk::PlaceWindow . center")
root.mainloop()
Παράδειγμα εκτέλεσης:
$ python e5a6.py
7GUIs task 2

Άσκηση Ε5Α7 - Υλοποιήστε με το tkinter την 3η εργασία (Book Flight) από την ιστοσελίδα 7GUIs. Δηλαδή, δημιουργήστε ένα παράθυρο που να συμπεριφέρεται όπως τα ακόλουθο:

Book Flight

Λύση άσκησης E5A7

e5a7.py
# Λύση από τον φοιτητή Μπακογιάννη Θωμά
# του Τμήματος Πληροφορικής και Τηλεπικοινωνιών
# του Πανεπιστημίου Ιωαννίνων τον Απρίλιο του 2024

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
from datetime import datetime

class Application(tk.Tk):
    def __init__(self):
        super().__init__()

        # Set up main window
        self.title("Book Flight")
        self.geometry("300x120")
        for x in range(4):
            self.rowconfigure(x, weight=1)
        self.columnconfigure(0, weight=1)

        # Set up combo box
        options = ["one-way flight", "return flight"]
        self.flight_options = ttk.Combobox(self, values=options, state="readonly")
        self.flight_options.set("one-way flight")
        self.flight_options.bind("<<ComboboxSelected>>", self.on_combobox_select)
        self.flight_options.grid(row=0, sticky="nesw", padx=3, pady=3)

        # Set up start date entry
        self.entry_val1 = tk.StringVar()
        self.entry_val1.trace_add("write", self.validate_date)
        self.start_date = tk.Entry(self, textvariable=self.entry_val1)
        self.start_date.grid(row=1, sticky="nesw", padx=3, pady=3)

        # Set up return date entry
        self.entry_val2 = tk.StringVar()
        self.entry_val2.trace_add("write", self.validate_date)
        self.return_date = tk.Entry(self, textvariable=self.entry_val2)
        self.return_date["state"] = "disabled"
        self.return_date.grid(row=2, sticky="nesw", padx=4, pady=4)

        # Set up book button
        self.book_button = tk.Button(self, text="Book", command=self.display_message, state="disabled", foreground="gray")
        self.book_button.grid(row=3, sticky="nesw", padx=5, pady=5)

    def check_box1(self):
        try:
            datetime.strptime(self.start_date.get(), '%d.%m.%Y')
            return True
        except ValueError:
            return False

    def check_box2(self):
        try:
            datetime.strptime(self.return_date.get(), '%d.%m.%Y')
            return True
        except ValueError:
            return False

    def on_combobox_select(self, event):
        # Extra checks for boxes
        if self.flight_options.get() == "one-way flight":
            self.return_date["state"] = "disabled"
            if self.check_box1():
                self.book_button.configure(state="normal", foreground="black")
            else:
                self.book_button.configure(state="disabled", foreground="gray")
        else:
            self.return_date["state"] = "normal"
            if self.check_box1() and self.check_box2():
                self.book_button.configure(state="normal", foreground="black")
            else:
                self.book_button.configure(state="disabled", foreground="gray")

    def validate_date(self, *args):
        focused_widget = self.focus_get()

        # Validate date entries
        if focused_widget in [self.start_date, self.return_date]:
            if not focused_widget.get():
                focused_widget["bg"] = "white"
                self.book_button.configure(state="disabled", foreground="gray")
            else:
                try:
                    datetime.strptime(focused_widget.get(), '%d.%m.%Y')
                    focused_widget["bg"] = "white"
                    self.book_button.configure(state="normal", foreground="black")
                except ValueError:
                    focused_widget["bg"] = "red"
                    self.book_button.configure(state="disabled", foreground="gray")

        # Compare dates if return flight is selected
        if self.flight_options.get() == "return flight":
            self.compare_dates()

    def compare_dates(self):
        date_str1 = self.start_date.get()
        date_str2 = self.return_date.get()

        # Compare start and return dates
        try:
            date1 = datetime.strptime(date_str1, "%d.%m.%Y")
            date2 = datetime.strptime(date_str2, "%d.%m.%Y")
            if date2 < date1:
                self.book_button.configure(state="disabled", fg="gray")
            else:
                self.book_button.configure(state="normal", fg="black")
        except ValueError:
            self.book_button.configure(state="disabled", fg="gray")

    def display_message(self):
        # Display booking information
        if self.flight_options.get() == "one-way flight":
            messagebox.showinfo("Booking information", f"You have booked a one-way flight for {self.start_date.get()}")
        else:
            messagebox.showinfo("Booking information", f"You have booked a return flight departing on {self.start_date.get()} and returning on {self.return_date.get()}")

if __name__ == "__main__":
    app = Application()
    app.mainloop()

Άσκηση Ε5Α8 - Υλοποιήστε με το tkinter την 4η εργασία (Timer) από την ιστοσελίδα 7GUIs. Δημιουργήστε ένα παράθυρο όπως το ακόλουθο:

Timer

Λύση άσκησης E5A8

e5a8.py
# Λύση από τον φοιτητή Μπακογιάννη Θωμά
# του Τμήματος Πληροφορικής και Τηλεπικοινωνιών
# του Πανεπιστημίου Ιωαννίνων τον Απρίλιο του 2024

import tkinter as tk
from tkinter import ttk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()

        # Setting up mainwindow
        self.title("Timer")
        self.geometry("300x150")
        for x in range(4):
            self.grid_rowconfigure(x, weight=1)
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=3)

        self.et_label = ttk.Label(self, text="Elapsed Time:")
        self.et_label.grid(row=0, column=0)

        self.et_time = ttk.Label(self, text="0")
        self.et_time.grid(row=1, column=0)

        self.progress_bar = ttk.Progressbar(self, orient="horizontal", length=200, mode="determinate")
        self.progress_bar.grid(row=0, column=1, padx=3, pady=10)

        self.d_label = ttk.Label(self, text="Duration:")
        self.d_label.grid(row=2, column=0)

        self.d_time = 10
        self.elapsed_ms = 0
        self.timer_id = None # Initialize variable for the id of the timer
        self.d_slider = ttk.Scale(self, from_=0, to=100, orient="horizontal", command=self.update_duration)
        self.d_slider.set(10)
        self.d_slider.grid(row=2, column=1, sticky="nswe", padx=10)

        self.reset_button = ttk.Button(self, text="Reset", command=self.reset)
        self.reset_button.grid(row=3, column=0, columnspan=2, sticky="nswe", padx=10, pady=10)

        # Start the timer
        self.timer()

    # Main timer function
    def timer(self): 
        self.et_time['text'] = "{:.1f}".format(self.elapsed_ms/1000) + "s"  # Display elapsed time with one decimal place
        diff =  self.d_time - (self.elapsed_ms/1000)

        if not (self.elapsed_ms/1000) >= self.d_time:
            self.progress_bar["value"] = ((self.d_time - diff) / self.d_time) * 100 if self.d_time else 0
            self.elapsed_ms += 1
            # Run every 1 ms
            self.timer_id = self.after(1, self.timer)
        else:
            self.progress_bar["value"] = 100

    # Reset the timer
    def reset(self):
        self.elapsed_ms = 0
        self.after_cancel(self.timer_id) # Stop all instances of timer
        self.timer()

    # Update the duration based on the slider
    def update_duration(self, value):
        self.d_time = float(value)
        if self.d_time > self.elapsed_ms/1000 and self.progress_bar["value"] == 100:
            self.timer()

if __name__ == "__main__":
    app = Application()
    app.mainloop()