czwartek, 31 stycznia 2013

Obsługa zdarzeń w Qt 4/PyQt

Na codzień tworzę aplikacje w Javie i to przez używanie SWING-a ukształtowało się moje podejście do obsługi zdarzeń, które mają miejsce w komponentach graficznych wyświetlanych użytkownikom. Stąd, kiedy zaczynałem swoją przygodę z Qt 4 i PyQt pewną niespodzianką był mechanizm obsługi zdarzeń, który zastosowano w tej bibliotece. Odbiega od tego z Javy, gdzie robi się to za pomocą tak zwanych listenerów, czyli klas implementujących pewien interfejs definiujący metodę, która jest wywoływana jako rezultat zdarzenia. Na przykład kliknięcie na przycisk powoduje między innymi uruchomienie metody actionPerformed z interfejsu ActionListener, którego implementację rejestruje się dla każdego przycisku.
W przypadku Qt stosuje się tak zwane sygnały (signals) i sloty (slots). Zasada jest tutaj dość prosta - komponent graficzny - na przykład przycisk QPushButton w momencie, gdy zostanie kliknięty emituje sygnał, którego etykieta wygląda następująco clicked(bool). Takie sformułowanie oznacza, że w reakcji na kliknięcie przycisku do podanego slotu zostanie przekazana wartość logiczna (typ bool). Łatwo się domyślić, że rolę slotu pełni inna metoda, która w reakcji na zdarzenie powinna wykonać jakąś operację. Jedynym wymaganiem, jakie musi spełniać slot, to akceptować taką samą liczbę parametrów, tego samego typu, jak to jest zdefiniowane dla sygnału, z którym jest związany. W opisywanym przypadku metoda - slot ma zdefiniowany jeden parametr, przez który będzie przekazana wartość logiczna.
Jednak aby wystąpiła reakcja na zdarzenie, trzeba wybrany sygnał połączyć ze zdefiniowanym slotem. Kontynuując przykład z przyciskiem i sygnałem clicked(bool) utwórzmy metodę klikniecie(zaznaczony). Gdy połączymy sygnał clicked i metodę klikniecie, to w chwili, gdy użytkownik naciśnie za pomocą myszy przycisk, zostanie wywołana metoda klikniecie, a parametr zaznaczony otrzyma wartość logiczną True albo False. Jeżeli tworzymy interfejs przy pomocy PyQt to istnieje kilka sposobów na zdefiniowanie połącznia:
  1. stosując metodę QObject.connect(QObject nadajnik, QtCore.SIGNAL(etykieta), QObject odbiornik, QtCore.SLOT(etykieta)). Nadajnik to obiekt - źródło sygnału, natomiast odbiornik, to obiekt, w którym znajduje się metoda - slot. SIGNAL i SLOT to dwie metody, które służą do określenia sygnału i slotu. Robi się to podając ich etykiety zapisane w postaci łańcucha znaków. Na przykład QObject.connect(przycisk, SIGNAL("clicked(bool)"), poleTxt, SLOT("klikniecie(zaznaczony)")).
  2. Drugi ze sposobów, to pewne uproszczenie pierwszej i polega na tym, że zamiast podawać wprost obiekt - odbiornik i etykietę metody, można bezpośrednio wskazać jej referencję, czyli: QObject.connect( przycisk, SIGNAL("clicked(bool)"), poleTxt.klikniecie).
  3. Wreszcie ostatni ze sposobów, najbliższy konwencji tworzenia programów w Pythonie. Sygnały w PyQt zostały zaimplementowane w ten sposób, że każdy z nich jest także obiektem. Możemy się więc odwołać do niego bezpośrednio i wskazać docelową metodę, która powinna być wywołana w reakcji na zdarzenie: przycisk.clicked.connect(poleTxt.klikniecie).
Poniższy kod prezentuje krótki program, w którym zastosowano wszystkie opisane wcześniej metody obsługi zdarzeń w Qt 4 / PyQt
    class Program(object):
        def __init__(self):
            przycisk = QPushButton("Przycisk")
            QObject.connect(przycisk, SIGNAL("clicked(bool)"), self, SLOT("klikniecie(zaznaczony)"))
            QObject.connect(przycisk, SIGNAL("clicked(bool)"), self.klikniecie))
            przycisk.clicked.connect(self.klikniecie)

        def klikniecie(self, zaznaczony):
            print 'klikniecie przycisku ', zaznaczony

