6.4. ToDoPw

Realizacja prostej listy zadań do zrobienia jako aplikacji okienkowej, z wykorzystaniem biblioteki Qt5 i wiązań Pythona PyQt5. Aplikacja umożliwia dodawanie, usuwanie, edycję i oznaczanie jako wykonane zadań, zapisywanych w bazie SQLite obsługiwanej za pomocą systemu ORM Peewee. Biblioteka Peewee musi być zainstalowana w systemie.

Przykład wykorzystuje programowanie obiektowe (ang. Object Oriented Programing) i ilustruje technikę programowania model/widok (ang. Model/View Programming).

../../_images/todopw06.png

Attention

Wymagana wiedza:

  • Znajomość Pythona w stopniu średnim.
  • Znajomość podstaw projektowania interfejsu z wykorzystaniem biblioteki Qt (zob. scenariusze Kalkulator i Widżety).
  • Znajomość podstaw systemów ORM (zob. scenariusz Systemy ORM).

6.4.1. Interfejs

Budowanie aplikacji zaczniemy od przygotowania podstawowego interfejsu. Na początku utwórzmy katalog aplikacji, w którym zapisywać będziemy wszystkie pliki:

~$ mkdir todopw

Następnie w dowolnym edytorze tworzymy plik o nazwie gui.py, który posłuży do definiowania składników interfejsu. Wklejamy do niego poniższy kod:

Plik gui.py. Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# -*- coding: utf-8 -*-

from PyQt5.QtWidgets import QTableView, QPushButton
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout


class Ui_Widget(object):

    def setupUi(self, Widget):
        Widget.setObjectName("Widget")

        # tabelaryczny widok danych
        self.widok = QTableView()

        # przyciski Push ###
        self.logujBtn = QPushButton("Za&loguj")
        self.koniecBtn = QPushButton("&Koniec")

        # układ przycisków Push ###
        uklad = QHBoxLayout()
        uklad.addWidget(self.logujBtn)
        uklad.addWidget(self.koniecBtn)

        # główny układ okna ###
        ukladV = QVBoxLayout(self)
        ukladV.addWidget(self.widok)
        ukladV.addLayout(uklad)

        # właściwości widżetu ###
        self.setWindowTitle("Prosta lista zadań")
        self.resize(500, 300)

Centralnym elementem aplikacji będzie komponent QTableView, który potrafi wyświetlać dane w formie tabeli na podstawie zdefiniowanego modelu. Użyjemy go po to, aby oddzielić dane od sposobu ich prezentacji (zob. Model/View programming). Taka architektura przydaje się zwłaszcza wtedy, kiedy aplikacja okienkowa stanowi przede wszystkim interfejs służący prezentacji i ewentualnie edycji danych, przechowywanych niezależnie, np. w bazie.

Pod kontrolką widoku umieszczamy obok siebie dwa przyciski, za pomocą których będzie się można zalogować do aplikacji i ją zakończyć.

Główne okno i obiekt aplikacji utworzymy w pliku todopw.py, który musi zostać zapisany w tym samym katalogu co plik opisujący interfejs. Jego zawartość na początku będzie następująca:

Plik todopw.py. Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtWidgets import QMessageBox, QInputDialog
from gui_z0 import Ui_Widget


class Zadania(QWidget, Ui_Widget):

    def __init__(self, parent=None):
        super(Zadania, self).__init__(parent)
        self.setupUi(self)

        self.logujBtn.clicked.connect(self.loguj)
        self.koniecBtn.clicked.connect(self.koniec)

    def loguj(self):
        login, ok = QInputDialog.getText(self, 'Logowanie', 'Podaj login:')
        if ok:
            haslo, ok = QInputDialog.getText(self, 'Logowanie', 'Podaj haslo:')
            if ok:
                if not login or not haslo:
                    QMessageBox.warning(
                        self, 'Błąd', 'Pusty login lub hasło!', QMessageBox.Ok)
                    return
                QMessageBox.information(
                    self, 'Dane logowania',
                    'Podano: ' + login + ' ' + haslo, QMessageBox.Ok)

    def koniec(self):
        self.close()

