niedziela, 28 maja 2017

Kolejny błąd odnaleziony.

Przeznaczyłem trochę czasu na testowanie BioCounter'a poprzez jego użytkowanie. To znaczy, wczytywałem zdjęcia, w różnym formacie i zaznaczałem na nich różne elementy. Przy czym moim celem nie było celowe zepsucie działania aplikacji, ale sprawdzenie, czy jakiś błąd nie wyjdzie podczas normalnego użytkowania.

Znalazłem w ten sposób jeden błąd. Okazało się, że aplikacja nie trzymała popranie informacji dotyczącej rozszerzenia wczytanego pliku. Po wczytaniu np. pliku w formacie "jpg", aplikacja trzymała informacje na temat tego rozszerzenia. Jeśli użytkownik oznaczył interesujące elementy na zdjęciu, a następnie zdecydował się na eksport takiego zdjęcia, wszystko przebiegało zgodnie z założeniami i program poprawnie sugerował rozszerzenie pliku jako "jpg". Problem pojawiał się w momencie kiedy po otwarciu pierwszego zdjęcia, wybierano otwieranie kolejnego, ale proces ten anulowano. W tym momencie program gubił informacje o rozszerzeniu obrabianego pliku, a podczas próby eksportu tego pliku, brakowało domyślnego rozszerzenia w oknie dialogowym.

Rozwiązanie tego problemu okazało się bardzo proste i polegało na zmodyfikowaniu zmiennej odpowiedzialnej, za przechowywanie informacji dotyczącej rozszerzenia wczytanego pliku graficznego.

czwartek, 25 maja 2017

Czyszczenie kodu.

Czystość pisanego kodu jest ważna, ponieważ poprawia czytelność i ułatwia pisanie samej aplikacji. Jednak w tym przypadku chodzi o usuwanie zbędnych elementów programu, które były potrzebne do debugowania jego kodu. Gdyż nie powinny być one widoczne dla użytkownika.

Oczywiście elementy te nie muszą być usunięte na stałe. Byłoby to nawet niewskazane, ponieważ za każdym razem kiedy zaistniała by potrzeba sprawdzenia działania jakiegoś stałego elementu aplikacji, kod odpowiedzialny za debugowanie trzeba byłoby na nowo dodawać. Wystarczy je sprytnie zdezaktywować dodając zmienną "debug" oraz stosując instrukcje warunkowe w odpowiednich, potrzebnych miejscach w programie. Jest to bardo wygodne, gdyż w łatwy sposób pozwala na włączanie i wyłączanie całych sekcji kodu.

Zdaję sobie sprawę, że do testowanie przeznaczone są testy jednostkowe. Jednak nie wiem, czy za pomocą takich testów można by przetestować wszystkie elementy implementowane z użyciem Tkinter. Dotychczas używałem testów jednostkowych do sprawdzania funkcjonowania klas, czy też modułów i do tego nadawały się wyśmienicie. Jednak nie wiem, czy można przeprowadzić takie testy w połączeniu z event'ami Tkinter.

Na moje potrzeby wystarczające okazało się wypisywania potrzebnych wartości w konsoli, ponieważ interesowało mnie, czy coś ma daną, spodziewaną wartość. Metoda ta, bardzo szybko osiąga swoje limity i nie nadaje się do testowania szerokiego zakresu wartości jednej zmiennej. Jednak dla moich potrzeb okazała się szybka i efektywna, co było wystarczającym powodem na jej zastosowanie.

niedziela, 21 maja 2017

Kontrola nad aplikacją.

Użytkownik powinien posiadać swobodę pracy z aplikacją, ale jaka kontrola zostanie mu dana, zależy jedynie od developera. Oczywiście wszystko powinno być wyważone i wskazane jest, aby zachować złoty środek. Aczkolwiek należy pamiętać, że obecność pewnych funkcji wiąże się z pewnymi oczekiwaniami dotyczącymi funkcji towarzyszących.

Dlatego kiedy dodałem do programu możliwość eksportowania obrazów z naniesionymi markerami, poszedłem o krok dalej i zaimplementowałem możliwość regulowania jakości eksportowanego obrazu. Dyskusyjne jest, czy było to potrzebne, ponieważ normą są dyski o kilkuset gigabajtowej pojemności, więc kilkaset kilobajtów wydaje się nie robić problemu. Jednak funkcja pozwalająca na zmianę jakości zapisywanego obrazu, jest nieodzownie związana z możliwością eksportowania samego obrazu. Te dwie funkcje w programach graficznych są połączone z sobą, a obecność jednej, bez dodania drugie powoduje, że produkt może sprawiać wrażenie niekompletnego.