W kolejnym artykule opisałem zasadę tworzenia sygnałów Qt w Pythonie.

wtorek, 22 stycznia 2013

Obsługa plików binarnych w Qt i Sqlite3

Jednym z elementów, które obowiązkowo muszą znaleźć się na karcie KEZA, wypełnianej dla każdego stanowiska w ramach badań Archeologicznego Zdjęcia Polski, jest mapa przedstawiająca jego położenie i ewentualnie kształt (jeżeli taki określono). W QAZP2 to zadanie jest realizowane w ten sposób, że plik zawierający mapę w formacie PNG, JPEG i innych obsługiwanych przez Qt po wskazaniu przez użytkownika jest zapisywany w bazie danych, tej samej, gdzie przechowywane są pozostałe informacje o badaniach. A dokładnie w tabeli MEDIA, gdzie oprócz samego pliku dodawane są jeszcze takie informacje jak jego format, oryginalna nazwa, a także tabelę, z którą jest związany rekord (np. STANOWISKA, TRASY, itd.). Z kolei informacja, z którego dokładnie stanowiska dotyczy określony plik jest zapisana w tabeli ST_MEDIA, gdzie wprowadzony jest identyfikator rekordu z tabeli MEDIA oraz identyfikator z tabeli STANOWISKA, tworząc tym samym połączenie między tymi relacjami. 
W bazach SQLITE do zapisu danych binarnych zapisuje się pola o typie BLOB. Przy pomocy klasy QImage obrazek jest odczytywany z pliku wskazanego przez użytkownika, a następnie zamieniany na sekwencję bajtów przez wczytanie do bufora QBuffer. Z kolei z tego dane są pobierane jako tablica bajtów QByteArray, którą zamieniamy na ciąg znaków. Taka procedura jest konieczna, gdyż pozwala w ostatecznym efekcie uzyskać bufor ale w reprezentacji, którą obsługuje sterownik bazy danych. Taki obiekt można już przekazać jako parametr w poleceniu SQL. Przebieg opisanej procedury można prześledzić w funkcji media.zapiszMapa(). Odczytywanie danych binarnych z bazy i konwertowanie ich na obiekt, który można wydrukować albo po prostu wyświetlić na ekranie przy pomocy Qt jest dużo prostsze. Po pierwsze za pomocą metody QByteArray.fromRawData(dane) konwertuje się wartość pobraną z pola BLOB na wspominaną już tablicę bajtów. Następnie wywołanie metody QImage.fromData(tablica) spowoduje utworzenie obiektu QImage na podstawie bajtów znajdujących się w tablicy utworzonej przed momentem.

wtorek, 15 stycznia 2013

Budowanie wykazów nazw

Ostatnimi czasy dużo się działo w kwestii QAZP2. Ostatnie poprawki błędów, niekiedy dość poważnych, nowe funkcjonalności - na przykład wykonywanie skryptów modyfikujących zawartość bazy danych - co może mieć znaczenie, gdy osoba odpowiadająca za techniczny aspekt bazy połączonej z QAZP2 nie ma do niej bezpośredniego dostępu. W takim wypadku wystarczy spreparować skrypt SQL i wysłać do użytkownika, który za pomocą interfejsu graficznego aplikuje przygotowane zmiany. Rzecz jasna taki mechanizm jest pewnym zagrożeniem dla stanu bazy danych, jednak jest to koszt, który można ponieść, pamiętając, by nie stosować skryptów z niepewnego źródła. Między innymi z tego powodu trudno było mi znaleźć czas i spełnić obietnicę, że nowe artykuły będą wprowadzane conajmniej raz na tydzień.

Ostatnimi czasy na blogu była poruszana kwestia wykazów pojęć archeologicznych, których używa się do klasyfikacji kulturowo-funkcjonalno-historycznej. W QAZP2 wykorzystywane są do tego wykazy przygotowane w Narodowym Instytucie Dziedzictwa i uzupełnione o nazwy, których obecności wymaga praktyka badań archeologicznych. Ich konstrukcję próbowałem opisać w poprzednim artykule, natomiast dzisiaj chciałbym przedstawić inne podejście, które chociaż nie zostało wdrożone, to wydaje się ciekawą alternatywą dla wyżej wspomnianego sposobu budowania wykazów.