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

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.

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.

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.