sobota, 24 listopada 2012

Drukowanie rozbudowanej tabeli w Qt4

Tydzień temu pisałem o drukowaniu dokumentów w Pythonie stosując metody biblioteki Qt4. Wszystko w kontekście tworzenia Kart Ewidencji Zabytków Archeologicznych (KEZA), które wypełnia się przede wszystkim na okoliczność prowadzenia badań w ramach projektu Archeologicznego Zdjęcia Polski.
Przypatrując się wzorcowej karcie KEZA można zauważyć, że faktycznie jest to bardzo rozbudowana tabela ze scalonymi komórkami. Eksperyment przeprowadzony przy pomocy Excela wykazał, że podobny efekt można osiągnąć stosując tabelę z 60 kolumnami i 35 wierszami równej wysokości, która wypełnia arkusz A4.

 Wstawianie tabeli

To okazało się szczęśliwym zrządzeniem losu, ponieważ dzięki temu i wykorzystaniu klas QTextDocument i QTextCursor nie musiałem implementować własnoręcznie funkcji rysującej tabelę. Zamiast tego mogłem wykorzystać metodę insertTable(wiersze, kolumny, format) klasy QTextCursor. Znaczenie dwóch pierwszych parametrów powinno być oczywiste. W pierwszym podaje się liczbę wierszy, w drugim liczbę kolumn. W przypadku tworzenia karty KEZA istotny jest trzeci, za pomocą którego określa się właściwości tabeli, takie jak obramowanie (border-style), odległość między obramowaniem a zawartością komórki (cellpadding). Te i inne parametry określa się w obiekcie klasy QTextTableFormat, podawanym jako trzeci argument metody insertTable. Po wykonaniu zwraca ona obiekt klasy QTextTable.

Przykład

Kod programu Pythona rozpoczynając od inicjalizacji klasy QTextDocument do wstawienia nowej tabeli mógłby wyglądać następująco:

doc = QTextDocument()
cur = QTextCursor(doc)
fmt = QTextTableFormat()
fmt.setCellPadding(1)
fmt.setBorderStyle(QTextFrameFormat.BorderStyle_Solid)
keza = cur.insertTable(35,60,fmt)

W sześciu liniach została utworzona duża tabela, zawierająca ponad 2100 komórek. Teraz kwestią pozostaje ich scalenia w taki sposób, żeby po wszystkim przypominała wzór karty KEZA. Do połączenia grupy komórek można zastosować metodę mergeCells(y, x, wysokosc, szerokosc) klasy QTextTable, gdzie argumenty y oraz x  oznaczają lokalizację pierwszej komórki w grupie łączonych (licząc od lewego górnego narożnika), a wysokość i szerokość to odpowiednio liczba komórek w pionie i w poziomie, które mają być scalone. Chcąc połączyć 6 komórek w górnym lewym rogu tabeli o wymiarach 3 x 2 trzeba wykonać polecenie keza.mergeCells(0,0,2,3). W przypadku generowania karty KEZA takich operacji jest zbyt dużo, aby każdą z osobna zapisywać jako wywołanie powyższej funkcji, dlatego zamiast tego stosuję prosty schemat w postaci pliku CSV, który zawiera definicję każdej docelowej komórki.

Formatowanie komórek

Na koniec pozostaje problem, jak odwołać się do wybranej komórki i wstawić do niej zawartość. Za pomocą metody cellAt(y,x) klasy QTextTable uzyskuje się referencję do komórki o podanych współrzędnych, którą reprezentuje klasa QTextTableCell. Metodą firstCursorPosition() można ustawić w niej kursor (dla przypomnienia reprezentuje go klasa QTextCursor) a następnie użyć go do wstawienia np. tekstu przy pomocy insertText(tekst). To nie wszystko każdą z komórek można formatować. Do tego potrzeba uzyskać informacje o jej bieżącym wyglądzie przy pomocy metody format() klasy QTextTableCell, a następnie stosując odpowiednie metody zmienić formatowanie. Na przykład metoda setBackground(Qt.yellow) zmieni tło komórki z białego na żółty. Na koniec należy to formatowanie jawnie zaplikować do komórki wywołując metodę setFormat. Bez tego zmiana tła będzie bezskuteczna.

Kontynuacja przykładu

keza.mergeCells(0,0,2,3)
grupa = keza.cellAt(0,0) # pobiera scaloną przed chwilą grupę komórek
gf = grupa.format() # formatowanie komórek
gf.setFont(QFont('Times',8,QFont.DemiBold))
grupa.setFormat(gf) # jawne wprowadzenie formatowania
grupa.firstCursorPosition().insertText('QAZP2') # zawartość komórki

