niedziela, 30 czerwca 2013

Qt4: edytowanie danych w tabeli

W poprzednim artykule opisałem zasady implementowania modelu tabeli, który określa sposób wyświetlania danych w komponencie QTableView. Skoro potrafimy już przedstawić użytkownikom informacje, to warto pokusić się o umożliwienie im modyfikowania wartości. Implementacja modelu będzie składała się z następujących etapów:
  1. Rozszerzenie klasy NowyModel przedstawionej poprzednio przez dodanie do niej metod setData() i flags().
  2. Rozszerzenie QStyledItemDelegate, której zadaniem jest wyświetlanie odpowiedniego edytora wartości na żądanie użytkownika.

1. Model tabeli

W pierwszym kroku trzeba rozszerzyć klasę NowyModel, która z kolei dziedziczy po klasie QAbstractTableModel i nadpisać dwie metody:
  • setData(indeks, wartość, rola) - metoda jest wywoływana, gdy użytkownik zatwierdza nową wartość w wybranej komórce tabeli,
  • flags(indeks) - metoda określa dla wskazanej komórki tabeli, czy można ją edytować.
Model, którego implementację opisywałem poprzednio powodował wyświetlenie danych źródłowych w trzech komórkach. W pierwszej początek zakresu, w drugiej koniec zakresu, a w trzeciej cały zakres sformatowany jako string. W nowej implementacji użytkownik będzie miał możliwość modyfikowania wartości w pierwszej i drugiej kolumnie.

class EdytowalnyModel(NowyModel):

    def __init__(self, wartosci):
        NowyModel.__init__(self, wartosci)
       
       
    def setData(self, indeks, wartosc, rola=Qt.EditRole):
        if rola != Qt.EditRole:
            return
        w, k = indeks.row(), ideks.column() # współrzędne komórki
        if k > 1:
# tylko pierwszą i drugą kolumnę można edytować
            return
        self._dane[w][k] = wartosc.toInt[0] # konwersja QVariant -> int

        self.dataChanged.emit(indeks, indeks)
                                             
                                             
    def flags(self, indeks):
        if indeks.column() <= 1:
            return Qt.ItemIsEditable 

        return Qt.ItemIsSelectable  

Dane źródłowe to lista (pole self._dane), z której każdy element jest wyświetlany jako osobny wiersz w komponencie QTableView. Elementami listy są pary liczb: pierwsza oznacza początek zakresu i jest wyświetlana w pierwszej kolumnie, druga - koniec zakresu - jest wyświetlana w drugiej kolumnie, a w trzecie wartość utworzona z połączenia pierwszej liczby i drugiej. Dlatego wartości w trzeciej kolumnie nie można edytować. Ona będzie się zmieniała automatycznie, gdy zmieni się wartość w pierwszej albo drugiej kolumnie. Trzeba pamiętać, że w tej implementacji nie jest sprawdzana poprawność wprowadzanych wartości, to znaczy użytkownik bez problemu zdefiniuje zakres, w którym pierwsza wartość z pary jest większa od drugiej.


2. Edytor wartości

Naszym użytkownikom chcemy ograniczyć wybór początku i końca zakresu. Dlatego zamiast prostego pola tekstowego będzie wyświetlana lista rozwijana QComboBox, z której będzie wybierał wartość. Aby uzyskać taki efekt trzeba rozszerzyć klasę QStyledItemDeleget, której komponent QTableView używa do określania, jaki edytor ma być wyświetlany dla wybranej komórki tabeli.

class ComboDelegate(QStyledItemDelegate):

    def __init__(self, parent=None):
        QStyledItemDelegate.__init__(self, parent=parent)
        self._dopuszczalne = ['1','2','3','4','5','6','7','8','9']
       
    def createEditor(self, parent, styl, indeks):
        self.initStyleOption(styl, indeks)
        cb = QComboBox(parent)
        cb.addItems(self._dopuszczalne)
        return cb
       
    def setEditorData(self, edytor, indeks):
        wartosc = indeks.data()
        edytor.setCurrentIndex(wartosc-1)
   
    def setModelData(self, edytor, model, indeks):
        model.setData(indeks, edytor.currentIndex()+1)