Oczywiście na popularność programu, czyli to czy odniesie sukces, czy też nie, składa się wiele innych czynników. Jednak to jak dany program jest odbierany przez użytkownika stanowi jeden z wyznaczników, który jest podwaliną tego sukcesu. Obecność pewnych funkcji programu może mieć charakter minimalistyczny, ale ważnym czynnikiem jest to, jakie rozwiązania są swoiste dla implementacji danych rozwiązań. Dlatego też, dodając pewne funkcjonalności programu nie można ograniczać się do nich samych, ale łączyć je z funkcjonalnościami, których może oczekiwać sam użytkownik. Podnosi to znacznie jakość samego programu, ale sprawia również, że praca z nim staje się bardziej komfortowa.

czwartek, 18 maja 2017

Drobnostki poprawiające ogólne wrażenie.

W jednym ze wcześniejszych postów pisałem, że na to czy program jest odbierany jako dobry, czy kiepski mogą wpływać niewielkie niuanse. Oprócz tego w programie może znajdować się wiele elementów poprawiających ogólne wrażenie i dodających elementy nie związane z podstawową funkcjonalnością.

Jednym z takich elementów jest okno z informacjami o programie. Jest to w zasadzie szczegół, na który nie zwraca się prawie uwagi. Aczkolwiek sam pamiętam, że czasami sprawdzałem, co w takim oknie znajdę i byłem zaskoczony informacjami jakie tam odkryłem. W szczególności kiedy dowiadywałem się, że np. dany program stworzyła tylko jedna osoba. Oczywiście takie informacje jak autorstwo programu, można umieścić w samym kodzie aplikacji, ale utworzenie do tego celu specjalnego okna, pokazuje pewną dbałość o szczegóły.

Oprócz informacji o autorstwie w oknie takim mogą znajdować się informacje dotyczące wersji programu, łącznie z numerem buildu, odnośnik do strony internetowej autora, czy też informacje o użytych bibliotekach. Takie ono może być również dobrym miejscem na dodanie loga programu. Możliwości realizacji takiego pomysłu jest na prawdę wiele.

niedziela, 14 maja 2017

Dodatkowe okna, kolejnych słów kilka.

Postanowiłem zaimplementować możliwość zmiany jakości dla zapisywanego pliku "jpg". Wybór padła na zastosowanie klasy Toplevel() i zbudowanie odpowiedniego interfejsu graficznego w nowym oknie.

Konstruowanie okna przebiegło bez żadnych problemów. Jednak mimo przekazywania wszystkich koniecznych parametrów, do odpowiednich ustawień, eksportowane pliki zapisywały się z domyślną wartością kompresji, wynoszącą 95. Okazało się, że nowe okno jest tworzone, ale cały program nie czeka na wprowadzenie zmian, tylko kontynuuje pracę w tle. Dlatego też zmiana ustawień nie maiła, żadnego wpływu na jakość eksportowanego pliku. Wprowadzone zmiany byłyby zastosowane dopiero do kolejnego eksportowanego pliku.

W celu pauzowania pracy głównego programu, trzeba wywołać metodę wait_window() na odpowiednim elemencie, co jest zależne od przyjętej implementacji tej metody. Oprócz pauzowania dobrym rozwiązaniem jest wcześniejsze wykonanie metody transient() na instancji klasy Toplevel() i przekazanie do tej metody głównego elementu programu. Spowoduje to związanie nowego okna z głównym oknem programu, dzięki czemu nowe okno nie pokaże się jako dodatkowa ikona na pasku zadań w Windows.

wtorek, 9 maja 2017

Dodatkowe okna w Tkinter.

Cała aplikacja Tkinter działa w pętli określonej za pomocą metody mainloop(), a wszystkie procesy zachodzą na jednym wątku. Aczkolwiek nie znam Tkinter tak dobrze, aby móc stwierdzić, czy wykorzystywany jest Threading, czy też nie. W związku z tym, kiedy trzeba utworzyć dodatkowe okno programu, nie można po prostu utworzyć kolejnej instancji Tk() i wywołać na niej kolejnej metody mainloop(). Nie wiem, jak wyglądało by działanie Tkinter z utworzeniem dwóch osobnych okien, na dwóch osobnych procesach, ale sama koncepcja nie wydaję się być za dobra.

Problem wielu okien można w łatwy sposób obejść dodając do listy kolejne instancje widget'ów, z danego segmentu GUI i w razie potrzeby niszczyć te obiekty, poprzez wywołanie na nich metody destroy(). W miejsce usuniętych elementów można utworzyć nowe, wchodzące w skład wywołanej części interfejsu.