drukarka = QPrinter()
doc.print_(drukarka) # wydrukowanie dokumentu na urządzeniu.

niedziela, 18 listopada 2012

Programowanie w Qt4: drukowanie tabel

Obecnie na "tapecie" mam problem generowania Kart Ewidencji Zabytków Archeologicznych (w skrócie KEZA), która jest formą archiwizowania informacji pochodzących z badań archeologicznych. Jej koncepcja wiąże się z początkami programu Archeologicznego Zdjęcia Polski, którego celem jest inwentaryzacja śladów osadnictwa w celu zapewnienia im ochrony konserwatorskiej. Jej wzór zmieniał się z biegiem lat, przy czym najważniejszym założeniem było to, by wszystkie informacje mieściły się na jednej kartce A4, co miało uzasadnienie w czasach "analogowych" metod archwizowania danych (to znaczy polegających na maszynowym wypełnianiu kart i wkładaniu do teczek czy segregatorów). Ponieważ do sprawozdania z badań archeologicznych muszą zawsze być dostarczone wydrukowane karty KEZA, to oczywiste jest, że QAZP2 musi także udostępniać taką funkcjonalność.
W najprostszej postaci wszystko czego potrzebujemy do drukowania przy pomocy Qt4 to dwie klasy: QPainter oraz QPrinter. Przykład ich zastosowania może wyglądać następująco:
drukarka = QPrinter()
drukarka.setOutputFormat(QPrinter.PdfFormat) # rodzaj drukarki
drukarka.setOutputFileName("nowy_plik.pdf")
painter = QPainter()
painter.begin(drukarka)
painter.drawText(10, 10, 'Test') # wydrukowanie tekstu
drukarka.newPage() # wysuniecie pierwszej strony
painter.drawText(10, 10, 'Test 2') # tekst na drugiej stronie
painter.end()

W w takiej formie drukowanie nie wiąże się z żadną filozofią: klasa QPrinter reprezentuje uniwersalny interfejs urządzenia, który umożliwia drukowanie do pliku PDF, na kartkach papieru i inne. Z kolei QPainter wykorzystując ten kanał wspomaga programistę w pisaniu na "urządzeniu" tekstu, rysowaniu figur geometrycznych albo drukowaniu obrazków.
Ponieważ karta KEZA to faktycznie tabela, to powższy kod byłby znacząco bardziej skomplikowany i zwiększający prawodopodobnieństwo wystąpienia błędów. Aby procedura drukowania była trochę bardziej abstrakcyjna, to QAZP2 zamiast klasy QPainter została wykorzystana inna - QTextDocument, która jak sama nazwa wskazuje jest używana do reprezentowania dokumentów. Te mogą być tworzone przez użytkownika, który wpisuje zdania w polu tekstowym, albo automatycznie przy pomocy klasy QTextCursor, która służy do określania miejsca, w którym bieżący dokument jest edytowany i wstawiania do niego różnych obiektów - ciągów znaków, tabel, obrazków, których kształ można określać za pomocą znaczników HTML. Gdy dokument jest już gotowy wystarczy wywołać funkcję print_(drukarka) klasy QTextDocument do wysłania go na urządzenie wskazane w parametrze drukarka. W tej metodzie zawartość dokumentu jest tłumaczona na ciąg poleceń takich jak w przykładzie powyżej.
To podejście w odróżnieniu od "niskopoziomowego" ma tą zaletę, że pozwala się skupić na zawartości dokumentu i jego wyglądzie. To w jaki sposób zostanie ona przełożona na papier albo plik PDF pozostaje w gestii klasy QTextDocument. Jak zostało ono wykorzystane w przypadku generowania karty KEZA opiszę następnym razem.

sobota, 10 listopada 2012

Przepis na PostGIS



Podstawą działania QAZP2 jest przestrzenna baza danych. Informacje o śladach działalności człowieka, a także o prowadzonych badaniach archeologicznych łączy się z przestrzenią po przez dodanie do nich współrzędnych geograficznych. W przypadku archeologii jest to kluczowa informacja, gdyż umożliwia analizę danych w kontekście krajobrazu i środowiska, które prawdopodobnie miały niebagatelny wpływ na decyzje podejmowane przez dawnych osadników.


Zanim przejdę do omówienia schematu bazy danych, do którego dostosowany jest QAZP2, w kilku zdaniach opiszę przygotowywanie systemu PostgreSQL do pracy z bazami przestrzennymi. Nie zamierzam szczegółowo omawiać poszczególnych poleceń - lepiej ode mnie robi to bogata dokumentacja. Zgodnie z tytułem - to ma być przepis, który szybko pozwoli upiec ciastko ;). Będę pisał z perspektywy użytkownika Linuksa, a dokładnie Debiana i zakładam, że czytelnicy posiadają podstawową wiedzę na temat posługiwania się tym systemem.