Implementacja sprowadza się do nadpisania trzech metod:
  • createEditor() - tworzy i zwraca nowy komponent edytora - w tym przypadku QComboBox, z którego można wybrać wartość z zakresu [1; 10]
  • setEditorData() - po utworzeniu edytora i przed wyświetleniem go użytkownikowi trzeba na liście zaznaczyć aktualną wartość. W tym przypadku to jest bardzo proste, gdyż indeks na liście rozwijanej będzie zawsze o 1 mniejszy od wyświetlanej liczby (pobieranej przy pomocy metody indeks.data())
  • setModelData() - metoda wywoływana na zakończenie edycji, jeżeli użytkownik zatwierdzi wybór nowej wartości. Jednym z parametrów jest obiekt klasy EdytowalnyModel, do którego wstawiamy nową wartość przy pomocy zaimplementowanej metody setData().

3. Uruchomienie

def nowaEdytowalnaTabela():
   tv = QTableView()
   dane = [[3, 6], [1, 8], [4, 9]]
   model = EdytowalnyModel(dane)
   tv.setModel(model)
   edytor = ComboDelegate()
   tv.setItemDelegateForColumn(0, edytor)
   tv.setItemDelegateForColumn(1, edytor)

Połączenie wszystkiego w całość jest bardzo proste. Najpierw inicjujemy nową implementację modelu z danymi początkowymi, następnie tworzymy instancję klasy, która wyświetli edytor na żądanie użytkownika i przydzielamy ją do pierwszej i drugiej kolumny metodą setItemDelegateForColumn()

Przedstawione implementacje modelu są bardzo proste i podatne na błędy. Nic nie stoi na przeszkodzie, by wprowadzić błędne dane początkowe (na przykład z wartościami z poza zakresu). Ale rozwiązanie tego problemu pozostawiam P.T czytelnikom jako zadanie domowe ;)

piątek, 31 maja 2013

Qt4 : wyświetlanie danych w tabeli

W implementacji mechanizmu zestawień w QAZP2, nad którą ostatnio pracowałem, do wyświetlania i edycji danych wykorzystałem tabele. W tym artykule chciałbym krótko przybliżyć zasady wyświetlania informacji przy pomocy komponentu QTableView.

W Qt4 postawą działania komponentów wyświetlających dane w postaci tabelarycznej są implementacje tak zwanego modelu tabeli, a dokładnie klasy QAbstractTableModel. Komponent QTableWidget używa swojej domyślnej implementacji, której nie sposób zmienić, ale za to zdefiniowanie nowej tabeli jest bardzo łatwe. Wystarczy podać jej wstępne wymiary, to znaczy liczbę wierszy oraz kolumn, a następnie wprowadzić wartości do komórek. Jeżeli zachodzi taka potrzeba można te wymiary zmieniać dodając kolejne kolumny i wiersze, albo je usuwając. Zastosowanie QTableWidget ma tę zaletę, że chcąc wyłącznie wyświetlać informacje można szybko osiągnąć zamierzony efekt. Schody zaczynają się, gdy dopuszczamy również edycję danych, bo wtedy trzeba śledzić za pomocą sygnałów zdarzenia w poszczególnych komórkach. W pewnym momencie stało się dla mnie denerwujące pisanie pętli, które na podstawie danych źródłowych, dla każdej komórki tworzyły odrębny obiekt klasy QTableWidgetItem zawierający wartość, która ma być wyświetlana, co sprawiało, że przy bardziej skomplikowanych wymaganiach kod stawał się nieczytelny, a na dodatek za każdym razem trzeba pętlę definiować od nowa. Dlatego osobiście preferuję stosowanie klasy QTableView, która tym różni się od QTableWidget, że do jej używania konieczne jest własnoręczne wskazanie modelu tabeli. 

Aby zastosować QTableView w pierwszej kolejności należy zdefiniować nowy model tabeli. Polega to na utworzeniu nowej klasy, która dziedziczy po klase QAbstractTableModel, w której zaimplementowane są conajmniej trzy metody: rowCount(), columnCount() oraz data(). Znaczenia pierwszych dwóch można się łatwo domyślić: rowCount() oblicza i zwraca liczbę wierszy, a columnCount() liczbę kolumn. Natomiast metoda data(indeks, rola) powinna określać wartość, która ma być wyświetlona w komórce określonej przez indeks. Tu uwaga - metoda data() może być wywoływana wielokrotnie dla tej samej komórki.

Przykład

class NowyModel(QAbstractTableModel):
    def __init__(self, dane): # dane reprezentuje dwuwymiarowa lista
         QAbstractTableModel.__init__(self)
         self._dane = dane

    def rowCount(self, parent=QModelIndex()):
        return len(self._dane)

    def columnCount(self, parent=QModelIndex()):
        return 3 #  z góry zakładamy, że tabela ma mieć 3 kolumny

    def data(self, indeks, rola=Qt.DisplayRole):
        if rola == Qt.DisplayRole:
            r, c = indeks.row(), indeks.column() # współrzędne komórki
            if c < 2:
                return self._dane[r][c]
            elif c == 2:
                return "< %d;  %d>" % (self._dane[r][0], self._dane[r][1])
         return None # wartość zwracana, gdy nie zostały spełnione inne warunki

