Pokazywanie postów oznaczonych etykietą Tkinter. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Tkinter. Pokaż wszystkie posty

niedziela, 4 czerwca 2017

Podsumowanie konkursu.

Konkurs "Daj się poznać", edycja 2017 został oficjalnie zakończony. Pragnę pogratulować tym samym wszystkim uczestnikom, a tym którym udało się zakwalifikować dalej, życzyć powodzenia w fazie finałowej.

Jako, że spełniłem wszystkie wymagania, udało mi się dostać do grona finalistów. Oddałem również głosy na projekty, które uważałem za interesujące. Aczkolwiek, mój wybór chciałbym pozostawić w tajemnicy. Widziałem sporo interesujących projektów i blogów, więc ograniczenie wyboru do trzech pozycji nie było łatwym zadaniem. Śledząc inne blogi dowiedziałem się kilku interesujących rzeczy. Być może ktoś dowiedział się czegoś wartego poznania z mojego bloga.

Na koniec, małe podsumowanie:

  • Cały projekt rozwijałem przez 11 tygodni.
  • Podczas tego czasu zrobiłem 45 commit'ów.
  • Utworzyłem 8302 linie kodu.
  • Usunąłem 3223 linie kodu.

Powyższe statystyki nieco przekłamują wielkość aplikacji, ponieważ obejmują wszystkie utworzone linie kodu, które zostały commitowan'e oraz wszystkie które zostały usunięte. Aczkolwiek różnica w ilości linii dodanych i usuniętych wynosi 5079, co nie odzwierciedla prawidłowej ilości 4285 linii kodu (wraz z pustymi liniami, bez pustych linii - 3522). Szybkie poszukiwania z użyciem google nie dały mi odpowiedzi na to, w jaki sposób liczone są commit'owane linie na github'ie.

Statystyki dotyczące ilości linii kodu.

Name: statistics_gui.py              | Lines: 895
Name: markers_gui.py                 | Lines: 532
Name: run.py                         | Lines: 345
Name: main_gui.py                    | Lines: 317
Name: texts.py                       | Lines: 280
Name: options_gui.py                 | Lines: 213
Name: statistics.py                  | Lines: 135
Name: png_export_gui.py              | Lines: 92
Name: export_canvas_utilities.py     | Lines: 90
Name: jpg_export_gui.py              | Lines: 88
Name: tif_export_gui.py              | Lines: 81
Name: samples.py                     | Lines: 67
Name: widgets_geometries.py          | Lines: 61
Name: about_gui.py                   | Lines: 58
Name: shapes.py                      | Lines: 56
Name: settings_utilities.py          | Lines: 54
Name: export_statistics.py           | Lines: 32
Name: manage_settings.py             | Lines: 31
Name: screen_utilities.py            | Lines: 28
Name: statuses.py                    | Lines: 26
Name: marker.py                      | Lines: 22
Name: marker_utilities.py            | Lines: 19

TOTAL: 3522

Wszystkie pliki zostały automatycznie przeskanowane, a linie w plikach policzone z zastosowaniem prostego skryptu, z wyłączeniem, pustych linii, ale z uwzględnieniem docstrings oraz komentarzy.

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.


niedziela, 30 kwietnia 2017

Nawet przypisywanie przycisków może być podchwytliwe.

Okazało się, że skróty klawiszowe nie działały poprawnie. Trochę potrwało zanim znalazłem przyczynę tego zachowania. Po pierwsze dodając skrót do drop down menu, nie wystarczy skorzystać z argumentu "accelerate". Przekazany w ten sposób argument, jest tylko i wyłącznie tekstem wyświetlonym po prawej stronie etykiety, aby dany skrót działał, należy go również zbindować.

Odbywa się to przez metodę "bind_all". Strona będąca moim głównym źródłem informacji na temat Tkinter nie działała, wiec nie mogłem się dowiedzieć, jak to dokładnie działa. Według mojego rozumowania, co jest w zasadzie czystą spekulacją, metoda "bind" dotyczy, czy raczej jest wywoływana na obiekcie interfejsu, który jest aktualnie "sfokusowany". Wygląda na to, że przed wywołaniem callback'a tej metody, wyszukiwany jest widget posiadający focus, który mógł uzyskać np. poprzez kliknięcie lewym przyciskiem myszy. Jeżeli fokus widget'u zgadzał się z koordynatami event'u, to callback był wykonywany. Natomiast metoda "bind_all", bind'uje niejako daną komendę do wszystkich obecnych elementów. Chociaż zaznaczam, że są to tylko moje spekulacje.