1. Instalacja i konfiguracja PostgreSQL


# apt-get install postgresql postgis

To polecenie chyba nie wymaga komentarza. Po kilku albo kilkunastu minutach oczekiwania (w zależności od prędkości połączenia) aktualna wersja oprogramowania - czyli system PostgreSQL i jego rozszerzenie Postgis powinno znaleźć się na dysku i zostać zainstalowane i uruchomione.

# cd /etc/postgresql/{wersja}/main/
# cp pg_hba.conf pg_hba.bak
# echo "local all all password" >> /etc/postgresql/{wersja}/main/pg_hba.conf
# /etc/init.d/postgresql restart

W domyślnej konfiguracji z bazą może się połączyć użytkownik root albo specjalnie tworzony do tego celu postgresql. A więc przed nawiązaniem połączenia z bazą i wykonaniem polecenia SELECT, UPDATE, CREATE itp. należy zalogować się jako root albo postgresql (ten drugi ma zdefiniowane domyślne hasło postgresql). Osobiście wolę inne podejście, w którym użytkownika i hasło podaje się w chwili łączenia z bazą danych. Ma ono dodatkowe uzasadnienie: w sytuacji, w której będziemy próbowali się połączyć w QGIS z bazą jako inny użytkownik, na przykład milosz, w domyślnej konfiguracji Postgre odrzuci nasze rządanie. Dlatego do pliku pg_hba.conf dodajemy następujący wiersz local all all password (trzecia linia). Żeby zabezpieczyć się przed uszkodzeniem tego pliku, wcześniej (druga linia) należy wykonać jego kopię zapasową. Ta operacja wymaga zrestartowania systemu, co czyni polecenie w lini czwartej.


To jest najprostsza konfiguracja, jaką można sobie wyobrazić, ale w zupełności wystarcza do lokalnego testowania i korzystania z systemu PostgreSQL.

# createuser -U postgresql -W -d -P tester

Na koniec tworzymy nowego użytkownika o nazwie tester, który będzie uprawniony do tworzenia nowych baz danych (przełącznik -d). Przełącznik -P oznacza, że w chwili tworzenia użytkownika zostaniemy poproszeni o nadanie mu hasła. Przełączniki -U i -W są standardowe dla każdego polecenia PostgreSQL. Pierwszym wskazujemy tego użytkownika, jako który łączymy się do bazy w celu utworzenia nowego - w tym przypadku tym użytkownikiem jest wspominany już postgresql; użycie drugiego spowoduje, że przed dodaniem nowego, trzeba będzie podać hasło użytkownika postgresql.

2. Tworzenie bazy PostGIS

# createdb -O tester -h localhost -p 5432 -U tester -W azp2
# createlang plpgsql -U tester -W azp2

Kiedy nowy użytkownik jest już dodany, czas na utworzenie bazy danych. W pierwszym poleceniu tworzymy nową bazę o nazwie azp2, której właścicielem będzie tester (przełącznik -O). Po wykonaniu drugiej komendy w bazie azp2 będzie można tworzyć procedury w języku PL/SQL. Przełączniki -W i -U mają takie samo znaczenie, jak przy dodawaniu nowego użytkownika.

# cd /usr/share/postgresql/{wersja_postgre}/contrib/postgis-{wersja_postgis}
# psql -f postgis.sql -U tester -W azp2
# psql -f spatial_ref_sys.sql -U tester -W azp2

Na koniec do bazy azp2 trzeba dodać procedury, których używamy np. do dodawania współrzędnych, wyszukiwania na ich podstawie, itd., które są zaimplementowane w języku PL/SQL, o którym pisałem powyżej. Polecenia, które dodają wspomniane procedury znajdują się w katalogu utworzonym w chwili instalacji Postgis. Przechodzimy do niego w pierwszej lini, a następnie wywołujemy narzędzie psql podając jako parametr plik postgis.sql, który zawiera definicje procedur SQL. Do pracy z przestrzenną bazą danych przydatne będzie także dodanie definicji systemów odniesienia, które są używane do konwertowania współrzędnych. To także robimy za pomocą polecenia psql podając jako parametr plik spatial_ref_sys.sql.
I tyle. Baza przestrzenna jest gotowa do pracy. Od tej chwili można już tworzyć tabele ze współrzędnymi geograficznymi. Ale o tym innym razem.