def nowaTabela():
    tv = QTableView()
   dane = [[10, 20], [-8, -3], [0, 5]]
   model = NowyModel(dane)
   tv.setModel(model)

Dane źródłowe składają się z trzech wierszy, z których każdy zawiera dwie wartości oznaczające początek i koniec jakiegoś zakresu liczb. Aby osiągnąć taki efekt, że w każdym wierszu tabeli w pierwszej kolumnie będzie wyświetlany począte zakresu, w drugiej koniec, a w trzeciej cały zakres (sformatowany jako string), została odpowiednio zaimplementowana metoda data(). To na co warto zwrócić uwagę, bo często prowadzi do błędów w wyświetlaniu danych, to drugi parametr o nazwie rola. QTableView wywołuje data() nie tylko po to żeby wyświetlić wartość w komórce, ale także pobrać dane do umieszczenia w edytorze, jeżeli modyfikowanie informacji jest dopuszczalne. Do odróżnienia tych sytuacji służy właśnie wspomniana rola. Jeżeli data() jest wywołana w celu wyświetlania to rola jest równa Qt.DisplayRole, natomiast gdy zachodzi edycja to rola jest równa Qt.EditRole.

Model tabeli można udoskonalić implementując kolejne metody. Jedną z nich jest headerData(sekcja, orientacja, rola), która określa wartości, które mają być wyświetlane w nagłówku. Jej definicja jest podobna do metody data(). Jeżeli orientacja == Qt.Horizontal to sekcja wskazuje na kolumnę tabeli, natomiast jeżeli orientacja == Qt.Vertical  to sekcja określa numer wiersza.  Nagłówki można także modyfikować, jeśli dopuszcza to nasza implementacja i na tą okoliczność należy sprawdzać, podobnie jak w przypadku metody data() wartość parametru rola

Przykład

class NaglowkiModel(NowyModel):

    def __init__(self, dane):
        NowyModel.__init__(self, dane)
        self._naglowki = ["Początek", "Koniec", "Zakres"]

    def headerData(sekcja, orientacja, rola=Qt.DisplayRole):
        if rola == Qt.DisplayRole and orientacja == Qt.Horizontal:
            return self._naglowki[sekcja]
        return None
   
Implementacja modelu tabeli do wyświetlania danych w Qt4 jest bardzo prosta. Ma jeszcze tą zaletę, że po zdefiniowaniu go w jednym miejscu można go stosować wielokrotnie, jeżeli sytuacja na to pozwala. 
W kolejnym artykule spróbuję przedstawić jak używa się QTableView, gdy ma być dopuszczalna edycja wyświetlanych danych.

niedziela, 24 lutego 2013

Tymczasowa warstwa wektorowa w QGIS

Ważnym elementem filtrowania stanowisk według wybranych kryteriów jest ich wyświetlenie na mapie, choćby po to, by przeanalizować ich rozproszenie w przestrzeni. W QAZP2 jest używana tymczasowa warstwa, gdzie QgsVectorDataProvider jest zaimplementowany w ten sposób, że pobiera obiekty prosto z pamięci, a nie z pliku SHP, albo przestrzennej bazy danych.

Tworzenie tymczasowej warstwy wektorowej razem z zawartością jest dość proste i składa się z następujących kroków:
  1. Utworzenie pustej warstwy wektorowej przez wywołanie konstruktora klasy w następujący sposób: QgsVectorLayer(typGeometryczny, nazwa, 'memory'), gdzie wartość typGeometryczny powinna mieć na przykład wartość 'Point', albo 'Polygon', czy 'Line'. 
  2. Rozpoczęcie edycji przez wywołanie metody QgsVectorLayer.startEditing().
  3. Określenie atrybutów warstwy przez podanie ich listy. Każdy z nich jest reprezentowany przez klasę QgsField. W warstwie tymczasowej można używać tylko następujące typy pól: QVariant.String, QVariant.Double oraz QVariant.Int
  4. Dodanie obiektów QgsFeature, które mają być wyświetlane na warstwie.
  5. Zatwierdzenie edycji.
Funkcja qgsop.tempWarstwa() jest przykładem implementacji powyższego algorytmu.

niedziela, 17 lutego 2013

Golang : obsługa bazy Spatialite