W związku z powyższym, aby skróty z drop down menu działały, zostały zbindowane do obiektu, main_frame, który swoją wielkością obejmuje niemal całe okno. Był niejako krok zapobiegawczy, ponieważ obiekt main_frame, był kolejnym głównym obiektem, zaraz po root, gdyż nie wiem, czy bind_all nie jest związane w jakiś sposób z hierarchią widget'ów w oknie. Jednak okazało się, że przypisanie "bind_all", tylko do wyznaczonego obiektu, np przycisku, powoduje oczekiwane działanie i skrót można wywołać w każdym miejscu okna.

Inną rzeczą było zapisywanie pliku graficznego posiadającego naniesione markery. Szukałem jak najłatwiejszej metody na osiągnięcie tego celu, jednak okazało się, że nie można w prosty sposób dokonać zapisania canvas'u Tkinter za pomocą biblioteki Pillow. Konieczne jest utworzenie obiektu Image i przekazanie go do klasy ImageDraw, w której następuje dodawanie elementów, do instancji klasy Image. Metody wchodzące w skład klasy ImageDraw, są podobne do metod Tkinter canvas. Wystarczyło pobrać listę wszystkich markerów, skonwertować ich kolory do postaci "RGBA" i nanieść na obraz przeznaczony do zapisania do pliku. W związku z tym implementacja tej funkcjonalności nie była najprostsza, ale nadal okazała się być dość prostym zadaniem.

sobota, 29 kwietnia 2017

Trudne implementacje.

Powoli przygotowuję grunt pod umożliwienie zapisywania zmodyfikowanego zdjęcia, to znaczy zdjęcia z naniesionymi znacznikami. Optymalnie było by zapisywać zmodyfikowane zdjęcie do formatu pozwalającego na dodawanie warstw. W ten sposób każda grupa znaczników znajdowała by się na osobnej warstwie, co mocno ułatwiło by dalszą pracę z plikiem. Nie wiem czy biblioteka "pillow" pozwala na zapis do takiego formatu. Na razie zadowolę się zapisem do któregoś z najpopularniejszych formatów.

Dobrym wyborem wydaje się ".png", ze względu na dobrą jakość obrazu, aczkolwiek kosztem wielkości pliku. Dobrym pomysłem wydaje się również sprawdzanie automatyczne wczytanego pliku i zapis do tego samego formatu. Zwalnia to z ewentualnych problemów związanych z konwersją między formatami. Jednak poza ewentualną utratą jakości obrazu, taka konwersja nie powinna stanowić żadnego problemu.

Najlepszym rozwiązaniem wydaje się również zapis zdjęcia w jego oryginalnym rozmiarze. To znaczy bez skalowania, które odbyło się w oknie programu. Wyszedłem z założenia, że ta zaimplementowana funkcja powiększenia, jest tylko i wyłącznie na potrzeby podglądu.

środa, 26 kwietnia 2017

CSV format popularny chociaż nieznany.

Zaimplementowałem eksport statystyk do pliku w formacie ".csv" (coma separated values). Format ten nie wyróżnia się niczym szczególnym, a plik zapisany z takim rozszerzeniem można otworzyć najprostszym programem do edycji tekstu. Po wczytaniu pliku, pojawią się rzędy wartości, które w kolumnach oddzielone są od siebie średnikami, albo innymi znakami. Pierwszy rząd może stanowić nagłówek, pozwalający na zorientowanie się, czego dotyczą poszczególne kolumny danych.
Plik w tym formacie można bez problemu zaimportować do arkusza kalkulacyjnego, a wczytane dane poddać następnie statystycznej obróbce.

W zastosowanej implementacji nie eksportuję wartości procentowych do pliku. W założeniu wartości te miały pomóc ocenie ilościowej, w przypadku pracy tylko z samym BioCounter'em. Ponadto po importowaniu pliku do arkusza kalkulacyjnego dostępnych jest mnóstwo lepszych narzędzi, pozwalających na wykonanie rozmaitych analiz statystycznych.

Język Python posiada wbudowany moduł ułatwiający zapis pliku do formatu ".csv". Dzięki czemu implementacja eksportu do tego formatu staje się bardzo łatwa. Dodatkowo użyłem również modułu "codecs", pozwalającego na zapisywanie plików z czcionkami zawierającymi polskie znaki diakrytyczne.

