TL;DR: Po raz pierwszy udało mi się wprowadzić sensowne klasy do projektu. Mam klasę Entry oraz Data, analogicznie jak w kartach trzymanych w ręce. Pojedyncza karta to jedno a zbiór tych kart to drugie.

Jeśli kiedyś faktycznie zostanę programistą to dzień 1 maja 2017 roku powinienem uznać za przełomowy. Jaram się. To żaden krok dla ludzkości, ale wielki dla mnie. Po raz pierwszy mam wrażenie, że coś w mojej głowie w kwestii programowania przeskoczyło. Oto po raz pierwszy zrobiłem klasy. Jak już wspominałem. Jaram się.

Ostatnio gapiłem się w mój kod jak gapa w gnot. Pisałem o tym, że teoretycznie wiem czym jest klasa, ale jak to się ma do mojego kodu? Co tam klasą powinno a co nie powinno być?

Analogia do kart

Przypomniało mi się, że na kursie Pythona z Coursery robiłem jedno z zadań, które polegało na napisaniu gry karcianej Black Jack.

Tam ważne było, żeby określić czym jest w ogóle karta a potem czym są karty które trzymasz w ręce. Pomyślałem, że kartą w moim przypadku jest input użytkownika natomiast kartami trzymanymi w ręce jest zbiór tych inputów.

Gapiłem się długo, zapisałem dotychczasowy kod bonusowo w pliku i z pełnym przeświadczeniem, że wszystko mi się rozpieprzy zacząłem przepisywać.

Zadziałało

Kiedy tylko udało mi się napisać klasę Entry i Data, od razu wprowadziłem pewne ulepszenia aż w końcu powstał kod, który możesz zobaczyć na dole.

Wcześniej zbiorem danych była lista w której umieszczałem sekundy przekonwertowane z inputu użytkownika. Odszedłem od tego ogranicznogo pomysłu na rzecz zbioru klas entry.

class Entry

W moim przypadku ważne jest, żeby wyciągnąć jak najwięcej informacji z inputu użytkownika.

Po pierwsze trzeba go w ogóle przekonwertować z formatu HH:MM:SS na liczbę sekund, żeby prościej móc robić na takich danych wszelkie potrzebne obiliczenia. Do tego śłuży def cleaner(self):. Ponadto w przyszłości, gdy użytkownik będzie miał możliwość określenia konkretnego dnia obliczyć na ich podstawie liczbę sekund od Epoch.

Po drugie potrzebuję też informacji jaki tryp/mode został wybrany. Po prostu jakiej czynności dotyczy input użytkownika (jazdy, odpoczynku, itd.).

Na koniec mam metodę def __str__(self): dzięki której zwracany jest string który zawiera wartość w formacie HH:MM:SS oraz mode, który został wybrany, np. „4:30:00 Driving”.

Ta metoda (BTW jaka jest różnica pomiędzy funkcją a metodą?) służy właśnie do tego, że po wywołaniu klasy, otrzymamy tego stringa – to jest super przydatna rzecz.

class Data

Tutaj w ramach inicjalizacji tej klasy powstaje dopiero pusta lista w którą będą wrzucane klasy Entry na podstawie inputu użytkownika.

To rozdzielenie jest o tyle przydatne, że niektóre rzeczy muszą być zrobione na pojedynczym inpucie a dopiero potem potrzebujemy coś zrobić ze zbiorem tych danych. Dopiero kiedy mam zbiór inputów użytkownika mogę zabierać się za wyświetlanie informacji użytkownikowi. Dopiero konkretny zbiór danych wrzucam na listbox w GUI tak, żeby użytkownik mógł na nich pracować. To jest bardziej logiczna konstrukcja.

Myślę, że to był duży krok do przodu. Zresztą od razu, gdy udało mi się przepisać ten kod zabrałem się za czyszczenie go i porządkowanie. Przy okazji powstało kilka nowych funkcjonalności jak np. kolorowanie inputów na listbox w GUI. O tym jednak już nie w tym wpisie. Pozdro.

from tkinter import *
import re
import datetime
import Pmw

def start():
    global current_data
    current_data = Data()
    current_data.update()
    current_data.info()

def add_entry():
    current_data.add()

def clear_all():  # clear top_frame_input
    top_frame_input.delete(0, END)
    top_frame_input.focus()

def clear_one():  # clear last digit from top_frame_input
    user_input = top_frame_input.get()[:-1]
    top_frame_input.delete(0, END)
    top_frame_input.insert(0, user_input)