list_of_widgets = [self.a_button_1, self.a_button_2]
for element in list_of_widgets:
    element.destroy()

Rozwiązanie to działa bardzo dobrze i sam stosowałem je wielokrotnie, a jedynym minusem jest konieczność tworzenia list widget'ów. Szybkość tworzenia nowych elementów GUI również nie stanowi problemu i jest niezauważalna dla użytkownika.

Jeśli jednak zajdzie sytuacja, kiedy konieczne jest utworzenie nowego okna, to w tym celu należy użyć klasy Toplevel(). Oferuje ona szereg standardowych opcji konfiguracji, takich jak przy tworzeniu głównego okna. Największą różnicą jest to, że na instancji klasy Toplevel() nie wywołuje się metody mainloop(). Przekazywanie zmiennych między obydwoma oknami również zachodzi bezproblemowo.

Użycie jednej, albo drugiej metody zależy tylko i wyłącznie od przyjętego projektu interfejsu. Nic nie stoi na przeszkodzie, aby połączyć obydwie metody i stosować je zamiennie, w odpowiednich do tego miejscach.

niedziela, 7 maja 2017

Status bar.

Obecność status bar'a nie jest obowiązkowa, ale wydaje się, że pomaga w pewien sposób przekazywać użytkownikowi informacje, dotyczące działania aplikacji. Budując okno programu miałem w planach dodanie status bar'a, ale zakodowanie tej funkcjonalności odkładałem w czasie, gdyż nie była ona krytyczna dla funkcjonowania całego programu.

Wyświetlanie statusów następuje poprzez, zmianę wartości odpowiedniej instancji klasy StringVar() w widget'cie ttk.Label. Bardzo istotnym założeniem było wyświetlanie danej wiadomości tylko przez określony okres czasu, a następnie czyszczenie całego paska. Kolejkowanie zadań w Tkinter odbywa się poprzez zastosowanie metody "after" na jakimś widget'cie. Piszę jakimś, ponieważ implementując to rozwiązanie dostrzegłem, że instancja widget'a nie musi należeć do tej samej hierarchii. Jednak dla zachowania jednorodności, wszystkie wywołania metody "after" przypisywałem do tego samego obiektu. W metodzie tej przekazuje się czas (w milisekundach) po jakim ma zostać wywołana dana funkcja oraz przekazuje się samą funkcję.

self.canvas_frame.after(2000, self.clear_status)

Okazało się, że częściowo myliłem się odnośnie funkcjonowania tej metody. Nie wiedziałem, że kolejne jej wywołanie, nie anuluje automatycznie poprzedniego. Zamiast tego, kolejne wywołania są kolejkowane i wywoływane po odpowiednim upływie czasu. Jeśli zdefiniowane interwały są niewielkie nie sprawia to problemu, aczkolwiek przy zastosowaniu okresu 2000 ms, mogą dziać się ciekawe rzeczy. Użyłem tego rozwiązania do wyświetlania liczby odpowiadającej wielkości markera, po zmianie jego wielkości za pomocą skrótów klawiaturowych (oraz odpowiadających przycisków). Przy zmianie wielkości o kilka stopni, następowało kolejkowanie zadań i ich wywoływanie po odpowiednim czasie, co kilkukrotnie, w niewielkich odstępach czasu, czyściło status bar'a. Spowodowało to migotanie tekstu wyświetlanego na status bar'ze. Być może uszło by to uwadze większości użytkowników, jednak w pewien sposób wskazywało na kiepską jakość kodu w programie. Było też nie estetyczne.

Jak zwykle szukałem rozwiązań w internecie, głównie na "stacku". Najprostszym z nich było utworzenie atrybutu klasy, do którego przypisywane było aktualne zadanie, "self.job_queue". Jeśli atrybut ten posiadał wartość "None", przypisywano do niego pierwsze zadanie. Jeśli zadanie to zostało wykonane, wartość zostawała ponownie zmieniana na "None". Jeśli natomiast, przed wykonaniem zadania została ponownie wywołana metoda "after", to poprzednie zadanie było anulowane za pomocą metody "after_cancel", a do "self.job_que", przypisywano nowe zadanie. W ten sposób status bar zostawał czyszczony tylko raz, 2000 ms po wyświetleniu ostatniego tekstu, problem migotania został rozwiązany.

sobota, 6 maja 2017

Walka z zakładkami.

