GUI do programu CRUD cz.3

Poniżej opisałem kod klasy NewCarWindow opisującej nowe okno potomne, tworzone gdy kliknięto przycisk add_car_button:

import tkinter as tk
from sql_helper import Helper


class NewCarWindow():
    def __init__(self, root, parent):
        self.top_level = tk.Toplevel(root)
        self.helper = Helper()
        self.parent = parent
        x = self.top_level.winfo_screenwidth()
        y = self.top_level.winfo_screenheight()
        geometry = '+{}+{}'.format(int((x / 2) - 100),
                                   int((y / 2) - 100))
        size = self.top_level.geometry(geometry)

Najpierw importuję wymagane klasy, następnie w metodzie __init__() ustalane jest położenie okna potomnego względem okna głównego.

        self.top_level.title('Add car')
        self.top_level.grab_set()
        label_make = tk.Label(self.top_level, text='Make: ',
                              font=12, padx=20, pady=10).grid(row=0, column=0)
        label_model = tk.Label(self.top_level, text='Model: ',
                               font=12, padx=20, pady=10).grid(row=1, column=0)
        label_year = tk.Label(self.top_level, text='Year: ',
                              font=12, padx=20, pady=10).grid(row=2, column=0)
        label_vrn = tk.Label(self.top_level, text='VRN: ',
                             font=12, padx=20, pady=10).grid(row=3, column=0)
        label_vin = tk.Label(self.top_level, text='VIN: ',
                             font=12, padx=20, pady=10).grid(row=4, column=0)

Następnie ustawiany jest tytuł okna potomnego. Metoda grab_set() powoduje, że okno główne jest nieaktywne – nie można np. wcisnąć żadnego przycisku itp. Kolejnym krokiem jest ustawienie etykiet tekstowych opisujących pola wprowadzania, które definiuję poniżej, wraz z ze zmiennymi typu StringVar, przechowującymi wartości wprowadzane do tych pól:

    self.make_sv = tk.StringVar()
    self.model_sv = tk.StringVar()
    self.year_sv = tk.StringVar()
    self.vrn_sv = tk.StringVar()
    self.vin_sv = tk.StringVar()
    self.info_sv = tk.StringVar()

    entry_make = tk.Entry(
            self.top_level, text=self.make_sv)
    entry_make.focus_set()
    entry_make.grid(row=0, column=1)
    entry_model = tk.Entry(
            self.top_level, text=self.model_sv).grid(row=1, column=1)
    entry_year = tk.Entry(
            self.top_level, text=self.year_sv).grid(row=2, column=1)
    entry_vrn = tk.Entry(
            self.top_level, text=self.vrn_sv).grid(row=3, column=1)
    entry_vin = tk.Entry(
            self.top_level, text=self.vin_sv).grid(row=4, column=1)
    info_label = tk.Label(self.top_level, textvariable=self.info_sv,
                              font=12, padx=10, pady=10, fg='red').grid(row=5, column=0, columnspan=2)

Etykieta info_label służy do wyświetlenia komunikatu w razie nie wypełnienia wszystkich wymaganych pól do utworzenia nowego auta.

        save_button = tk.Button(self.top_level, text='Save',
                                command=self.save_new_car)
        save_button.bind('<Return>', self.save_new_car)
        save_button.grid(row=6, column=1, sticky='W', padx=10)
        cancel_button = tk.Button(self.top_level, text='Cancel',
                                  command=self.top_level.destroy)
        cancel_button.grid(row=6, column=1, sticky='W', padx=70)
        cancel_button.bind('<Return>', self.top_level.destroy)

Na dole okna wyświetlone zostają przyciski Save oraz Cancel do zapisania danych o nowym aucie w bazie danych lub zamknięcia okna ‘Add car’.

def save_new_car(self, event=None):
    if self.make_sv.get() and self.model_sv.get() and self.year_sv.get() and self.vrn_sv.get() and self.vin_sv.get():
        self.helper.add_car(self.make_sv.get(),
                            self.model_sv.get(),
                            self.year_sv.get(),
                            self.vrn_sv.get(),
                            self.vin_sv.get())
        self.top_level.destroy()
        self.parent.show_cars()
    else:
        self.info_sv.set('Please fill in all entry fields')

Kod metody save_new_car() jest aktywowany, jeśli naciśnięto przycisk Save. Jeśli wypełnione zostały wszystkie wartości potrzebne do utworzenia nowego auta to zostaje uruchomiona metoda add_car() klasy Helper, a następnie zamykane jest okno potomne i aktualizowane są wpisy o autach w oknie głównym za pomocą metody show_cars() klasy CarManager.

