czwartek, 22 maja 2014

Zestawienia: nieformalnie o podstawach języka SQL - część 3

Trzecia i ostatnia część cyklu poświęconego podstawom języka SQL. W pierwszej i drugiej części stworzyliśmy polecenie SQL, które na podstawie zdefiniowanych kryteriów wyszukuje w bazie danych stanowiska i powiązane z nimi fakty kulturowe (określenie okresu dziejów, jednostki kulturowej, funkcji a także znalezionych śladów działalności człowieka). To polecenie ma następującą postać:

SELECT S.ID, S.OBSZAR_AZP, S.NR_AZP, F.FUNKCJA FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO WHERE S.OBSZAR_AZP = '55-12' AND F.KULTURA IS NOT NULL AND S.DATA_BADAN >= '2012-01-01' AND S.DATA_BADAN <= '2012-12-31'

Wskazujemy w nim, że w zestawieniu ma znaleźć się identyfikator stanowiska, numer obszaru AZP, numer stanowiska w ramach obszaru AZP - te atrybuty są pobierane z tabeli Stanowiska oznaczonej literą 'S' oraz fukcję stanowiska - informacja zapisana w tabeli FaktyKulturowe. Pod uwagę są brane stanowiska z obszaru '55-12', które mają określoną funkcję i zostały przebadane między 1 stycznia 2012 i 31 grudnia 2012 roku.

Istotą wielu zestawień są obliczenia, które można także wykonywać przy pomocy języka SQL. Służą do tego tak zwane funkcje agregujące. Nazywamy je w ten sposób, ponieważ zasadą ich działania jest to, że zbierają wyniki z wierszy, które spełniają podane warunki i przetwarzają je w określony sposób. 

Przykładem funkcji agregującej jest funkcja COUNT(nazwa_atrybutu), która oblicza ile razy pojawiła się wartość w kolumnie, której nazwę wpisaliśmy między nawiasami okrągłymi (w miejscu oznaczonym jako nazwa_atrybutu). Na przykład, jeżeli w poleceniu SQL użyjemy funkcji COUNT, a w jej parametrze podamy kolumnę id z tabeli stanowiska, to policzy wszystkie wiersze, które mają w tej kolumnie jakąś wartość.

SELECT COUNT(S.ID) FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO WHERE S.OBSZAR_AZP = '55-12' AND F.KULTURA IS NOT NULL AND S.DATA_BADAN >= '2012-01-01' AND S.DATA_BADAN <= '2012-12-31'

Wynik powyższego polecenia można zinterpretować jako liczbę stanowisk zapisanych w bazie danych z obszaru '55-12', które mają określoną funkcję i zostały przebadane między 1 stycznia 2012 i 31 grudnia 2012 roku. Warto zauważyć, że wynik takiego polecenia będzie zawsze zawierał dokładnie jeden wiersz z jedną kolumną, w które zostanie wyświetlona liczba stanowisk spełniających kryteria.

Innym przykładem funkcji agregującej jest SUM(nazwa_atrybutu), która dodaje do siebie wartości zapisane w kolumnie nazwa_atrybutu. Wartościami tymi muszą być liczby. Możemy jej użyć do obliczenia łącznej liczby fragmentów ceramiki znalezionych na stanowiskach.


SELECT SUM(F.CERAMIKA) FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO WHERE S.OBSZAR_AZP = '55-12' AND F.KULTURA IS NOT NULL AND S.DATA_BADAN >= '2012-01-01' AND S.DATA_BADAN <= '2012-12-31'

Ponownie wynikiem będzie dokładnie jeden wiersz zawierający sumę wartości z kolumny CERAMIKA w tabeli FaktyKulturowe, w wierszach, które spełniają podane kryteria.

W jednym poleceniu SQL można użyć więcej niż jednej funkcji agregującej. Na przykład:

SELECT COUNT(S.ID), SUM(F.CERAMIKA) FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO WHERE S.OBSZAR_AZP = '55-12' AND F.KULTURA IS NOT NULL AND S.DATA_BADAN >= '2012-01-01' AND S.DATA_BADAN <= '2012-12-31'

Wynikiem polecenia będzie dokładnie jeden wiersz z dwoma kolumnami. W pierwszej zostanie wyświetlona liczba stanowisk, w drugiej liczba fragmentów ceramiki, które zostały na nich znalezione. Inne przydatne funkcje agregujące to MIN - określa minimalną wartość w kolumnie, MAX - maksymalną wartość w kolumnie, AVG - oblicza średnią wartości w kolumnie.