W QAZP2 pojawiła się nowa funkcja, moim zdaniem jedna z ważniejszych, czyli filtrowanie danych. Póki co można ją stosować tylko do stanowisk i polega na wybieraniu z listy stanowisk tych, które spełniają wybrane kryteria. A więc w pierwszym kroku wyszukujemy stanowiska znajdujące się na przykład na pewnym obszarze, a następnie spośród nich stanowiska, które mają dużą wartość dla badań nad przeszłością i wobec których istnieje zagrożenie zniszczenia. To oczywiście tylko przykład, bo praktycznie stanowiska można filtrować w oparciu o większość atrybutów edytowanych w programie. A prezentację sposobu stosowania tego mechanizmu można obejrzeć na krótkim filmie.

To by było na tyle, jeżeli chodzi o ostanie wydarzenia, bo dzisiaj chciałbym dokończyć temat z poprzedniego tygodnia, czyli wykorzystanie języka Go do realizacji projektu QAZP2. Wtedy omówiłem kilka interesujących właściwości Go, które odróżniają go od dobrze znanych C/C++ a także popularnych języków obiektowych, na przykład Javy. Go (Golang) charakteryzuje się bogatą biblioteką standardową, która umożliwia realizację podstawowych zadań, bez konieczności instalacji dodatkowych zależności. W pozostałych przypadkach można się posłużyć intuicyjnym i wygodnym mechanizmem zarządzania nimi. Na przykład do pobrania sterownika do bazy SQLite/Spatialite. Istnieje kilka alternatywnym projektów, w ramach których jest on rozwijany, ja zdecydowałem się na Go-Sqlite3. Jak już pisałem do jego instalacji wystarczy wykonać polecenie
go get github.com/mattn/go-sqlite3
, które po pierwsze pobierze kod źródłowy utrzymywany w repozytorium GIT i po drugie skompiluje go, jeżeli spełnione są wszystkie zależności. To jest proste nie tylko w teorii, z tym że w normalnej postaci sterownika nie można używać do baz przestrzennych. Wynika to z tego, że przed wykonaniem jakiegokolwiek polecenia SQL, które dotyczy danych geometrycznych należy załadować rozszerzenie spatialite, na przykład stosując polecenie (na linuksie) select load_extension('libspatialite.so.3'). Do tego, by ono zadziałało trzeba wcześniej zmodyfikować kod źródłowy sterownika w ten sposób, by w chwili "łączenia się" z bazą umożliwiał ładowanie rozszerzeń, która to możliwość jest domyślnie zablokowana.
Dowolny program może korzystać z bazy SQLite przy pomocy biblioteki funkcji, których używa się na przykład do otwierania bazy, wykonywania poleceń SQL i pobierania wyników. Wśród nich jest także funkcja sqlite3_enable_load_extension, która służy (jak sama nazwa wskazuje ...) do włączenia możliwości ładowania rozszerzeń. Trzeba ją wykonać po otworzeniu pliku z bazą danych. A więc w pliku sqlite3 , wewnątrz funkcji Open, przed deklaracją zwracającą wskaźnik do nowego połączenia z bazą należy wstawić następujące polecenie: 
C.sqlite3_enable_load_extension(db, C.int(1)). I to wszystko. Po otworzeniu bazy zostanie wywołana ww. funkcja, gdzie parametrami są wskaźnik do bazy i liczba 1 oznaczająca True. Po nawiązaniu połączenia wystarczy wykonać polecenie SQL, które spowoduje załadowanie rozszerzenia Spatialite i od tej chwili można przetwarzać dane geometryczne zapisane w przestrzennej bazie danych.

sobota, 9 lutego 2013

Golang : import danych AZP

Jednym z ważniejszych aspektów związanych z projektem QAZP było przygotowanie narzędzia, które (w miarę) bezboleśnie pozwoli na "przerzucenie" danych utworzonych w programie AZPMAX do relacyjnej bazy SQL. Oczywiście pierwszą decyzją był wybór języka programowania, który z wiadomych względów padł na Jave, choćby z tego powodu, że mam sporo doświadczeń związanych z tworzeniem narzędzi obsługujących zarówno DBF-y jak i bazy danych. Niestety musiałem z tego zrezygnować, bo mimo wcześniejszych pozytywnych wyników, tym razem sterownik do SQLite odmówił współpracy z danymi przestrzennymi. Przy próbie inicjalizacji rozszerzenia Spatialite JVM przestawała działać. W związku z tym zdecydowałem się stworzyć narzędzie do importu w języku Golang, który od kilku lat jest rozwijany przez dobrze znaną firmę Google. Ten wybór też ma uzasadnienie, biorąc pod uwagę, że:
  1. mam już pewne doświadczenie związane z tworzeniem oprogramowania w Go,
  2. dobra obsługa baz sql, w tym Sqlite.
