TL;DR: Pojawiły się kolorowe przyciski, kolorowanie lini na listbox w zależności od wybranego trybu wpisu oraz wyświetlanie info o pozostałym czasie. Opisuję tutaj kod, który się za tym kryje.

W ostanim poście pisałem o tym, że kod został przepisany na taki z klasami. Czym one są u mnie oraz jakie mają zadanie możesz przeczytać właśnie tam. Teraz chciałbym pochylić się nad funkcjonalnościami, które w między czasie powstały a o których jeszcze nie wspominałem.

Screenshot from 2017-05-05 16-27-30croped

Pierwsze co się rzuca w oczy to kolorowe przyciski zmiany trybu wprowadzanego inputu – wybór pomiędzy jazdą, pracą itp.

# ------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

Pętla for określa dla każdego przycisku ‚name’ – czyli nazwę przycisku oraz string, który daje jego wciśnięcie, ‚text’ – czyli to co na przycisku jest napisane oraz kolor przycisku.

Obecnie przyciski mają symbole Unicode, które przypominają te, które są na prawdziwych tachografach. W przyszłości wrzucę tam grafikę.

☉ Jazda/Driving

⚒ Praca/Work

⛝ Dyspozycyjność/POA

⧦ Odpoczynek/Rest

UWAGA: Pamiętaj, że te przyciski należą do Pmw i przez to może nieco inaczej dodaje się komponenty.

Jeszcze więcej kolorów

Kolorowe są również wartości na listbox wyświetlanym użytkownikowi. Ponieważ tylko tam jest istotne, żeby odróżniały się od siebie trzeba było to uwzględnić w metodzie update klasy Data.

class Data:
...
    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

Przypomnijmy, że za każdym razem kiedy ta metoda jest wywoływana lista jest czyszczona i tworzona od nowa. Dlatego na początku mamy entries_list.delete(0, END)
Potem każda wartość po kolei jest wpisywana na listbox. W zależności od informacji o jej trybie jest ona kolorowana na odpowiedni kolor.

Dajmy jeszcze numerki

Dodatkowo uznałem, że dla lepszej widoczności co się dzieje na listbox, dobrze będzie, żeby każda linia była numerowana zgodnie z kolejnością wprowadzanych danych.

Pomimo tego, że można tą informację odczytać z indeksu wartości, to Python po pierwsze liczy od zera a po drugie indeksuje od góry do dołu- czyli najwyżej to 0. Ja potrzebuję, żeby po pierwsze liczył od 1 a po drugie, aby nr 1 był na dole, bo w takiej kolejności są dodawane wpisy. Stąd wziął się line_number. Najpierw jest określane ile w ogóle jest linii wyciągając długość listy: line_number = len(self.records), a potem ten numer jest użyty i na koniec odejmujemy jeden: line_number -= 1 i całość się powtarza.

A może by tak coś przydatnego?

No i co  najważniejsze program zaczął wyświetlać jakieś przydatne informacje użytkownikowi. Do tego celu wprowadziłem dwie metody sum oraz remaining_time.

Tu nie było, żadnych większych trudności. Jeśli chodzi o def sum(self, v): to jeśli jest wywoływana jako self.sum(‚total’) zwraca sumę wszystkich wpisów lub self.sum(PODAJ_MODE) sumuje wpisy w podanym trybie/mode, np. self.sum(‚D’) – sumuje wpisy tylko w trybie jazdy.

Kiedy już wiemy ile jest w sumie np. jazdy to w metodzie def time_remaining(self, v): dowiemy się czy dostępny czas jazdy dla całego dnia (w tym momencie dla całego zbioru danych czyli listy self.records) został wykorzystany czy nie.

Strasznie skomplikowanie to brzmi w słowach, ale to jest dość proste i wygląda tak:

...
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 time_remaining(self, v):

    if v == 'total':
        total_remaining = 54000  # 15h = 54000 seconds
        total_remaining -= self.sum('total')

        if total_remaining < 0:
            total_remaining = self.converter(total_remaining)
            return str(total_remaining)+' TIME OUT!'
        return self.converter(total_remaining)

    # remaining time for modes
    elif v == 'D':
        driving_remaining = 36000  # 10h
        driving_remaining -= self.sum('D')
        if driving_remaining < 0:
            return str(self.converter(driving_remaining))+' TIME OUT!'
        else:
            return self.converter(driving_remaining)

Jest jeszcze inna kwestia, której nie udało mi się jeszcze do końca rozwiązać. Przepisy mówią, że kierowca musi zrobić przerwę trwającą minium 45 min po 4,5h jazdy. Potrzebuję, żeby program sprawdził czy przepisy zostały złamane. Tym jednak zajmę się już w następnym poście.

Wszystkie informacje na razie są wyświetlane po prostu na dole w label za pomocą metody info:

...

    def info(self):
        status.set('Driving: '+str(self.converter(self.sum('D'))) +
                   ' / time remaining: '+str(self.time_remaining('D'))+'\n' +
                   'Work: '+str(self.converter(self.sum('W')))+'\n' +
                   'POA: '+str(self.converter(self.sum('P')))+'\n' +
                   'Rest: '+str(self.converter(self.sum('R')))+'\n' +
                   'Total time: ' + str(self.converter(self.sum('total')))+' / day time remaining: ' +
                   str(self.time_remaining('total'))+'\n' +
                   str(self.was_break())
                   )

Natomiast cały kod wygląda tak:

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):
        if sec < 0:  # This is temporary solution, there is a problem with negative modulo
            sec *= -1
            conversion = '%d:%02d:%02d' % (sec / 3600, sec / 60 % 60, sec % 60)
            return '-'+str(conversion)
        else:
            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 time_remaining(self, v):

        if v == 'total':
            total_remaining = 54000  # 15h = 54000 seconds
            total_remaining -= self.sum('total')

            if total_remaining < 0:
                total_remaining = self.converter(total_remaining)
                return str(total_remaining)+' TIME OUT!'
            return self.converter(total_remaining)

        # remaining time for modes
        elif v == 'D':
            driving_remaining = 36000  # 10h
            driving_remaining -= self.sum('D')
            if driving_remaining < 0:                 return str(self.converter(driving_remaining))+' TIME OUT!'             else:                 return self.converter(driving_remaining) # __________This is in progress, doesn't work very well yet__________     def was_break(self):         global info_list         driving_time = 0         info = ''         info_list = []         for x in self.records:             print(x.get_mode())             if x.get_mode() == 'D':                 driving_time += x.get_value()             if x.get_mode() == 'R' or driving_time > 120:  # and x.get_value() == 120:
                if driving_time > 120:

                    info = "Too much driving"

                    info_list.append(info)

                return info+str(driving_time)+str(info_list)
        return info+str(driving_time)
# ______________________________________________

    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('Driving: '+str(self.converter(self.sum('D'))) +
                   ' / time remaining: '+str(self.time_remaining('D'))+'\n' +
                   'Work: '+str(self.converter(self.sum('W')))+'\n' +
                   'POA: '+str(self.converter(self.sum('P')))+'\n' +
                   'Rest: '+str(self.converter(self.sum('R')))+'\n' +
                   'Total time: ' + str(self.converter(self.sum('total')))+' / day time remaining: ' +
                   str(self.time_remaining('total'))+'\n' +
                   str(self.was_break())
                   )

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()

 

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