Ważnym aspektem używania funkcji agregujących jest grupowanie wyników. Wyobraźmy sobie, że chcemy uzyskać liczbę stanowisk zapisanych w bazie danych z podziałem na jednostki kulturowe. W takim wypadku polecenie SQL powinno mieć taką postać:

 
SELECT F.KULTURA, COUNT(S.ID) FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO GROUP BY F.KULTURA

Na pierwszy rzut oka widać, że polecenie jest bardzo podobne do tych, które opisałem wcześniej. Mamy funkcję agregującą COUNT(S.ID), która policzy wszystkie stanowiska. Oprócz tego mamy atrybut KULTURA, według którego zostaną pogrupowane i policzone stanowiska. Wynik polecenia będzie zawierał dwie kolumny i tyle wierszy, ile różnych kultur zostało znalezionych w tabeli FaktyKulturowe. Do nazwy każdej kultury, w drugiej kolumnie wiersza będzie przypisana liczba oznaczająca liczbę stanowisk reprezentujących jednostkę z kolumny pierwszej.

Oprócz tego w poleceniu pojawiło się wyrażenie GROUP BY F.KULTURA. Używając grupowania w poleceniach sql trzeba jawnie podać, według której kolumny (albo wielu kolumn) mają być agregowane wyniki. Należy pamiętać, że w poleceniach grupujących, między słowami SELECT i FROM mogą znaleźć się tylko funkcje agregujące i nazwy atrybutów, według których są łączone wartości. Te same nazwy atrybutów muszą znaleźć się w wyrażeniu GROUP BY.

W ramach ćwiczenia spróbuj napisać polecenie obliczające średnią ilość fragmentów ceramiki na wszystkich stanowiskach w bazie danych z podziałem na numer obszaru AZP i porównaj to z prawidłową odpowiedzią poniżej.

SELECT S.OBSZAR_AZP, AVG(F.CERAMIKA) FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO GROUP BY S.OBSZAR_AZP

 

środa, 21 maja 2014

Zestawienia: nieformalnie o podstawach języka SQL - część 2

W poprzednim poście przedstawiłem proste polecenie SQL, które wyświetla wszystkie stanowiska archeologiczne z przypisanymi do nich informacją o funkcji stanowiska (osada, cmentarzysko, itp.). Ma ono następującą postać:

SELECT S.ID, S.OBSZAR_AZP, S.NR_AZP, F.FUNKCJA FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO

Fragment S.ID, S.OBSZAR_AZP, S.NR_AZP, F.FUNKCJA oznacza, że mają zostać pobrane wartości z następujących kolumn: identyfikator stanowiska, numer obszaru  AZP, numer stanowiska w ramach obszaru AZP, funkcja stanowiska.

Fragment FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.ID oznacza, że polecenie SQL dotyczy tabeli Stanowiska oznaczonej skrótem 'S' oraz tabeli FaktyKulturowe oznaczonej jako 'F'. Spójnik LEFT OUTER JOIN łączący nazwy tabel spowoduje, że nawet jeżeli stanowisko nie ma określonego ani jednego faktu kulturowego, to i tak zostanie wyświetlone (wtedy w miejscu funkcji pojawi się pusta wartość). Z kolei  pisząc ON S.ID = F.STANOWISKO, informujemy, że każdy fakt kulturowy z tabeli FaktyKulturowe, w kolumnie (atrybucie) STANOWISKO ma zapisany identyfikator stanowiska, do którego należy. Aby wyświetlić tylko te stanowiska, które mają conajmniej jeden fakt kulturowy zamiast spójnika LEFT OUTER JOIN należy użyć JOIN (zaznacz aby zobaczyć prawidłową odpowiedź).

Powyższe polecenie wyświetli wszystkie stanowisk oraz ich funkcje zapisane w bazie danych. Natomiast naszym celem jest pobranie tylko stanowisk z obszaru AZP 55-12. Żeby osiągnąć taki efekt, musimy sformułować kryterium, którego spełnienie będzie warunkiem wyświetlenia wiersza z wartościami. W opisanym przpadku zapiszemy je następująco:

WHERE S.OBSZAR_AZP = '55-12'

Oznacza ono tyle, że w każdym wierszu wartość w kolumnie OBSZAR_AZP tabeli Stanowiska zostanie porównana z numerem '55-12'. Jeżeli jest z nim identyczna, to takie stanowisko zostanie uwzględnione w wynikach polecenia. Jeżeli nie - zostanie pominięte.