niedziela, 23 kwietnia 2017

Małymi krokami, ale do przodu.

Wiele nie napisałem. Zastanawiałem się głównie nad tym, gdzie umieścić opcję eksportu statystyk. Na razie padło na panel "Statistics", ale rozważałem również nad dodaniem tej funkcji do drop down menu "File". Chodzi mi o zachowanie jak największej intuicyjności oraz funkcjonalności. Jednak najczęściej sporo rzeczy wychodzi podczas codziennego użytkowania, wtedy najłatwiej wprowadzać wszelkie udogodnienia.

Sprawdziłem również jak zmienia się rozmiar instancji klasy, w przypadku kiedy zawiera ona statyczną metodę, w porównaniu z instancją, która tej metody nie posiada. Okazało się, że statyczna metoda przypisana do klasy nie zmienia wielkości instancji. Być może robię coś, źle ponieważ po dodaniu metody, która dodawała do instancji klasy kolejny atrybut, wielkość instancji nadal nie uległa zmianie. Nawet po użyciu tej metody i przypisaniu nowego atrybutu do instancji.

Ilość miejsca w pamięci zajmowanej przez dany element programu można sprawdzić poprzez importowanie modułu "sys" i użycie komendy:

sys.getsizeof(element_to_check)

Jest to bardzo wygodne polecenie, do kontroli wielkość poszczególnych elementów w pamięci, w celu optymalizacji pracy programu. Moduł "sys" zawiera dużo więcej przydatnych metod, do sprawdzania działania programu i jego pracy w systemie.

sobota, 22 kwietnia 2017

Błędów nie sposób przewidzieć.

Miałem zamiar zaimplementować eksport statystyk do pliku. Jednak odkryłem kolejny błąd i zająłem się jego naprawą. W tym wypadku debugowanie zajęło mi bardzo dużo czasu. Wiedziałem co nie działa i jak powinno działać, ale nie mogłem odnaleźć miejsca odpowiedzialnego za błędy i go naprawić. Okazało się, że kod wymagał więcej niż korekcji tylko w jednym miejscu.

Przyczyną było skalowanie załadowanego obrazka. Wraz ze zdjęciem skalowaniu powinny ulegać również markery, to znaczy nie one same, ale ich pozycja powinna być odpowiednio przeliczana. Dodatkowo, ważne było, aby obiekt "marker" trzymał informacje dotyczące jego absolutnej pozycji.

To był właściwy problem. Nieodpowiednie przeliczanie pozycji. Podczas dodawania markera do "canvas" nie uwzględniałem skali, więc zarejestrowane pozycje markerów, były obarczone błędem związanym z ustawionym powiększeniem dla wczytanego obrazka. Innym problemem było uwzględnianie skali podczas "rysowania" samego markera. Działanie to jest konieczne, podczas przeliczania pozycji, po zmianie skalowania wczytanego zdjęcia, ale w programie zachodziło za każdym razem, kiedy program nanosił marker na "canvas". Implementacja usuwania markerów i nanoszenia nowych, po zmianie przybliżenia, spowodowała w tym wypadku wydłużenie czasu odnajdywania błędu. Jednak nie znalazłem lepszego sposobu na zaimplementowanie tej funkcjonalności.

Nanoszenie nowego markera jest uwzględniane i podczas tej czynności program nie uwzględnia skali załadowanego obrazka. Natomiast w celu wyliczania i uwzględniania absolutnych pozycji markerów utworzyłem dodatkową klasę posiadającą statyczną metodę. Nie wiem, czy w Pythonie instancja klasy zawiera również statyczne metody, więc wybrałem to rozwiązanie, aby niepotrzebnie nie zapychać pamięci komputera,ponieważ markerów naniesionym na "canvas" może być bardzo dużo.

poniedziałek, 17 kwietnia 2017

Nieprzewidziane funkcje, czyli naprawa błędów.

Nawet w nieskomplikowanym kodzie mogą znajdować się błędy. Problem ten dotyka każdego oprogramowania, nawet tego najprostszego. Przeczytałem kiedyś, że na 100 linijek kodu, w 1-3 znajduje się błąd. Wszakże tekst dotyczył programów napisanych w C++, aczkolwiek problem ten ma uniwersalny wymiar i dotyczy wszelkich języków programowania.