Na minus trzeba zaliczyć brak obsługi DBF-ów, ale to póki co rozwiązałem kopiując dane z plików CSV, a do tego formatu można łatwo wyeksportować DBF, posługując się na przykład arkuszem kalkulacyjnym.
Golang / Go ma kilka interesujących właściwości, które przyniosły mu już dużo rozgłosu i powodują, że język dość szybko zyskuje na popularności. Jest statycznie typowany, ale określenie typu zmiennej można przerzucić na kompilator. Wydaje mi się, że nie można go uznać za niskopoziomowy, bo nie umożliwia na przykład wykorzystywanie assemblera, natomiast różnorakie porównania wykazują, że w wielu przypadkach szybkością dorównuje programom pisanym w C / C++. Nad tymi dwoma ma przewagę (przec co niektórych uznawaną za wadę) automatycznego zarządzania pamięcią (garbage collector). Programy można tworzyć w modelu strukturalnym - do reperentowania danych używa się list, map, i struktur bardzo podobnych do tych stosowanych w C / C++, albo takim, który oferuje abstrakcję i polimorfizm (ale bez dziedziczenia) znany z paradygmatu programowania obiektowego. Realizuje się to przy pomocy tak zwanych interfejsów, które nieco przypominają konstrukcje o tej samej nazwie, które stosuje się w Javie. W Golang interfejs jest pewnym kontraktem, który umożliwia tworzenie polimorficznych obiektów. Natomiast inny jest sposób ich stosowania. O ile w Javie, jawnie trzeba podać, że klasa jest implementacją, o tyle w przypadku Golang jest to automatycznie rozpoznawane przez kompilator. Każdy typ danych, dla którego zaimplementowano funkcje wskazane w definicji interfejsu staje się automatycznie jego implementacją. W narzędziu do importu, o którym mowa został zdefiniowany interfejs Tabela z jedną metodą Params, która zwraca tablicę wartości dowolnego typu. Każda struktura, która implementuje interfejs może być zastosowana w metodzie dodaj(ps *sql.Stmt, t Tabela, spr bool), która wykonując metodę Params pobiera wartości ze struktury w takiej kolejności, które powinny być zastosowane jako parametry polecenia SQL reprezentowanego przez inny interfejs Stmt zdefiniowany w bibliotece standardowej Golang.
W tym krótkim opisie trudno wskazać wszystkie właściwości Go, ja skupiłem się tylko na tych, które były istotne z uwagi na problem, który musiałem rozwiązać. W następnym poście opisałem, jak "zmusić" sterownik Go do obsługi danych geometrycznych zapisanych w przestrzennej bazie danych.

niedziela, 3 lutego 2013

Implementacja nowych sygnałów Qt w Pythonie

Dzisiaj będzie krótko, w ramach uzupełnienia do poprzedniego artykułu. Opisałem w nim krótko koncepcję obsługi zdarzeń w Qt4, natomiast teraz chciałbym to uzupełnić o definiowanie nowych sygnałów dla widgetów tworzonych w Pythonie.
Zasada jest dość prosta, bo opiera się na stosowaniu metody pyqtSignal(args, name=nazwa_sygnalu), której zadaniem jest utworzenie nowego sygnału.

Przykład (tworzenie nowego sygnału)

from PyQt4.QtCore import pyqtSignal
from PyQt4.QtGui import QWidget, QPushButton

class NowyWidget(QWidget):
    
    def __init__(self, parent):
        QWidget.__init__(self, parent)
        btn = QPushButton('Kliknij')
        btn.clicked.connect(self.btnClick)

    widgetSygnal = pyqtSignal(str, name='widgetSygnal')

    def btnClick(self, ch):
        self.widgetSygnal.emit('Witaj')

def nowySlot(wiadomosc):
    print wiadomosc

def main():
    wgt = NowyWidget()
    wgt.widgetSygnal.connect(nowySlot)

Ze zdarzeniem QPushButton.clicked został połączony slot w postaci metody btnKlik zdefiniowany w klasie NowyWidget. Gdy użytkownik kliknie w przycisk, zostanie ona uruchomiona, a następnie wyemitowany nowy sygnał widgetSygnal, który został utworzony przy pomocy wspomnianej funkcji pyqtSignal. Zgodnie z jej wywołaniem slot, który podłączymy do sygnału powinien przyjmować jeden argument typu str, tak jak metoda nowySlot, która wyświetla otrzymaną wiadomość na ekranie.

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.