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.