Pierwszy błąd, który znalazłem dotyczył "nieskończonych" wymiarów obiektu Tkinter "canvas". Okazało się, że po wczytaniu zdjęcia, ograniczałem rozmiar "canvas'u", do wymiarów zdjęcia, gdyż było to potrzebne do przypisania belek scroll'owania. Jednak sam "canvas" tworzony był dużo wcześniej i nie posiadał tych ograniczeń. Pomimo, że belki przewijania był niewidoczne, "canvas" można było przewijać w pionie, za pomocą rolki myszy oraz w poziomie trzymając wciśnięty klawisz shift i używając w tym czasie rolki myszy. Nie wiem jakie są realne ograniczenia w wymiarach "canvas", ale ja zaniechałem dalszego przewijania przy 35 tysiącach pikseli w poziomie. Nie wiem nawet, czy eksportując obraz w takiej rozdzielczości udało by się tego dokonać dla jakiegoś sensownego formatu np. jpg. Akurat nie pamiętam, czy te koordynaty były przechowywane jako float, czy integer. Jednak Python jest w stanie przechowywać dużo większe liczby całkowite, niż tradycyjne języki programowania. Udało mi się obliczyć 1000000! i zapisać tą wartość do pliku, bez stosowania żadnych dodatkowych bibliotek.

Drugi błąd dotyczył usuwania naniesionych markerów. Obiekty do usunięcia są rejestrowane, poprzez sprawdzenie, jakie obiekty pokrywają się z "testowym" wirtualnym obiektem, a badane jest to w miejscu naciśnięcia prawego przycisku myszy. Okazało się, że wczytany obraz, zawsze się pokrywał z tym obiektem, a co za tym idzie zawsze był usuwany. Naprawa tego błędu była prosta. Przed usunięciem pokrywających się obiektów, sprawdzane jest, czy dany obiekt przeznaczony do usunięcia posiada "tag" znajdujący się na liście "tag'ów" przypisywanych markerom. Jeśli posiada, to jest usuwany.

Mimo, że znalezione błędy były łatwe do uniknięcia, to mimo tego jakoś przesmyknęły się do programu. Pokazuje to jasno, że nawet w najprostszej funkcjonalności może kryć się nieprzewidziany błąd.

niedziela, 16 kwietnia 2017

Dokumentacja kodu.

Jako, że stworzyłem już sporą część programu, a sprawę tą dotychczas zaniedbywałem, postanowiłem dopisać trochę dokumentacji do kodu. Mimo, że staram się zawsze, aby nazwy zmiennych, funkcji, metod i klas, były jak najbardziej opisowe, to lubię dopisać te kilka dodatkowych linijek dokumentacji. Jest to o tyle dobre, że po dłuższej przerwie ułatwi w przypomnieniu struktury danego programu.

Tworząc ten program nie skupiam się tylko na jednym fragmencie, to znaczy nie tworzę go w całości od początku, do końca. Często zmieniam klasę lub element funkcjonalny programu, do którego dopisuję kod. Nie wiem, czy ta metodyka sprawdzała by się dla innych osób. Jednak mi pomaga zapamiętać strukturę całego programu i wydaje mi się, że jest to związane ze sposobem, w jaki funkcjonuje pamięć. Powtarzając i przypominając sobie cyklicznie o danych fragmentach programu tworzę ich mapę i utrwalam ją w pamięci. Oczywiście nie jest to z dokładnością do jednej zmiennej, czy metody. W budowanym od podstaw programie nie ms to znaczenia, ze względu na często dokonywane zmiany. Jednak pomaga mi w ogólnym rozeznaniu na temat klas i metod.

Inną sprawą jest zmiana aktualnie kodowanego segmentu. Pomaga mi to uniknąć znużenia, które mogło by być związane, z pracą tylko i wyłącznie nad jedną klasą, czy funkcją. Po powrocie do kodowania danego elementu, mam na niego niejako świrze spojrzenie, co pomaga wyłapywać pewne błędy i usprawniać kod. Oczywiście idealnie byłoby pisać kod nie wymagający poprawek i usprawnień, jednak na razie wydaje się to poza moim zasięgiem.

To świeże spojrzenie jest związane z tym jak funkcjonują neurony. Otóż pracując nad jednym elementem, tylko jedna grupa neuronów ulega pobudzaniu wraz z częścią neuronów sąsiadujących. Początkowo siła z jaką są one wzbudzone utrzymuje się na wysokim poziomie, a nawet rośnie. Jednak po pewnym czasie ich aktywność spada, co wiąże się z wyczerpaniem neuroprzekaźników uwalnianych do przestrzeni synapsy. Z takim miejscowym pobudzeniem wiąże się, to że myśli i pomysły są ukierunkowane w pewien sposób.