Możemy rozbudować nasz warunek i dodać drugie kryterium - na liście z wynikami mają znaleźć się tylko stanowiska, które mają określoną jednostkę kulturową (KPL, Kultura łużycka, itp.). Żeby uzyskać taki efekt warunek powinien wyglądać tak:

WHERE S.OBSZAR_AZP = '55-12' AND F.KULTURA IS NOT NULL

Kryterium dotyczące obszaru AZP oraz kultury muszą być obowiązkowo spełnione dla każdego wiersza wyników, dlatego połączyliśmy je operatorem logicznym AND. Gdyby wystarczające byłoby spełnienie tylko jednego z wymagań, to zamiast AND powinniśmy użyć (per analogiam) operatora OR.

F.KULTURA IS NOT NULL (w wolnym tłumaczeniu: "Kultura nie jest nullem") oznacza, że wartość w kolumnie KULTURA w tabeli FaktyKulturowe. W odwrotnej sytuacji, gdyby zależało nam tylko na stanowiskach bez określonej kultury - powinniśmy napisać F.KULTURA IS NULL (tłum. "Kultura jest nullem").

Możemy też użyć innych operatorów matematycznych: większy, mniejszy lub równy, mniejszy, itd. Dodajmy do warunków kolejne kryterium - wszystkie stanowiska powinny być przebadane po 1 stycznia 2012 roku:

WHERE S.OBSZAR_AZP = '55-12' AND F.KULTURA IS NOT NULL AND S.DATA_BADAN >= '2012-01-01'

W rezultacie otrzymaliśmy następujące polecenie:

SELECT S.ID, S.OBSZAR_AZP, S.NR_AZP, F.FUNKCJA FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO WHERE S.OBSZAR_AZP = '55-12' AND F.KULTURA IS NOT NULL AND S.DATA_BADAN >= '2012-01-01'

Zastanów się jak, wykorzystując dotychczasową wiedzę, zmienić powyższe polecenie,  aby otrzymać tylko stanowiska z okresu między 1 stycznia 2012 i 31 grudnia 2012 i porównaj z prawidłową odpowiedzią (zaczernioną).

SELECT S.ID, S.OBSZAR_AZP, S.NR_AZP, F.FUNKCJA FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO WHERE S.OBSZAR_AZP = '55-12' AND F.KULTURA IS NOT NULL AND S.DATA_BADAN >= '2012-01-01' AND S.DATA_BADAN <= '2012-12-31'

c.d.n

niedziela, 11 maja 2014

Zestawienia: nieformalnie o podstawach języka SQL - część 1

Przez kilka ostatnich dni intensywnie pracowałem nad mechanizmem zestawień w QAZP2. Kod można już pobrać z repozytorium, ale nowa wersja nie jest wydana. Po tych zmianach użytkownik może definiować zestawienia, a dokładnie rzecz ujmując - polecenia SQL, w sposób bardzo zbliżony do tego, jak się to robi w takich programach jak Microsoft Access, albo LibreOffice Base. Czyli do wyszukania pożądanych informacji teoretycznie nie jest potrzebna znajomość języka SQL, bo wszystkie parametry zestawienia - takie jak wyświetlane kolumny, albo warunki ograniczające wyniki, można wyklikać, przy pomocy graficznego interfejsu użytkownika.

Jednak w praktyce okazuje się, że przy tworzeniu zestawień podstawowa znajomość SQL jest jak najbardziej na miejscu, szczególnie, gdy planujemy utworzenie jakiegość skomplikowanego podsumowania. I o tych podstawach, które moim zdaniem pokrywają 90% potrzeb przeciętnego użytkownika bazy danych, jest ten artykuł.

Wyobraźmy sobie bazę danych w której znajdują się dwie tabele: Stanowiska oraz FaktyKulturowe. Tabela Stanowiska ma następujące kolumny (atrybuty):
  • id - niepowtarzalny identyfikator stanowiska,
  • obszar_azp - nr obszaru AZP,
  • nr_azp - nr stanowiska w ramach obszaru AZP,
  • data_badan - data przeprowadzonych badań.
Tabela FaktyKulturowe zawiera informacje o śladach pobytu człowieka (np. fragmentach ceramiki, narzędziach, itp.) - przesłankach do uznania jakiegoś miejsca za stanowisko archeologiczne i określenia jego chronologii. Tabela ma zdefiniowane następujące kolumny ( atrybuty):
  • id - niepowtarzalny identyfikator faktu
  • stanowisko - identyfikator stanowiska, którego dotyczy fakt. Pojedyncze stanowisko może mieć przypisany jeden lub więcej faktów, lub nie mieć przypisanych faktów wogóle.
  • okres - okres dziejów - na przykład: Neolit, Nowożytność, epoka Żelaza, itd.,
  • kultura - jednostka kulturowa - na przykład Kultura Pucharów Lejkowatych,
  • funkcja - funkcja stanowiska - np. osada, cmentarzysko, obozowisko, itd.
  • liczba fragmentów ceramiki