sobota, 3 listopada 2012

QGIS API: zastępowanie domyślnego formularza

W QuantumGIS, każdy obiekt geograficzny reprezentowany klasą QgsFeature charakteryzuje się zbiorem właściwości (atrybutów). W przypadku punktów kopiowanych przez QAZP2 do relacyjnej bazy są to na przykład nazwa punktu (nadawana przez użytkownika), rodzaj badań (lotnicze, powierzchniowe, itp.), czas rejestracji i inne. Jednym ze sposobów wprowadzania obiektów jest wskazanie myszką ich lokalizacji, a w przypadku dwuwymiarowych (np. poligonów) dodatkowo ich kształtu. W rezultacie zostaje wyświetlone okno dialogowe, w którym określa się atrybuty, o których mowa była wcześniej. W domyślnej postaci składa się ono z pól tekstowych, w których użytkownik wprowadza odpowiednie wartości. I tak się dzieje bez względu na to, czy kolumna bazy danych zawiera dane tekstowe, czy liczbowe, logiczne, albo na przykład są ograniczone do pewnego zakresu wartości (tzw. dziedziny). To zachowanie można zmienić modyfikując właściwości warstwy, nad którą pracujemy, jednak trzeba to robić dla każdego projektu z osobna. W tym artykule chciałbym przedstawić sposób na zastępowanie domyślnego okna dialogowe takim, które jest dostosowane do naszych potrzeb bez udziału użytkownika.

Przed rozpoczęciem pisania funkcji w Pythonie konieczne jest utworzenie naszego formularza. QGIS wymaga przygotowania go w postaci pliku XML z rozszerzeniem *.ui, który można wygenerować w programie QtDesigner, który jest dostarczany z biblioteką Qt. Można wtedy umieścić w oknie dialogowym różne komponenty takie jak listy rozwijane (QCombobox), pola liczbowe (QSpinBox), czy do wprowadzania wartości logicznych (QCheckbox). Trzeba przy tym pamiętać, żeby każdy z komponentów miał taką samą nazwę jak kolumna w tabeli, której odpowiada. Przykładem takiego formularza jest ten stosowany w QAZP2 do wprowadzania punktów. Jego zawartość można obejrzeć otwierając plik w dowolnym edytorze tekstu, albo w QtDesignerze.

Kluczową kwestią dla rozwiązania tego problemu zastąpienia domyślnego okna dialogowego jest uchwycenie chwili, w której interesująca nas warstwa wektorowa zostaje wczytana w programie QGIS. Aby to zrobić trzeba się odwołać do obiektu klasy qgis.core.QgsMapLayerRegistry, który można uzyskać wywołując statyczną metodę instance(). Za każdym razem, gdy użytkownik otwiera nową warstwę, emitowany jest syngnał layerWasAdded, na który musimy zareagować podejmując odpowiednią akcję. To działanie będzie reprezentowała funkcja dodajUi(warstwa) w następującej postaci:

def dodajUi(warstwa):
    if warstwa.name() == 'miejsca':
        warstwa.setEditForm(abspath(__file__+'/../forms/miejsca.ui'))


Przez parametr warstwa przekazywana jest referencja do obiektu klasy QgsVectorLayer, której wczytanie wyemitowało sygnał layerWasAdded. Jeżeli warstwa nazywa się 'miejsca', to trzeba wywołać metodę setEditForm, w której podajemy bezpośrednią ścieżkę pliku *.ui wygenerwanego w QtDesignerze. Wartość __file__ dla dowolnego modułu Pythona zwaraca jego lokalizację w systemie plików. W tym przypadku katalog 'forms', w którym znajduje się plik 'miejsca.ui' jest zapisany na tym samym poziomie co katalog z modułem, który zawiera funkcję dodajUi. Przykład implementacji funkcji dodajUi można znaleźć w module qazp.py.

Aby wskazać, jaka akcja ma być wykonana po emisji sygnału layerWasAdded, trzeba go połączyć z funkcją dodajUi, co w bibliotece Qt realizuje się wywołując sekwencję QgsMapLayerRegistry.instance().layerWasAdded.connect(dodajUi). Ta operacja jest wykonywana w chwili inicjalizacji wtyczki, która zgodnie z wymaganiami QGIS następuje w funkcji initGui w momencie uruchamiania programu albo po pierwszej instalacji nowej wtyczki. Od tej chwili, gdy użytkownik będzie próbował dodać kolejny punkt na mapie, zamiast domyślnego okna dialogowego zostanie mu wyświetlone to, które zostało zdefiniowane w pliku miejsca.ui.