Prostym dowodem na to jest sytuacja, w której staramy się przypomnieć np. nazwisko aktora, który grał w danym filmie. Może się wydawać, że np. miał nazwisko zaczynające się na literę "B". Intensywnie myślimy wtedy o takich nazwiskach - Bronson, Bogart, itp. Im dłużej o tym myślimy, tym więcej nazwisk się przypomina. Jednak, często brakuje tego jednego, konkretnego nazwiska. Po pewnym czasie rezygnujemy, gdyż nie możemy sobie tego aktora przypomnieć. Odkładamy temat na bok, zmieniamy na inny. Wtedy, często się zdarza, że po dłuższej chwili wpadamy na to konkretne nazwisko, o które chodziło od samego początku.

Jest to właśnie związane wzbudzaniem określonej grupy neuronów, która ukierunkowuje w pewien sposób aktualne myślenie. W socjotechnice często wykorzystuje się podobne działanie neuronów.

sobota, 15 kwietnia 2017

Skalowanie obrazu.

Wydawało by się, że skalowanie obrazu jest czymś bardo prostym. Jednak to zagadnienie jest dużo bardziej skomplikowane, niż początkowo mogło by się wydawać. Przywykliśmy do tej opcji tak bardzo, że w programach graficznych nawet nie zwracamy na nią szczególnej uwagi.Niestety nie znam kodu źródłowego Gimp'a na wcale, więc nie mogę przedstawić jak to wygląda w rzeczywistości, mogę opisać jedynie na jakie trudności natrafiłem implementując to w kodzie
Bio Countera.

Okazało się, że musiałem sporo ograniczyć powiększanie obrazu, ponieważ już kilkukrotnie powiększony obraz zajmował bardzo dużą ilość ramu. Ponadto proces samego skalowania obrazu wydłużał się znacznie, wraz z powiększaniem skali. Nie jestem pewien do końca dlaczego, być może powodem jest to, że PIL przeliczając wielkość obrazu korzysta z CPU zamiast z GPU. Rozwiązaniem tego problemu byłoby skalowanie tylko niewielkiego wycinka obrazu, bezpośrednio widocznego dla użytkownika. Dzięki temu problem niewielkiej ilości dostępnego ramu staje się nieistotny, a czas potrzebny do przeskalowania tylko niewielkiego wycinka obrazu ulega istotnemu skróceniu. Innym rozwiązaniem jest skorzystanie z biblioteki do obróbki obrazu, która do transformacji obrazu będzie wykorzystywała GPU. Chociaż w tym wypadku, problem z rosnącą wielkością obrazu pozostanie.

Na razie nie mam czasu, na skupianie się nad implementacją takich rozwiązań. Pozostało jeszcze sporo do zrobienia i wciąż brakuje pewnych obowiązkowych funkcjonalności programu.

wtorek, 11 kwietnia 2017

Wygoda użytkowania, znaczenie pierwszorzędowe.

Nastąpił niewielki commit, ale znaczący. Wreszcie połączyłem przyciski powiększające i zmniejszające rozmiar markerów, z samymi markerami naniesionymi na Tkinter "canvas". Tym oto sposobem, wszystkie podstawowe funkcjonalności markerów zostały dodane.

Do finiszu, pozostało jeszcze daleko. W najbliższym czasie zamierzam dodać możliwość pomniejszania oraz powiększania wczytanego obrazu. Nie wiem, czy w związku z tym nie będzie konieczne dodanie możliwości automatycznego skalowania wielkości markerów. Bądź umożliwienie włączenia takiej funkcji, poprzez zaznaczenie odpowiedniego "checkbox'a" w panelu opcji.

Pod względem funkcjonalności i wygody użytkowania, przeciętny program od programu dobrego oddzielają właśnie takie drobne rzeczy. Może wydawać się to niuansem, nie wartym uwagi na tym etapie rozwoju programu, gdyż zawsze można wprowadzić zmiany, ale wraz z powiększaniem programu, coraz trudniej czegoś takiego dokonać. Kilkakrotnie spotykałem dobre oprogramowanie, które odpychało przeciętnie wykonanym interfejsem użytkownika. Chyba najlepszym tego przykładem był Blender w wersji do 2.49c (chodzi mi o tą wersję posiadającą stary wygląd). Sam program był bardzo dobry i po zapoznaniu się z niezbędnymi skrótami klawiaturowymi korzystało się z niego bardzo przyjemnie, ale ze względu na przestarzałe GUI, początki pracy z tym programem były dla mnie trudne. W przypadku Blendera na pewnym etapie rozrostu tego programu, postanowiono przepisać cały kod aplikacji i zapadła decyzja, że będzie to również związane z powstaniem nowego GUI.