if __name__ == '__main__':
    import sys

    app = QApplication(sys.argv)
    okno = Zadania()
    okno.show()
    okno.move(350, 200)
    sys.exit(app.exec_())

Podobnie jak w poprzednich scenariuszach klasa Zadania dziedziczy z klasy Ui_Widget, aby utworzyć interfejs aplikacji. W konstruktorze skupiamy się na działaniu aplikacji, czyli wiążemy kliknięcia przycisków z odpowiednimi slotami.

Przeglądanie i dodawanie zadań wymaga zalogowania, które obsługuje funkcja loguj(). Login i hasło użytkownika można pobrać za pomocą widżetu QInputDialog, np.: login, ok = QInputDialog.getText(self, 'Logowanie', 'Podaj login:'). Zmienna ok przyjmie wartość True, jeżeli użytkownik zamknie okno naciśnięciem przycisku OK.

Jeżeli użytkownik nie podał loginu lub hasła, za pomocą okna dialogowego typu QMessageBox wyświetlamy ostrzeżenie (warning). W przeciwnym wypadku wyświetlamy okno informacyjne (information) z wprowadzonymi wartościami.

Aplikację testujemy wpisując w terminalu polecenie:

~/todopw$ python todopw.py
../../_images/todopw00.png

6.4.2. Okno logowania

Pobieranie loginu i hasła w osobnych dialogach nie jest optymalne. Na podstawie klasy QDialog stworzymy specjalne okno dialogowe. Na początku dodajemy importy:

Plik gui.py – importy. Kod nr
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialog, QDialogButtonBox
from PyQt5.QtWidgets import QLabel, QLineEdit
from PyQt5.QtWidgets import QGridLayout

Na końcu pliku gui.py wstawiamy:

Plik gui.py. Kod nr
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class LoginDialog(QDialog):
    """ Okno dialogowe logowania """

    def __init__(self, parent=None):
        super(LoginDialog, self).__init__(parent)

        # etykiety, pola edycyjne i przyciski ###
        loginLbl = QLabel('Login')
        hasloLbl = QLabel('Hasło')
        self.login = QLineEdit()
        self.haslo = QLineEdit()
        self.przyciski = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            Qt.Horizontal, self)

        # układ główny ###
        uklad = QGridLayout(self)
        uklad.addWidget(loginLbl, 0, 0)
        uklad.addWidget(self.login, 0, 1)
        uklad.addWidget(hasloLbl, 1, 0)
        uklad.addWidget(self.haslo, 1, 1)
        uklad.addWidget(self.przyciski, 2, 0, 2, 0)

        # sygnały i sloty ###
        self.przyciski.accepted.connect(self.accept)
        self.przyciski.rejected.connect(self.reject)

        # właściwości widżetu ###
        self.setModal(True)
        self.setWindowTitle('Logowanie')

    def loginHaslo(self):
        return (self.login.text().strip(),
                self.haslo.text().strip())

    # metoda statyczna, tworzy dialog i zwraca (login, haslo, ok)
    @staticmethod
    def getLoginHaslo(parent=None):
        dialog = LoginDialog(parent)
        dialog.login.setFocus()
        ok = dialog.exec_()
        login, haslo = dialog.loginHaslo()
        return (login, haslo, ok == QDialog.Accepted)

Okno składa się z dwóch etykiet, odpowiadających im 1-liniowych pól edycyjnych oraz standardowych przycisków. Wywołanie metody setModal(True) powoduje, że dopóki użytkownik nie zamknie okna, nie może manipulować oknem rodzica, czyli aplikacją.

Do wywołania okna użyjemy metody statycznej getLoginHaslo() (zob. metoda statyczna) klasy LoginDialog. Można by ją zapisać nawet poza definicją klasy, ale ponieważ ściśle jest z nią związana, używamy dekoratora @staticmethod. Metodę wywołamy w pliku todopw.py w postaci LoginDialog.getLoginHaslo(self). Tworzy ona okno dialogowe (dialog = LoginDialog(parent)) i aktywuje pole loginu. Następnie wyświetla okno i zapisuje odpowiedź użytkownika (wciśnięty przycisk) w zmiennej: ok = dialog.exec_(). Po zamknięciu okna pobiera wpisane dane za pomocą funkcji pomocniczej loginHaslo() i zwraca je, o ile użytkownik wcisnął przycisk OK.