Okno potomne ‘Add car’. Komunikat wyświetla się, gdy nie wypełnione wszystkie pola wprowadzania. Focus ustawiony na pierwszym elemencie.

***

Kod klasy RepairsWindow, aktywowany po naciśnięciu przycisku repairs_button okna głównego:

import tkinter as tk
from tkinter import ttk
from sql_helper import Helper
from date_picker import DatePicker


class RepairsWindow():
    def __init__(self, root, car):
        self.top_level = tk.Toplevel(root)
        self.root = root
        self.top_level.title('Repairs')
        self.top_level.grab_set()
        self.car = car
        self.helper = Helper()

        x = self.top_level.winfo_screenwidth()
        y = self.top_level.winfo_screenheight()
        geometry = '+{}+{}'.format(int((x / 2) - 150),
                                   int((y / 2) - 150))
        size = self.top_level.geometry(geometry)

Na początku importuję wszystkie niezbędne klasy. W metodzie __init__() tworzone jest nowe okno – egzemplarz klasy TopLevel, ustawiany jest tytuł okna na ‘Repairs’. Następnie za pomocą metody grab_set() blokowana jest możliwość uruchamiania widżetów z okna głównego. Ustawiane są zmienne self.car i self.helper, a następnie dokonywane jest pozycjonowanie okna potomnego względem okna głównego.

        toolbox_frame = tk.Frame(self.top_level)
        toolbox_frame.grid(column=0, row=0, sticky='W')
        self.add_repair_img = tk.PhotoImage(file='Resources/add_repair.gif')
        add_repair_button = tk.Button(
            toolbox_frame, image=self.add_repair_img, command=self.add_repair)
        add_repair_button.grid(column=0, row=0, sticky='W')
        add_repair_button.bind('<Return>', self.add_repair)
        add_repair_button.bind('<KP_Enter>', self.add_repair)
        if car.sold:
            add_repair_button.config(state='disabled')

W oknie potomnym tworzona jest ramka toolbox_frame, w której umieszczony zostaje przycisk add_repair_button. Do utworzonego przycisku zostaje dowiązana metoda add_repair() – aktywowana za pomocą myszki lub klawiatury po naciśnięciu Enter na klawiaturze numerycznej lub głównej. Sprawdzane jest następnie, czy auto nie jest oznaczone jako ‘sprzedane’, wtedy przycisk umożliwiający dodawanie nowych napraw jest nieaktywny.

        col_headers = ('No', 'Date', 'Description')
        self.repairs_tv = ttk.Treeview(self.top_level, columns=col_headers,
                                       show='headings', selectmode='none')
        self.repairs_tv.tag_configure('c1', background='ivory2')
        self.repairs_tv.tag_configure('c2', background='ivory3')
        for i, col in enumerate(col_headers):
            self.repairs_tv.heading(col, text=col)
            self.repairs_tv.column(col, anchor='center')
            if i == 0:
                self.repairs_tv.column(col, width=50, stretch='NO')
        self.repairs_tv.grid(column=0, row=2,  sticky='NSWE')

        scrollbar = tk.Scrollbar(self.top_level, command=self.repairs_tv.yview)
        scrollbar.grid(column=1, row=2, sticky='NS')
        
        self.show_repairs()

Kolejny fragment kodu opisuje utworzenie widżetu Treeview, w którym będą wyświetlane dane o naprawach. Dla poprawienia czytelności utworzyłem dwa tagi, które ustawiają kolory tła wpisywanych elementów widżetu. Metoda __init__() kończy się wywołaniem metody show_repairs(), która dodaje elementy o naprawach do widżetu Treeview.

    def show_repairs(self):
        repairs = self.helper.show_repairs(self.car)
        self.repairs_tv.delete(*self.repairs_tv.get_children())
        for i, repair in enumerate(repairs, start=1):
            repair = (i, repair[0], repair[2])
            if i % 2:
                self.repairs_tv.insert('', 'end', values=repair, tag='c1')
            else:
                self.repairs_tv.insert('', 'end', values=repair, tag='c2')

W metodzie show_repairs() wywoływana jest odpowiednia metoda klasy Helper, która pobiera dane o naprawach z bazy danych. Następnie kasowane są elementy repairs_tv i ustawiane są zaktualizowane dane. Dla poprawienia czytelności kolejne wiersze są naprzemiennie kolorowane tzn.