Zagadnienia związane z neuroergonomią mogą pomóc w zaprojektowaniu wygodnego interfejsu. Jednak mimo tego, że pewne rozwiązania wyglądają doskonale w teorii, nie oznacza, że równie dobrze sprawdzą się w praktyce. Trzeba też brać pod uwagę, że ciężko przewidzieć jak dane rozwiązania sprawdzą się w przypadku, kiedy wraz z rozwojem programu, na danej zakładce przybędzie elementów dodających kolejne funkcjonalności.

Bio Counter w założeniu ma być prostym programem i chciałbym, aby korzystanie z niego było wygodne i przyjemne. Samo umieszczenie opcji pozwalającej na zmianę wielkości markera na odrębnej zakładce, było związane z przypisaniem odpowiednich funkcji, do odpowiednich kategorii GUI. Było to również podyktowane ograniczeniem minimalnej rozdzielczości do 800x600, którą to w późniejszym czasie ograniczyłem do 1024x768, ponieważ stwierdziłem, że ekrany posiadające rozdzielczość 800x600 nie są już stosowane. Dobrym rozwiązaniem wydaje się dodanie do opcji zmiany wielkości markera, jakiegoś łatwego do zapamiętania skrótu klawiaturowego np. alt + rolka myszy. Aczkolwiek z rolką myszy jest już związanych kilka funkcji i chciałbym uniknąć sytuacji, w której staną się one mylące.

niedziela, 9 kwietnia 2017

Usuwanie markerów, zaginiona funkcjonalność.

To było to, czego brakowało w tym programie. To znaczy, kiedy dodałem możliwość nanoszenia markerów na "canvas" nie dałem użytkownikowi możliwości usuwania markerów już istniejących. Konieczność odnotowywania błędów "na kartce", ze względu na brak możliwości korekcji pomyłek w programie, była by bardzo irytująca. Bez niej program, nie nadawałby się do użycia.

Ta funkcjonalność była zaplanowana. Jednak zanim ją zaimplementowałam, chciałem dodać klasę oraz elementy interfejsu odpowiedzialne, za zliczanie klikniętych elementów. Gdyż był to element, niejako będący w centrum, chciałem mieć pewność, że wszystko działa tak jak należy.

Okazało się, że sposób w jaki zaimplementowałem zmianę koloru markerów, spowodował niemałe problemy. Spędziłem sporo czasu, na dojście do tego, co działa nie tak jak powinno i dlaczego nie funkcjonuje zgodnie z moim zamysłem.

Podczas dodawania elementów do "canvas" w Tkinter, są one przechowywane w zmiennej typu "touple", a "nazwa" tych elementów jest zgodna z kolejnością ich dodania. Usuwanie markerów działało doskonale, do czasu aż zmieniałem barwę, którychś z istniejących markerów. Problemem była numeracja tych elementów. Program zmieniając kolory wybranych elementów, nie zmienia tak na prawdę ich barwy. Stare elementy są usuwane z "canvas", a na ich miejsce tworzone są nowe posiadające zaktualizowany kolor. Jako, że program przechowuje również informacje o wszystkich markerach w instancjach odpowiednich klas, zapisanych w liście, powstał problem z powiązaniem elementów między tymi dwoma zbiorami. Marker, który był nr 2 na liście markerów, po zmianie barwy stawał się ostatnim elementem w zbiorze ("touple") obiektów "canvas",

Jako, że ktoś mądry powiedział kiedyś, że o wiele łatwiej jest dodać coś do klasy, niż coś z niej usunąć. Postanowiłem dodać do klasy "Marker" zmienną przechowującą informację odnoszącą się do nazwy danego elementu na "canvas". W przypadku zmiany barwy elementu, podczas ponownego rysowania obiektów na "canvas" ich nazwy przechowywane przez instancję klasy "Marker" są aktualizowane. Rozwiązanie to wydaje się proste i jest również skuteczne.