W pliku todopw.py uzupełniamy importy:

Plik todopw.py – importy. Kod nr
from gui import Ui_Widget, LoginDialog

– i zmieniamy funkcję loguj():

Plik gui.py. Kod nr
19
20
21
22
23
24
25
26
27
28
29
30
    def loguj(self):
        login, haslo, ok = LoginDialog.getLoginHaslo(self)
        if not ok:
            return

        if not login or not haslo:
            QMessageBox.warning(self, 'Błąd',
                                'Pusty login lub hasło!', QMessageBox.Ok)
            return

        QMessageBox.information(self,
            'Dane logowania', 'Podano: ' + login + ' ' + haslo, QMessageBox.Ok)

Przetestuj działanie nowego okna dialogowego.

../../_images/todopw01.png
../../_images/todopw01a.png

6.4.3. Podłączamy bazę

Dane użytkowników oraz ich listy zadań zapisywać będziemy w bazie SQLite. Dla uproszczenia jej obsługi wykorzystamy prosty system ORM Peewee. Kod umieścimy w osobnym pliku o nazwie baza.py. Po utworzeniu tego pliku wypełniamy go poniższą zawartością:

Plik baza.py. Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# -*- coding: utf-8 -*-

from peewee import *
from datetime import datetime

baza = SqliteDatabase('adresy.db')


class BazaModel(Model):  # klasa bazowa

    class Meta:
        database = baza


class Osoba(BazaModel):
    login = CharField(null=False, unique=True)
    haslo = CharField()

    class Meta:
        order_by = ('login',)


class Zadanie(BazaModel):
    tresc = TextField(null=False)
    datad = DateTimeField(default=datetime.now)
    wykonane = BooleanField(default=False)
    osoba = ForeignKeyField(Osoba, related_name='zadania')

    class Meta:
        order_by = ('datad',)


def polacz():
    baza.connect()  # nawiązujemy połączenie z bazą
    baza.create_tables([Osoba, Zadanie], True)  # tworzymy tabele
    ladujDane()  # wstawiamy początkowe dane
    return True


def loguj(login, haslo):
    try:
        osoba, created = Osoba.get_or_create(login=login, haslo=haslo)
        return osoba
    except IntegrityError:
        return None


def ladujDane():
    """ Przygotowanie początkowych danych testowych """
    if Osoba.select().count() > 0:
        return
    osoby = ('adam', 'ewa')
    zadania = ('Pierwsze zadanie', 'Drugie zadanie', 'Trzecie zadanie')
    for login in osoby:
        o = Osoba(login=login, haslo='123')
        o.save()
        for tresc in zadania:
            z = Zadanie(tresc=tresc, osoba=o)
            z.save()
    baza.commit()
    baza.close()

Po zaimportowaniu wymaganych modułów mamy definicje klas Osoba i Zadania, na podstawie których tworzyć będziemy obiekty reprezentujące użytkownika i jego zadania. W pliku definiujemy również instancję bazy w instrukcji: baza = SqliteDatabase('adresy.db'). Jako argument podajemy nazwę pliku, w którym zapisywane będą dane.

Dalej mamy trzy funkcje pomocnicze:

  • polacz() – służy do nawiązania połączenia z bazą, utworzenia tabel, o ile ich w bazie nie ma oraz do wywołania funkcji ładującej początkowe dane testowe;
  • loguj() – funkcja stara się odczytać z bazy dane użytkownika o podanym loginie i haśle; jeżeli użytkownika nie ma w bazie, zostaje automatycznie utworzony pod warunkiem, że podany login nie został wcześniej wykorzystany; w takim wypadku zamiast obiektu reprezentującego użytkownika zwrócona zostanie wartość None;
  • ladujDane() – jeżeli tabela użytkowników jest pusta, funkcja doda dane dwóch testowych użytkowników.

Resztę zmian nanosimy w pliku todopw.py. Przede wszystkim importujemy przygotowany przed chwilą moduł obsługujący bazę:

Plik todopw.py – importy. Kod nr
import baza

Dalej uzupełniamy funkcję loguj():

Plik todopw.py. Kod nr
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    def loguj(self):
        """ Logowanie użytkownika """
        login, haslo, ok = LoginDialog.getLoginHaslo(self)
        if not ok:
            return

        if not login or not haslo:
            QMessageBox.warning(self, 'Błąd',
                                'Pusty login lub hasło!', QMessageBox.Ok)
            return

        self.osoba = baza.loguj(login, haslo)
        if self.osoba is None:
            QMessageBox.critical(self, 'Błąd', 'Błędne hasło!', QMessageBox.Ok)
            return

        QMessageBox.information(self,
            'Dane logowania', 'Podano: ' + login + ' ' + haslo, QMessageBox.Ok)

Jak widać, dopisujemy kod logujący użytkownika w bazie: self.osoba = baza.loguj(login, haslo).

Na końcu pliku, po utworzeniu obiektu aplikacji (app = QApplication(sys.argv)), musimy jeszcze wywołać funkcję ustanawiającą połączenie z bazą, czyli wstawić kod baza.polacz():

Plik todopw.py. Kod nr
42
43
44
45
if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    baza.polacz()

Przetestuj działanie aplikacji. Znakiem poprawnego jej działania będzie utworzenie pliku bazy adresy.db, komunikat wyświetlający poprawnie podany login i hasło lub komunikat o błędzie, jeżeli login został już w bazie użyty, a hasło do niego nie pasuje.

6.4.4. Model danych

Kluczowym zadaniem podczas programowania z wykorzystaniem techniki model/widok jest zaimplementowanie modelu. Jego zadaniem jest stworzenie interfejsu dostępu do danych dla komponentów pełniących rolę widoków. Zob. Model Classess.

Note

Warto zauważyć, ze dane udostępniane przez model mogą być prezentowane za pomocą różnych widoków jednocześnie.

Ponieważ listę zadań przechowujemy w zewnętrznej bazie danych w tabeli, model stworzymy na podstawie klasy QAbstractTableModel. W nowym pliku o nazwie tabmodel.py umieszczamy następujący kod:

Plik tabmodel.py. Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt, QVariant


class TabModel(QAbstractTableModel):
    """ Tabelaryczny model danych """

    def __init__(self, pola=[], dane=[], parent=None):
        super(TabModel, self).__init__()
        self.pola = pola
        self.tabela = dane

    def aktualizuj(self, dane):
        """ Przypisuje źródło danych do modelu """
        print(dane)
        self.tabela = dane

    def rowCount(self, parent=QModelIndex()):
        """ Zwraca ilość wierszy """
        return len(self.tabela)

    def columnCount(self, parent=QModelIndex()):
        """ Zwraca ilość kolumn """
        if self.tabela:
            return len(self.tabela[0])
        else:
            return 0

    def data(self, index, rola=Qt.DisplayRole):
        """ Wyświetlanie danych """
        i = index.row()
        j = index.column()

        if rola == Qt.DisplayRole:
            return '{0}'.format(self.tabela[i][j])
        else:
            return QVariant()

Konstruktor klasy TabModel opcjonalnie przyjmuje listę pól oraz listę rekordów – z tych możliwości skorzystamy później. Dane będzie można również przypisać za pomocą metody aktualizuj(). Wywołanie print(dane) jest w niej umieszczone tylko w celach poglądowych: wydrukuje przekazane dane w konsoli.

Dwie kolejne funkcje rowCount() i columnCount() są obowiązkowe i zgodnie ze swoimi nazwami zwracają ilość wierszy (len(self.tabela)) i kolumn (len(self.tabela[0])) w każdym wierszu. Jak widać, dane przekazywać będziemy w postaci listy list, czy też listy dwuwymiarowej.

Funkcja data() również jest obowiązkowa i odpowiada za wyświetlanie danych. Wywoływana jest dla każdego wiersza i każdej kolumny osobno. Trzecim parametrem tej funkcji jest tzw. rola (zob. ItemDataRole ), oznaczająca rodzaj danych wymaganych przez widok do właściwego wyświetlenia danych. Domyślną wartością jest Qt.DisplayRole, czyli wyświetlanie danych, dla której zwracamy reprezentację tekstową naszych danych: return '{0}'.format(self.tabela[i][j]).

Dane przekazywane do modelu odczytamy za pomocą funkcji, którą dopisujemy do pliku baza.py:

Plik baza.py. Kod nr
64
65
66
67
68
69
70
71
72
73
74
75
def czytajDane(osoba):
    """ Pobranie zadań danego użytkownika z bazy """
    zadania = []  # lista zadań
    wpisy = Zadanie.select().where(Zadanie.osoba == osoba)
    for z in wpisy:
        zadania.append([
            z.id,  # identyfikator zadania
            z.tresc,  # treść zadania
            '{0:%Y-%m-%d %H:%M:%S}'.format(z.datad),  # data dodania
            z.wykonane,  # bool: czy wykonane?
            False])  # bool: czy usunąć?
    return zadania

Funkcję czytajDane() odczytuje wszystkie zadania danego użytkownika z bazy: wpisy = Zadanie.select().where(Zadanie.osoba == osoba). Następnie w pętli do listy zadania dodajemy rekordy opisujące kolejne zadania (zadania.append()). Każdy rekord to lista, która zawiera: identyfikator, treść, datę dodania, pole oznaczające wykonanie zadania oraz dodatkową wartość logiczną, która pozwoli wskazać zadania do usunięcia.

Pozostaje nam edycja pliku todopw.py. Na początku trzeba zaimportować model:

Plik todopw.py – importy. Kod nr
from tabmodel import TabModel

Następnie tworzymy jego instancję. Uzupełniamy fragment uruchamiający aplikację o kod: model = TabModel():

Plik todopw.py. Kod nr
48
49
50
51
52
53
54
55
56
if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    baza.polacz()
    model = TabModel()
    okno = Zadania()
    okno.show()
    okno.move(350, 200)
    sys.exit(app.exec_())

Zadania użytkownika odczytujemy w funkcji loguj(), w której kod wyświetlający dialog informacyjny (QMessageBox.information(...)) zastępujemy oraz dodajemy nową funkcję:

Plik todopw.py – funkcja loguj(). Kod nr
37
38
39
40
41
42
43
        zadania = baza.czytajDane(self.osoba)
        model.aktualizuj(zadania)
        model.layoutChanged.emit()
        self.odswiezWidok()

    def odswiezWidok(self):
        self.widok.setModel(model)  # przekazanie modelu do widoku

Po odczytaniu zadań zadania = baza.czytajDane(self.osoba) przypisujemy dane modelowi model.aktualizuj(zadania).

Instrukcja model.layoutChanged.emit() powoduje wysłanie sygnału powiadamiającego widok o zmianie danych. Umieszczamy ją, aby po ewentualnym ponownym zalogowaniu kolejny użytkownik zobaczył swoje zadania.

Dane modelu musimy przekazać widokowi. To zadanie metody odswiezWidok(), która wywołuje polecenie: self.widok.setModel(model).

Przetestuj aplikację logując się jako “adam” lub “ewa” z hasłem “123”.

../../_images/todopw03.png

6.4.5. Dodawanie zadań

Możemy już przeglądać zadania, ale jeżeli zalogujemy się jako nowy użytkownik, nic w tabeli nie zobaczymy. Aby umożliwić dodawanie zadań, w pliku gui.py tworzymy nowy przycisk “Dodaj”, który po uruchomieniu będzie nieaktywny:

Plik gui.py. Kod nr
19
20
21
22
23
24
25
26
27
28
29
        # przyciski Push ###
        self.logujBtn = QPushButton("Za&loguj")
        self.koniecBtn = QPushButton("&Koniec")
        self.dodajBtn = QPushButton("&Dodaj")
        self.dodajBtn.setEnabled(False)

        # układ przycisków Push ###
        uklad = QHBoxLayout()
        uklad.addWidget(self.logujBtn)
        uklad.addWidget(self.dodajBtn)
        uklad.addWidget(self.koniecBtn)

W pliku todopw.py uzupełniamy konstruktor i dodajemy nową funkcję dodaj():

Plik todopw.py. Kod nr
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    def __init__(self, parent=None):
        super(Zadania, self).__init__(parent)
        self.setupUi(self)

        self.logujBtn.clicked.connect(self.loguj)
        self.koniecBtn.clicked.connect(self.koniec)
        self.dodajBtn.clicked.connect(self.dodaj)

    def dodaj(self):
        """ Dodawanie nowego zadania """
        zadanie, ok = QInputDialog.getMultiLineText(self,
                                                    'Zadanie',
                                                    'Co jest do zrobienia?')
        if not ok or not zadanie.strip():
            QMessageBox.critical(self,
                                 'Błąd',
                                 'Zadanie nie może być puste.',
                                 QMessageBox.Ok)
            return

        zadanie = baza.dodajZadanie(self.osoba, zadanie)
        model.tabela.append(zadanie)
        model.layoutChanged.emit()  # wyemituj sygnał: zaszła zmiana!
        if len(model.tabela) == 1:  # jeżeli to pierwsze zadanie
            self.odswiezWidok()     # trzeba przekazać model do widoku

Kliknięcie przycisku “Dodaj” wiążemy z nową funkcją dodaj(). Treść zadania pobieramy za pomocą omawianego okna typu QInputDialog. Po sprawdzeniu, czy użytkownik w ogóle coś wpisał, wywołujemy funkcję dodajZadanie() z modułu baza, która zapisuje nowe dane w bazie. Następnie aktualizujemy dane modelu, czyli do listy zadań dodajemy rekord nowego zadania: model.tabela.append(zadanie). Ponieważ następuje zmiana danych modelu, emitujemy odpowiedni sygnał: model.layoutChanged.emit().

Jeżeli nowe zadanie jest pierwszym w modelu (if len(model.tabela) == 1), należy jeszcze odświeżyć widok. Wywołujemy więc funkcję odswiezWidok(), którą modyfikujemy do podanej postaci:

Plik todopw.py. Kod nr
61
62
63
64
65
66
67
    def odswiezWidok(self):
        self.widok.setModel(model)  # przekazanie modelu do widoku
        self.widok.hideColumn(0)  # ukrywamy kolumnę id
        # ograniczenie szerokości ostatniej kolumny
        self.widok.horizontalHeader().setStretchLastSection(True)
        # dopasowanie szerokości kolumn do zawartości
        self.widok.resizeColumnsToContents()

W uzupełnionej funkcji wywołujemy metody obiektu widoku, które ukrywają pierwszą kolumnę z identyfikatorami zadań, ograniczają szerokość ostatniej kolumny oraz powodują dopasowanie szerokości kolumn do zawartości.

Musimy jeszcze aktywować przycisk dodawania po zalogowaniu się użytkownika. Na końcu funkcji loguj() dopisujemy:

Plik todopw.py. Kod nr
self.dodajBtn.setEnabled(True)

W pliku baza.py dopisujemy jeszcze wspomnianą funkcję dodajZadanie():

Plik baza.py. Kod nr
78
79
80
81
82
83
84
85
86
87
def dodajZadanie(osoba, tresc):
    """ Dodawanie nowego zadania """
    zadanie = Zadanie(tresc=tresc, osoba=osoba)
    zadanie.save()
    return [
        zadanie.id,
        zadanie.tresc,
        '{0:%Y-%m-%d %H:%M:%S}'.format(zadanie.datad),
        zadanie.wykonane,
        False]

Zapisanie zadania jest proste dzięki wykorzystaniu systemu ORM. Tworzymy instancję klasy Zadanie: zadanie = Zadanie(tresc=tresc, osoba=osoba) – podając tylko wymagane dane. Wartości pozostałych pól utworzone zostaną na podstawie wartości domyślnych określonych w definicji klasy. Wywołanie metody save() zapisuje zadanie w bazie. Funkcja zwraca listę – rekord o takiej samej strukturze, jak funkcja czytajDane().

Pozostaje uruchomienie aplikacji i dodanie nowego zadania.

../../_images/todopw04.png

6.4.6. Edycja i widok danych

Edycję zadań można zrealizować za pomocą funkcjonalności modelu. Rozszerzamy więc funkcję data() i uzupełniamy definicję klasy TabModel w pliku tabmodel.py:

Plik tabmodel.py. Kod nr
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    def data(self, index, rola=Qt.DisplayRole):
        """ Wyświetlanie danych """
        i = index.row()
        j = index.column()

        if rola == Qt.DisplayRole:
            return '{0}'.format(self.tabela[i][j])
        elif rola == Qt.CheckStateRole and (j == 3 or j == 4):
            if self.tabela[i][j]:
                return Qt.Checked
            else:
                return Qt.Unchecked
        elif rola == Qt.EditRole and j == 1:
            return self.tabela[i][j]
        else:
            return QVariant()

    def flags(self, index):
        """ Zwraca właściwości kolumn tabeli """
        flags = super(TabModel, self).flags(index)
        j = index.column()
        if j == 1:
            flags |= Qt.ItemIsEditable
        elif j == 3 or j == 4:
            flags |= Qt.ItemIsUserCheckable

        return flags

    def setData(self, index, value, rola=Qt.DisplayRole):
        """ Zmiana danych """
        i = index.row()
        j = index.column()
        if rola == Qt.EditRole and j == 1:
            self.tabela[i][j] = value
        elif rola == Qt.CheckStateRole and (j == 3 or j == 4):
            if value:
                self.tabela[i][j] = True
            else:
                self.tabela[i][j] = False

        return True

    def headerData(self, sekcja, kierunek, rola=Qt.DisplayRole):
        """ Zwraca nagłówki kolumn """
        if rola == Qt.DisplayRole and kierunek == Qt.Horizontal:
            return self.pola[sekcja]
        elif rola == Qt.DisplayRole and kierunek == Qt.Vertical:
            return sekcja + 1
        else:
            return QVariant()

W funkcji data() dodajemy obsługę roli Qt.CheckStateRole, pozwalającej w polach typu prawda/fałsz wyświetlić kontrolki checkbox. Rozpoczęcie edycji danych, np. poprzez dwukrotne kliknięcie, wywołuje rolę Qt.EditRole, wtedy zwracamy do dotychczasowe dane.

Właściwości danego pola danych określa funkcja flags(), która wywoływana jest dla każdego pola osobno. W naszej implementacji, po sprawdzeniu indeksu pola, pozwalamy na zmianę treści zadania: flags |= Qt.ItemIsEditable. Pozwalamy również na oznaczenie zadania jako wykonanego i przeznaczonego do usunięcia: flags |= Qt.ItemIsUserCheckable.

Faktyczną edycję danych zatwierdza funkcja setData(). Po sprawdzeniu roli i indeksu pola aktualizuje ona treść zadania oraz stan pól typu checkbox w modelu.

Ostatnia funkcja, headerData(), odpowiada za wyświetlanie nagłówków kolumn. Nagłówki pól (resp. kolumn, kierunek == Qt.Horizontal), odczytywane są z listy: return self.pola[sekcja]. Kolejne rekordy (resp. wiersze, kierunek == Qt.Vertical) są kolejno numerowane: return sekcja+1. Zmienna sekcja oznacza numer kolumny lub wiersza.

Listę nagłówków kolumn definiujemy w pliku baza.py dopisując na końcu:

Plik baza.py. Kod nr
90
pola = ['Id', 'Zadanie', 'Dodano', 'Zrobione', 'Usuń']

W pliku todopw.py uzupełniamy jeszcze kod tworzący instancję modelu:

Plik todopw.py. Kod nr
72
73
74
75
76
if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    baza.polacz()
    model = TabModel(baza.pola)

Uruchom zmodyfikowaną aplikację. Spróbuj zmienić treść zadania dwukrotnie klikając. Oznacz wybrane zadania jako wykonane lub przeznaczone do usunięcia.

../../_images/todopw05.png

6.4.7. Zapisywanie zmian

Możemy już edytować zadania, oznaczać je jako wykonane i przeznaczone do usunięcia, ale zmiany te nie są zapisywane. Dodamy więc taką możliwość. W pliku gui.py tworzymy jeszcze jeden przycisk i dodajemy go do układu:

Plik gui.py. Kod nr
19
20
21
22
23
24
25
26
27
28
29
30
31
32
        # przyciski Push ###
        self.logujBtn = QPushButton("Za&loguj")
        self.koniecBtn = QPushButton("&Koniec")
        self.dodajBtn = QPushButton("&Dodaj")
        self.dodajBtn.setEnabled(False)
        self.zapiszBtn = QPushButton("&Zapisz")
        self.zapiszBtn.setEnabled(False)

        # układ przycisków Push ###
        uklad = QHBoxLayout()
        uklad.addWidget(self.logujBtn)
        uklad.addWidget(self.dodajBtn)
        uklad.addWidget(self.zapiszBtn)
        uklad.addWidget(self.koniecBtn)

W pliku todopw.py kliknięcie przycisku “Zapisz” wiążemy z nową funkcją zapisz():

Plik todopw.py. Kod nr
14
15
16
17
18
19
20
21
22
23
24
25
    def __init__(self, parent=None):
        super(Zadania, self).__init__(parent)
        self.setupUi(self)

        self.logujBtn.clicked.connect(self.loguj)
        self.koniecBtn.clicked.connect(self.koniec)
        self.dodajBtn.clicked.connect(self.dodaj)
        self.zapiszBtn.clicked.connect(self.zapisz)

    def zapisz(self):
        baza.zapiszDane(model.tabela)
        model.layoutChanged.emit()

Slot zapisz() wywołuje funkcję zdefiniowaną w module baza.py, przekazując jej listę z rekordami: baza.zapiszDane(model.tabela). Na koniec emitujemy sygnał zmiany, aby widok mógł uaktualnić dane, jeżeli jakieś zadania zostały usunięte.

Przycisk “Zapisz” podobnie jak “Dodaj” powinien być uaktywniony po zalogowaniu użytkownika. Na końcu funkcji loguj() należy dopisać kod:

Plik todopw.py. Kod nr
self.zapiszBtn.setEnabled(True)

Pozostaje dopisanie na końcu pliku baza.py funkcji zapisującej zmiany:

Plik baza.py. Kod nr
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def zapiszDane(zadania):
    """ Zapisywanie zmian """
    for i, z in enumerate(zadania):
        # utworzenie instancji zadania
        zadanie = Zadanie.select().where(Zadanie.id == z[0]).get()
        if z[4]:  # jeżeli zaznaczono zadanie do usunięcia
            zadanie.delete_instance()  # usunięcie zadania z bazy
            del zadania[i]  # usunięcie zadania z danych modelu
        else:
            zadanie.tresc = z[1]
            zadanie.wykonane = z[3]
            zadanie.save()

W pętli odczytujemy indeksy i rekordy z danymi zadań: for i, z in enumerate(zadania). Tworzymy instancję każdego zadania na podstawie identyfikatora zapisanego jako pierwszy element listy: zadanie = Zadanie.select().where(Zadanie.id == z[0]).get(). Później albo usuwamy zadanie, albo aktualizujemy przypisując polom “tresc” i “wykonane” dane z modelu.

To wszystko, przetestuj gotową aplikację.

6.4.8. Materiały

  1. Model/View Programming
  2. Model/View Tutorial
  3. Presenting Data in a Table View
  4. Layout Management

Źródła:


Licencja Creative Commons Materiały Python 101 udostępniane przez Centrum Edukacji Obywatelskiej na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowa.

Utworzony:2017-09-08 o 19:38 w Sphinx 1.4.5
Autorzy:Patrz plik “Autorzy”