Szablon polecenia wyszukującego dane w bazie SQL można przedstawić następująco:

SELECT 
      lista_atrybutów 
FROM 
      lista_tabel_oraz_widoków 
[WHERE 
      lista_warunków
[GROUP BY 
      lista_atrybutów_grupujących].

Postawą każdego zestawienia są tabele (conajmniej jedna), które przeszukuje się pod kątem określonych kryteriów i z których pobiera się wartości, które następnie są wyświetlane użytkownikowi. W poleceniu wyszukującym dane (w skrócie w poleceniu SELECT) tabele, które są potrzebne do wykonania zestawienia określa się w miejscu, które oznaczyłem jako lista_tabel_oraz_widoków.

Na takiej liście może znajdować się tylko jedna tabela i wtedy za wyrazem FROM wystarczy wstawić jej nazwę. Sprawa komplikuje się, gdy zestawienie dotyczy więcej niż jednej tabeli. Wyobraźmy sobie przypadek, w którym chcemy wyszukać wszystkie osady na obszarze AZP 55-12. Informacja o funkcji stanowiska jest zapisana w tabeli FaktyKulturowe. A informacja o obszarze AZP, do którego należy Stanowisko w tabeli Stanowiska. Stąd prosty wniosek, że w zestawieniu musimy użyć obu tabel.

Aby to zrobić musimy określić w jaki sposób stanowisko jest połączone z faktami kulturowymi. Tabela FaktyKulturowe ma atrybut stanowisko, który zawsze ma wartość odpowiadającą identyfikatorowi stanowiska, zapisywanego w atrybucie id w tabeli Stanowiska. Dysponując tą wiedzą możemy zbudować listę tabel:

... FROM Stanowiska S JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO ...

W ten prosty sposób przekazaliśmy, że wszystkie potrzebne informacje znajdują się w połączonych tabelach Stanowiska oraz FaktyKulturowe. Tak utworzona lista ma jedną wadę - stanowiska, które nie mają przypisanych faktów kulturowych zostaną pominięte. Czasami ma to uzasadnienie, a czasami nie. Aby spowodować, że Stanowisko zostanie wyświetlone nawet, jeżeli nie ma faktów kulturowych, powyższy fragment należałoby przepisać do takiej postaci:

... FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO ...

Zamiast pisać JOIN, użyliśmy wyrażenia LEFT OUTER JOIN. Jeżeli dla danego stanowiska nie ma w tabeli FaktyKulturowe takiego wiersza, który by zawierał identyfikator tego stanowiska, to wartości z tabeli Stanowiska i tak zostaną przeanalizowane i ewentualnie wyświetlone. Jeżeli musimy dołączyć kolejne tabele do listy, postępujemy w analogiczny sposób.

W zestawieniu zazwyczaj chcemy wyświetlić tylko informacje z wybranych kolumn tabeli. To, które wartości powinny być wyświetlone w zestawieniu określa się w miejscu, które oznaczyłem jako lista_atrybutów, czyli innymi słowy mówiąc lista nazw kolumn tabeli. 

SELECT S.ID, OBSZAR_AZP, FUNKCJA FROM Stanowiska S LEFT OUTER JOIN FaktyKulturowe F ON S.ID = F.STANOWISKO

Polecenie powyżej tworzy listę stanowisk, na której każde stanowisko może mieć określoną funkcję. Jeżeli funkcja jest nieokreślona, to pole w zestawieniu będzie puste. Lista atrybutów w zestawieniu jest następująca: S.ID - identyfikator stanowiska, OBSZAR_AZP, FUNKCJA. Skąd wiadomo, że S.ID oznacza identyfikator stanowiska, a nie identyfikator faktu kulturowego. Stąd, że użyliśmy "skrótu" (aliasu) S wymienionego także na liście tabel, gdzie przypisaliśmy literę 'S' do tabeli Stanowiska. 

A jak powinniśmy zażądać w poleceniu SQL wyświetlenia identyfikatora faktu kulturowego? Podświetl zaczerniony fragment aby porównać swoją odpowiedź z prawidłowym rozwiązaniem: F.ID 

Część druga

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.