def num_press(num):  # num pad button action
    if top_frame_input.get() == '00:00:00':
        clear_all()
    if num == "C":
        clear_all()
    elif num == ".":  # doesn't work because counter doesn't allow insert other thinks than numbers and ":"
        top_frame_input.insert(END, ":")
    else:

        top_frame_input.insert(END, num)
        top_frame_input.focus()

# =======tkinter window===========
win = Tk()
# win.geometry("775x325") # Force window size
win.wm_title("Tacho 0.0.4")
win.resizable(width=FALSE, height=FALSE)

Pmw.initialise(win)

'''
topFrame = Frame(win)
# topFrame.pack(fill=BOTH)
topFrame.grid(row=0, column=0, columnspan=2)
'''

topLeftFrame = Frame(win)
topLeftFrame.grid(row=0, column=0)

topRightFrame = Frame(win)
topRightFrame.grid(row=0, column=1)

leftFrame = Frame(win)
leftFrame.grid(row=1, column=0)

rightFrame = Frame(win)
rightFrame.grid(row=1, column=1)

bottomFrame = Frame(win)
bottomFrame.grid(row=2, columnspan=2)

# -------keys actions-----
win.bind("<Return>", lambda a: add_entry())
win.bind("<KP_Enter>", lambda a: add_entry())
win.bind("<KP_Add>", lambda a: add_entry())
# win.bind("<KP_Decimal>", lambda a: num_press(":"))

# ==========input entry===========

top_frame_input = Pmw.Counter(topRightFrame,
                              entry_font="Helvetica 20 bold",
                              entry_width=12,
                              autorepeat=True, datatype='time',
                              entryfield_validate={'validator': 'time'},
                              entryfield_value='00:00:00',
                              increment=60)
top_frame_input.grid(column=0)

top_frame_input.component('entry').focus_set()
top_frame_input.select_range(3, 5)
top_frame_input.icursor(5)

# ===========Listbox with scrollbar=================

entries_list = Pmw.ScrolledListBox(leftFrame, hscrollmode='none', vscrollmode='static',
                                   listbox_height=15, listbox_width=40)
'''
entries_list = Pmw.ComboBox(leftFrame, dropdown = 0, scrolledlist_vscrollmode = 'static',
                            scrolledlist_hscrollmode = 'none', scrolledlist_listbox_height = 15,
                            scrolledlist_listbox_width=40,
                            entryfield_validate = {'validator' : 'time'},
                            entryfield_value = '00:00:00',
                            )
'''

entries_list.grid(row=0, column=0)

# =======buttons===========
# -------num pad-----------
keyboard = []
keys = "789456123C0:"
i = 0
for j in range(1, 5):
    for k in range(3):
        keyboard.append(Button(rightFrame, text=keys[i], font="Helvetica 15 bold", height=1, width=2))
        keyboard[i].grid(row=j, column=k, pady=2, padx=2)
        keyboard[i]["command"] = lambda x=keys[i]: num_press(x)
        i += 1

# --------other buttons in num pad---------
add_entry_button = Button(rightFrame, text='+', font="Helvetica 15 bold", height=6, width=7, command=add_entry)
add_entry_button.grid(row=1, column=3, rowspan=4, columnspan=2, pady=2, padx=2)

clear_one_button = Button(rightFrame, text="←", font="Helvetica 15 bold", height=1, width=2, command=clear_one)
clear_one_button.grid(row=0, column=4, pady=2, padx=2)

# ------buttons to change mode of entry-------
select_mode = Pmw.RadioSelect(rightFrame, Button_height=1, Button_width=2,
                              Button_font="Helvetica 15 bold", pady=2, padx=2)

select_mode.grid(row=0, column=0, columnspan=4)

# Add some buttons to the horizontal RadioSelect - mode selection buttons.
for name, symbol, background in (('D', u'\u2609', 'green'), ('W', u'\u2692', 'blue'),
                                 ('P', u'\u26DD', 'yellow'), ('R', u'\u29E6', 'red')):
    select_mode.add(name, text=symbol, background=background)

select_mode.invoke(3)  # select break/rest as default

# ------top left buttons-------
top_left_buttons = Pmw.ButtonBox(topLeftFrame, Button_height=1,  # Button_width=2,
                                 Button_font="Helvetica 15 bold", pady=1, padx=1)

top_left_buttons.grid(row=0, column=0, columnspan=2)

# Add some buttons to the horizontal RadioSelect.
top_left_buttons.add('Delete', command=lambda: current_data.delete_item())
top_left_buttons.add('Edit')
top_left_buttons.add('Save')
top_left_buttons.add('Clear', command=start)

