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

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.