Funkcjonowanie Tkinter potrafi niejednokrotnie zaskoczyć, co może owocować niespodziewanym działaniem programu. Podczas przełączanie zakładek w widget'cie ttk.Notebook następuje zmiana elementu, który aktualnie posiada focus. Jest on przełączany na elementy umieszczone w zakładce, którą aktywowano. Działanie to może być niepożądane, jeśli w karcie znajduje się np. ttk.Entry, ponieważ spowoduje to zaznaczenie całego obecnego w elemencie tekstu.

Aby temu zapobiec można przekazać parametr "takefocus" o wartości "False", podczas tworzenia instancji "ttk.Entry". Ma to jednak poważne działanie uboczne, ponieważ wyłącza możliwość przełączania aktywnych elementów za pomocą przycisku Tab. Takie rozwiązanie odpada. Ponadto focus zostanie przeniesiony na kolejny obiekt znajdujący się na zakładce.

Inną możliwością jest bind'owanie do danej zakładki metody, w oparciu o "event"
"<Visibility>" i przypisanie jej odpowiedniego wywołania callback. "Event" ten zostaje aktywowany w momencie kiedy dany element staje się widoczny na ekranie.

self.tab_1.bind('<Visibility>', lambda event,
    tab=self.tab_1:callback_method(event, tab))

Następnie używając zdefiniowanej metody callback_method, można przenieść focus na inny element, w tym wypadku na samą zakładkę, która jest przekazywana do tej metody. To rozwiązanie działa, ale pozostawia po sobie pewne artefakty.

Tekst obecny w ttk.Entry nie jest zaznaczony, ale posiada pewne zauważalne pogrubienie. Jeśli możliwość wpisywania teksu do ttk.Entry została zablokowana poprzez ustawienie "state" na wartość "readonly", to tło teksu w takim polu zmienia kolor na biały. Jest to pewnie związane z tym, że tekst został zaznaczony podczas przełączania zakładki, po czym focus został przeniesiony na inny obiekt. W związku z tym, że są to obiekty stylizowane ttk, nie można tego szybko naprawić poprzez zmianę "hightlightthickness", czy "hightlightbackground", prawdopodobnie konieczne byłoby zdefiniowanie odpowiedniego stylu ttk.

Rozwiązanie okazało się proste i było raczej niespodziewane. Otóż podczas przełączanie zakładki focus zostaje przeniesiony zgodnie z obecną hierarchią powstałą między obiektami, a związaną z przypisywaniem danych elementów do siebie. W celu zdefiniowania wielkości samej zakładki konieczne jest utworzenie obiektu typu Frame lub ttk.Frame, która jest najwyżej w hierarchii. Widget'y te mają domyślnie ustawiony parametr "takefocus" na "False". Po zmianie wartości na "True", ramka będzie pierwszym obiektem w kolejce do otrzymania focus'a. Na ramce natomiast nie widać żadnego efektu zaznaczenia, problem został rozwiązany.

środa, 3 maja 2017

Testowania nic nie zastąpi.

Podczas pisania kodu starałem się sprawdzać funkcjonowanie programu, mimo tego nie uniknąłem błędów. Jest to związane z tym, że byłem skupiony na kontrolowaniu działania niewielkiej części programu w związku z czym, mojej uwadze mogły umknąć pewne rzeczy. Znalazłem i poprawiłem dwa następujące błędy.

Pierwszy odnaleziony błąd był dosyć stary. Podczas włączania programu wszystkie zmienne i wartości dotyczące markerów oraz ich ilości są wyzerowane. Po wczytaniu zdjęcia, a następnie pracy na nim, te wartości ulegają zmianie. Podczas wczytywania do programu kolejnego, nowego zdjęcia wartości te są zerowane, w celu umożliwienia zliczenia nowych elementów. Problem dotyczył tego, że wartości liczbowe przechowywane jako Tkinter StringVar() nie były zerowane i pokazywały wartości odnoszące się do starego zdjęcia.

Drugi błąd dotyczył zapisu zdjęcia z naniesionymi markerami. Do nazwy pliku automatycznie dodawane było jego rozszerzenie. Jeżeli podczas eksportu, w oknie dialogowym zaznaczono już istniejący plik, posiadający końcówkę określającą rozszerzenie, to mimo tego do nazwy pliku dodawany był kolejny, nowy fragment tekstu oznaczający rozszerzenie.

Powyższe przykłady pokazują, że nieoczekiwane błędy mogą się pojawić podczas zwykłej pracy z programem. Ponadto mogą znajdować się w kodzie, który napisany był dużo wcześniej, a podczas sprawdzania jego konkretnej funkcjonalności nie powodował żadnego problemu.