# =====bottom status=============
status = StringVar()
bottom_status_total = Label(bottomFrame, textvariable=status, bd=1, relief=SUNKEN,
                            font="Helvetica 15 bold", width=54)
status.set("")
bottom_status_total.pack(fill=X, expand=True, side=TOP, ipady=10, ipadx=10)

# ============================================
class Data:

    def __init__(self):
        self.records = []

    @staticmethod  # this let this method be called in class or outside
    def converter(sec):
        conversion = '%d:%02d:%02d' % (sec / 3600, sec / 60 % 60, sec % 60)  # convert to HH:MM:SS
        return conversion

    def add(self):
        if str(top_frame_input.get()) != '00:00:00':
            index = 0
            entry = Entry(select_mode.getvalue(), top_frame_input.get())
            try:
                index = entries_list.curselection()[0]  # try get position where entry should be added
            except IndexError:
                pass

            self.records.insert(index, entry)
            top_frame_input.setentry('00:00:00')
            top_frame_input.select_range(3, 5)  # entry field should be focus on minutes
            top_frame_input.icursor(5)
            self.info()
            self.update()

    def delete_item(self):
        # delete a selected line from the listbox and from entries
        try:
            # get selected line index
            index = entries_list.curselection()[0]
            entries_list.delete(index)  # delete item from listbox in GUI
            self.records.pop(index)  # delete item from data list
            self.update()
            self.info()

        except IndexError:
            pass

    def sum(self, v):
        total = 0

        if v == 'total':

            for v in self.records:
                total += v.get_value()
            return total
        else:  # sum for mode

            summary = 0
            for x in self.records:
                if v == x.get_mode():
                    summary += x.get_value()
            return summary

    def update(self):
        entries_list.delete(0, END)
        line_number = len(self.records)
        for record in self.records:
            entries_list.insert(END, str(line_number) + ') ' + str(record))
            # color lines:
            if record.get_mode() == 'R':
                entries_list.itemconfig(END, {'bg': 'red'}, foreground='white')
            elif record.get_mode() == 'P':
                entries_list.itemconfig(END, {'bg': 'yellow'})
            elif record.get_mode() == 'W':
                entries_list.itemconfig(END, {'bg': 'blue'}, foreground='white')
            elif record.get_mode() == 'D':
                entries_list.itemconfig(END, {'bg': 'green'}, foreground='white')

            line_number -= 1

    def info(self):
        status.set('Total time: ' + str(self.converter(self.sum('total')))+'\n' +
                   str(self.sum('total'))+' seconds'+'\n' +
                   'Resting in total: '+str(self.converter(self.sum('R')))+'\n' +
                   'Driving in total: '+str(self.converter(self.sum('D')))+'\n' +
                   'Working in total: '+str(self.converter(self.sum('W')))+'\n' +
                   'POA in total: '+str(self.converter(self.sum('P')))
                   )

class Entry:
    def __init__(self, mode, value):
        self.mode = mode
        self.value = value
        self.cleaner()

    def cleaner(self):
        user_input = str(self.value)
        user_input = re.sub('[^0-9:]', '', user_input)  # leaves only digits and ":" into input
        try:
            h, m, s = re.split(':', user_input)
            self.value = int(datetime.timedelta(hours=int(h), minutes=int(m), seconds=int(s)).total_seconds())
        except ValueError:
            try:
                m, s = re.split(':', user_input)
                self.value = int(datetime.timedelta(minutes=int(m), seconds=int(s)).total_seconds())
            except ValueError:
                self.value = re.sub('\D', '', user_input)  # clean input from non-digit characters
                self.value = int(datetime.timedelta(minutes=int(self.value)).total_seconds())

        # self.input = self.input.replace("+", "") # simplest method (replace just one character)
            # in case this one above will make a troubles
    def get_value(self):
        return self.value

    def get_mode(self):
        return self.mode

    def __str__(self):
        mode_names = {'D': 'Driving', 'W': 'Work', 'P': 'POA/availability', 'R': 'Rest/Break'}
        conversion = Data().converter(self.value)  # call static method from class Data
        # sec = self.value
        # conversion = '%d:%02d:%02d' % (sec / 3600, sec / 60 % 60, sec % 60)  # convert to HH:MM:SS
        return conversion + ' ' + mode_names[self.mode]
# ===================================================
start()
win.mainloop()

 

Jedna uwaga do wpisu “Teraz mam dwie ładne i sensowne klasy

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj /  Zmień )

Zdjęcie na Google

Komentujesz korzystając z konta Google. Wyloguj /  Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj /  Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj /  Zmień )

Połączenie z %s