Kod metody add_repair, który jest aktywowany, gdy naciśnięto przycisk add_repair_button za pomocą myszki lub z klawiatury:

    def add_repair(self, event=None):
        self.add_repair_frame = tk.Frame(self.top_level)
        self.add_repair_frame.grid(
            column=0, row=1, pady=20, sticky='WE')

        date_label = tk.Label(self.add_repair_frame,
                              text='Date:').grid(column=0, row=2)
        self.date_sv = tk.StringVar()
        self.date_entry = tk.Entry(self.add_repair_frame,
                                   text=self.date_sv)
        self.date_entry.focus_set()
        self.date_entry.grid(column=1, row=2, sticky='W')

        self.cal_img = tk.PhotoImage(file='Resources/calendar.gif')
        show_cal_btn = tk.Button(self.add_repair_frame, image=self.cal_img,
                                 command=self.show_cal, relief='flat').grid(column=1, row=2, sticky='W', padx=170)

        description_label = tk.Label(self.add_repair_frame,
                                     text='Description:').grid(column=0, row=3)
        self.description_sv = tk.StringVar()
        self.description_entry = tk.Entry(self.add_repair_frame,
                                          text=self.description_sv)
        self.description_entry.grid(column=1, row=3, ipadx=200)
        save_button = tk.Button(self.add_repair_frame, text='Save',
                                command=self.save_repair)
        save_button.grid(column=1, row=4, pady=10, sticky='E')
        save_button.bind('<Return>', self.save_repair)
        save_button.bind('<KP_Enter>', self.save_repair)
        cancel_button = tk.Button(self.add_repair_frame, text='Cancel',
                                  command=self.cancel_repair)
        cancel_button.grid(column=1, row=4, sticky='E', padx=60)
        cancel_button.bind('<Return>', self.cancel_repair)
        cancel_button.bind('<KP_Enter>', self.cancel_repair)

W metodzie add_repair() tworzona jest ramka w której umieszczone są pola etykiet i pola wprowadzania danych o dacie naprawy i opisie naprawy. Focus ustawiany jest na pole wpisywania daty – date_entry. Obok pola wprowadzania daty jest umieszczony przycisk do aktywowania obiektu date picker. Metoda zawiera jeszcze deklarację dwóch przycisków: cancel_button oraz save_button służące do zapisu wpisu o naprawie lub zamknięcia ramki dodawania wpisu tzn.

new repair
Okno Repairs po wciśnięciu przycisku add_repair_button

Kod metody cancel_repair(), która chowa ramkę repair_frame:

    def cancel_repair(self, event=None):
        self.add_repair_frame.grid_remove()

Kod metody save_repair():

def save_repair(self, event=None):
        if self.date_sv.get() and self.description_sv.get():
            self.helper.add_repair(self.car, self.date_sv.get(),
                                   self.description_sv.get())
            self.show_repairs()
            self.add_repair_frame.grid_remove()

W metodzie save_repair() dokonywana jest podstawowa walidacja formularza tzn. sprawdzane jest czy wszystkie wymagane pola do utworzenia wpisu o naprawie są wypełnione. Jeśli tak, to notatka o naprawie jest zapisywana w bazie danych (metoda add_repair klasy Helper), a następnie uaktualniane są wpisy o naprawach i chowana jest ramka repair_frame.

Pozostała metoda show_cal(), która jest aktywowana po wciśnięciu przycisku show_cal_btn:

    def show_cal(self, event=None):
        date_picker = DatePicker(self.top_level, self.date_entry, '%d-%m-%Y')
        self.description_entry.focus_set()

Medoda show_cal() tworzy instancję klasy DatePicker, którą utworzyłem w celu pobierania daty i wstawiania jej do podanego pola Entry. Klasa ta jako argument umożliwia wstawienie wybranego formatowania wybranej daty, dzięki czemu np. w ustawieniach aplikacji użytkownik mógłby wybrać jak ma być wyświetlana data.

Kod klasy DatePicker opiszę w kolejnym wpisie.

date picker
My DatePicker object

P.S. Jeśli okno główne ma się uruchamiać jako zmaksymalizowane, ale z opcją zmiany rozmiaru to w metodzie __init__() klasy CarManager należy dopisać:

root.attributes('-zoomed', True)

P.P.S. Jeśli wpisy o autach mają wyświetlać się z formatowaniem wierszy to metodę show_cars() można zmodyfikować, dopisując tagi definiujące kolory wierszy tzn.

    def show_cars(self):
        all_cars = self.helper.show_all_cars()
        self.car_tview.delete(*self.car_tview.get_children())
        for i, car in enumerate(all_cars, start=1):
            car = list(car)
            car.insert(0, i)
            if i % 2:
                self.car_tview.insert('', 'end', values=car, tag='c2')
            else:
                self.car_tview.insert('', 'end', values=car, tag='c1')
            self.car_tview.tag_configure('c1', background='ivory3')
            self.car_tview.tag_configure('c2', background='ivory2')
widżet Treeview z uwzględnieniem tagów formatujących

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *