6.3. 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).

Uwaga
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.3.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:
1# -*- coding: utf-8 -*-
2
3from PyQt5.QtWidgets import QTableView, QPushButton
4from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout
5
6
7class Ui_Widget(object):
8
9 def setupUi(self, Widget):
10 Widget.setObjectName("Widget")
11
12 # tabelaryczny widok danych
13 self.widok = QTableView()
14
15 # przyciski Push ###
16 self.logujBtn = QPushButton("Za&loguj")
17 self.koniecBtn = QPushButton("&Koniec")
18
19 # układ przycisków Push ###
20 uklad = QHBoxLayout()
21 uklad.addWidget(self.logujBtn)
22 uklad.addWidget(self.koniecBtn)
23
24 # główny układ okna ###
25 ukladV = QVBoxLayout(self)
26 ukladV.addWidget(self.widok)
27 ukladV.addLayout(uklad)
28
29 # właściwości widżetu ###
30 self.setWindowTitle("Prosta lista zadań")
31 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:
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4from __future__ import unicode_literals
5from PyQt5.QtWidgets import QApplication, QWidget
6from PyQt5.QtWidgets import QMessageBox, QInputDialog
7from gui_z0 import Ui_Widget
8
9
10class Zadania(QWidget, Ui_Widget):
11
12 def __init__(self, parent=None):
13 super(Zadania, self).__init__(parent)
14 self.setupUi(self)
15
16 self.logujBtn.clicked.connect(self.loguj)
17 self.koniecBtn.clicked.connect(self.koniec)
18
19 def loguj(self):
20 login, ok = QInputDialog.getText(self, 'Logowanie', 'Podaj login:')
21 if ok:
22 haslo, ok = QInputDialog.getText(self, 'Logowanie', 'Podaj haslo:')
23 if ok:
24 if not login or not haslo:
25 QMessageBox.warning(
26 self, 'Błąd', 'Pusty login lub hasło!', QMessageBox.Ok)
27 return
28 QMessageBox.information(
29 self, 'Dane logowania',
30 'Podano: ' + login + ' ' + haslo, QMessageBox.Ok)
31
32 def koniec(self):
33 self.close()
34
35if __name__ == '__main__':
36 import sys
37
38 app = QApplication(sys.argv)
39 okno = Zadania()
40 okno.show()
41 okno.move(350, 200)
42 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

6.3.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:
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:
38class LoginDialog(QDialog):
39 """ Okno dialogowe logowania """
40
41 def __init__(self, parent=None):
42 super(LoginDialog, self).__init__(parent)
43
44 # etykiety, pola edycyjne i przyciski ###
45 loginLbl = QLabel('Login')
46 hasloLbl = QLabel('Hasło')
47 self.login = QLineEdit()
48 self.haslo = QLineEdit()
49 self.przyciski = QDialogButtonBox(
50 QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
51 Qt.Horizontal, self)
52
53 # układ główny ###
54 uklad = QGridLayout(self)
55 uklad.addWidget(loginLbl, 0, 0)
56 uklad.addWidget(self.login, 0, 1)
57 uklad.addWidget(hasloLbl, 1, 0)
58 uklad.addWidget(self.haslo, 1, 1)
59 uklad.addWidget(self.przyciski, 2, 0, 2, 0)
60
61 # sygnały i sloty ###
62 self.przyciski.accepted.connect(self.accept)
63 self.przyciski.rejected.connect(self.reject)
64
65 # właściwości widżetu ###
66 self.setModal(True)
67 self.setWindowTitle('Logowanie')
68
69 def loginHaslo(self):
70 return (self.login.text().strip(),
71 self.haslo.text().strip())
72
73 # metoda statyczna, tworzy dialog i zwraca (login, haslo, ok)
74 @staticmethod
75 def getLoginHaslo(parent=None):
76 dialog = LoginDialog(parent)
77 dialog.login.setFocus()
78 ok = dialog.exec_()
79 login, haslo = dialog.loginHaslo()
80 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:
from gui import Ui_Widget, LoginDialog
– i zmieniamy funkcję loguj()
:
19 def loguj(self):
20 login, haslo, ok = LoginDialog.getLoginHaslo(self)
21 if not ok:
22 return
23
24 if not login or not haslo:
25 QMessageBox.warning(self, 'Błąd',
26 'Pusty login lub hasło!', QMessageBox.Ok)
27 return
28
29 QMessageBox.information(self,
30 'Dane logowania', 'Podano: ' + login + ' ' + haslo, QMessageBox.Ok)
Przetestuj działanie nowego okna dialogowego.


6.3.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ą:
1# -*- coding: utf-8 -*-
2
3from peewee import *
4from datetime import datetime
5
6baza = SqliteDatabase('adresy.db')
7
8
9class BazaModel(Model): # klasa bazowa
10
11 class Meta:
12 database = baza
13
14
15class Osoba(BazaModel):
16 login = CharField(null=False, unique=True)
17 haslo = CharField()
18
19 class Meta:
20 order_by = ('login',)
21
22
23class Zadanie(BazaModel):
24 tresc = TextField(null=False)
25 datad = DateTimeField(default=datetime.now)
26 wykonane = BooleanField(default=False)
27 osoba = ForeignKeyField(Osoba, related_name='zadania')
28
29 class Meta:
30 order_by = ('datad',)
31
32
33def polacz():
34 baza.connect() # nawiązujemy połączenie z bazą
35 baza.create_tables([Osoba, Zadanie], True) # tworzymy tabele
36 ladujDane() # wstawiamy początkowe dane
37 return True
38
39
40def loguj(login, haslo):
41 try:
42 osoba, created = Osoba.get_or_create(login=login, haslo=haslo)
43 return osoba
44 except IntegrityError:
45 return None
46
47
48def ladujDane():
49 """ Przygotowanie początkowych danych testowych """
50 if Osoba.select().count() > 0:
51 return
52 osoby = ('adam', 'ewa')
53 zadania = ('Pierwsze zadanie', 'Drugie zadanie', 'Trzecie zadanie')
54 for login in osoby:
55 o = Osoba(login=login, haslo='123')
56 o.save()
57 for tresc in zadania:
58 z = Zadanie(tresc=tresc, osoba=o)
59 z.save()
60 baza.commit()
61 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ę:
import baza
Dalej uzupełniamy funkcję loguj()
:
20 def loguj(self):
21 """ Logowanie użytkownika """
22 login, haslo, ok = LoginDialog.getLoginHaslo(self)
23 if not ok:
24 return
25
26 if not login or not haslo:
27 QMessageBox.warning(self, 'Błąd',
28 'Pusty login lub hasło!', QMessageBox.Ok)
29 return
30
31 self.osoba = baza.loguj(login, haslo)
32 if self.osoba is None:
33 QMessageBox.critical(self, 'Błąd', 'Błędne hasło!', QMessageBox.Ok)
34 return
35
36 QMessageBox.information(self,
37 '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()
:
42if __name__ == '__main__':
43 import sys
44 app = QApplication(sys.argv)
45 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.3.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.
Informacja
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:
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt, QVariant
4
5
6class TabModel(QAbstractTableModel):
7 """ Tabelaryczny model danych """
8
9 def __init__(self, pola=[], dane=[], parent=None):
10 super(TabModel, self).__init__()
11 self.pola = pola
12 self.tabela = dane
13
14 def aktualizuj(self, dane):
15 """ Przypisuje źródło danych do modelu """
16 print(dane)
17 self.tabela = dane
18
19 def rowCount(self, parent=QModelIndex()):
20 """ Zwraca ilość wierszy """
21 return len(self.tabela)
22
23 def columnCount(self, parent=QModelIndex()):
24 """ Zwraca ilość kolumn """
25 if self.tabela:
26 return len(self.tabela[0])
27 else:
28 return 0
29
30 def data(self, index, rola=Qt.DisplayRole):
31 """ Wyświetlanie danych """
32 i = index.row()
33 j = index.column()
34
35 if rola == Qt.DisplayRole:
36 return '{0}'.format(self.tabela[i][j])
37 else:
38 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
:
64def czytajDane(osoba):
65 """ Pobranie zadań danego użytkownika z bazy """
66 zadania = [] # lista zadań
67 wpisy = Zadanie.select().where(Zadanie.osoba == osoba)
68 for z in wpisy:
69 zadania.append([
70 z.id, # identyfikator zadania
71 z.tresc, # treść zadania
72 '{0:%Y-%m-%d %H:%M:%S}'.format(z.datad), # data dodania
73 z.wykonane, # bool: czy wykonane?
74 False]) # bool: czy usunąć?
75 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:
from tabmodel import TabModel
Następnie tworzymy jego instancję. Uzupełniamy fragment uruchamiający aplikację
o kod: model = TabModel()
:
48if __name__ == '__main__':
49 import sys
50 app = QApplication(sys.argv)
51 baza.polacz()
52 model = TabModel()
53 okno = Zadania()
54 okno.show()
55 okno.move(350, 200)
56 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ę:
37 zadania = baza.czytajDane(self.osoba)
38 model.aktualizuj(zadania)
39 model.layoutChanged.emit()
40 self.odswiezWidok()
41
42 def odswiezWidok(self):
43 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”.

6.3.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:
19 # przyciski Push ###
20 self.logujBtn = QPushButton("Za&loguj")
21 self.koniecBtn = QPushButton("&Koniec")
22 self.dodajBtn = QPushButton("&Dodaj")
23 self.dodajBtn.setEnabled(False)
24
25 # układ przycisków Push ###
26 uklad = QHBoxLayout()
27 uklad.addWidget(self.logujBtn)
28 uklad.addWidget(self.dodajBtn)
29 uklad.addWidget(self.koniecBtn)
W pliku todopw.py
uzupełniamy konstruktor i dodajemy nową funkcję dodaj()
:
14 def __init__(self, parent=None):
15 super(Zadania, self).__init__(parent)
16 self.setupUi(self)
17
18 self.logujBtn.clicked.connect(self.loguj)
19 self.koniecBtn.clicked.connect(self.koniec)
20 self.dodajBtn.clicked.connect(self.dodaj)
21
22 def dodaj(self):
23 """ Dodawanie nowego zadania """
24 zadanie, ok = QInputDialog.getMultiLineText(self,
25 'Zadanie',
26 'Co jest do zrobienia?')
27 if not ok or not zadanie.strip():
28 QMessageBox.critical(self,
29 'Błąd',
30 'Zadanie nie może być puste.',
31 QMessageBox.Ok)
32 return
33
34 zadanie = baza.dodajZadanie(self.osoba, zadanie)
35 model.tabela.append(zadanie)
36 model.layoutChanged.emit() # wyemituj sygnał: zaszła zmiana!
37 if len(model.tabela) == 1: # jeżeli to pierwsze zadanie
38 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:
61 def odswiezWidok(self):
62 self.widok.setModel(model) # przekazanie modelu do widoku
63 self.widok.hideColumn(0) # ukrywamy kolumnę id
64 # ograniczenie szerokości ostatniej kolumny
65 self.widok.horizontalHeader().setStretchLastSection(True)
66 # dopasowanie szerokości kolumn do zawartości
67 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:
self.dodajBtn.setEnabled(True)
W pliku baza.py
dopisujemy jeszcze wspomnianą funkcję dodajZadanie()
:
78def dodajZadanie(osoba, tresc):
79 """ Dodawanie nowego zadania """
80 zadanie = Zadanie(tresc=tresc, osoba=osoba)
81 zadanie.save()
82 return [
83 zadanie.id,
84 zadanie.tresc,
85 '{0:%Y-%m-%d %H:%M:%S}'.format(zadanie.datad),
86 zadanie.wykonane,
87 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.

6.3.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
:
30 def data(self, index, rola=Qt.DisplayRole):
31 """ Wyświetlanie danych """
32 i = index.row()
33 j = index.column()
34
35 if rola == Qt.DisplayRole:
36 return '{0}'.format(self.tabela[i][j])
37 elif rola == Qt.CheckStateRole and (j == 3 or j == 4):
38 if self.tabela[i][j]:
39 return Qt.Checked
40 else:
41 return Qt.Unchecked
42 elif rola == Qt.EditRole and j == 1:
43 return self.tabela[i][j]
44 else:
45 return QVariant()
46
47 def flags(self, index):
48 """ Zwraca właściwości kolumn tabeli """
49 flags = super(TabModel, self).flags(index)
50 j = index.column()
51 if j == 1:
52 flags |= Qt.ItemIsEditable
53 elif j == 3 or j == 4:
54 flags |= Qt.ItemIsUserCheckable
55
56 return flags
57
58 def setData(self, index, value, rola=Qt.DisplayRole):
59 """ Zmiana danych """
60 i = index.row()
61 j = index.column()
62 if rola == Qt.EditRole and j == 1:
63 self.tabela[i][j] = value
64 elif rola == Qt.CheckStateRole and (j == 3 or j == 4):
65 if value:
66 self.tabela[i][j] = True
67 else:
68 self.tabela[i][j] = False
69
70 return True
71
72 def headerData(self, sekcja, kierunek, rola=Qt.DisplayRole):
73 """ Zwraca nagłówki kolumn """
74 if rola == Qt.DisplayRole and kierunek == Qt.Horizontal:
75 return self.pola[sekcja]
76 elif rola == Qt.DisplayRole and kierunek == Qt.Vertical:
77 return sekcja + 1
78 else:
79 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:
90pola = ['Id', 'Zadanie', 'Dodano', 'Zrobione', 'Usuń']
W pliku todopw.py
uzupełniamy jeszcze kod tworzący instancję modelu:
72if __name__ == '__main__':
73 import sys
74 app = QApplication(sys.argv)
75 baza.polacz()
76 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.

6.3.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:
19 # przyciski Push ###
20 self.logujBtn = QPushButton("Za&loguj")
21 self.koniecBtn = QPushButton("&Koniec")
22 self.dodajBtn = QPushButton("&Dodaj")
23 self.dodajBtn.setEnabled(False)
24 self.zapiszBtn = QPushButton("&Zapisz")
25 self.zapiszBtn.setEnabled(False)
26
27 # układ przycisków Push ###
28 uklad = QHBoxLayout()
29 uklad.addWidget(self.logujBtn)
30 uklad.addWidget(self.dodajBtn)
31 uklad.addWidget(self.zapiszBtn)
32 uklad.addWidget(self.koniecBtn)
W pliku todopw.py
kliknięcie przycisku „Zapisz” wiążemy z nową funkcją zapisz()
:
14 def __init__(self, parent=None):
15 super(Zadania, self).__init__(parent)
16 self.setupUi(self)
17
18 self.logujBtn.clicked.connect(self.loguj)
19 self.koniecBtn.clicked.connect(self.koniec)
20 self.dodajBtn.clicked.connect(self.dodaj)
21 self.zapiszBtn.clicked.connect(self.zapisz)
22
23 def zapisz(self):
24 baza.zapiszDane(model.tabela)
25 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:
self.zapiszBtn.setEnabled(True)
Pozostaje dopisanie na końcu pliku baza.py
funkcji zapisującej zmiany:
93def zapiszDane(zadania):
94 """ Zapisywanie zmian """
95 for i, z in enumerate(zadania):
96 # utworzenie instancji zadania
97 zadanie = Zadanie.select().where(Zadanie.id == z[0]).get()
98 if z[4]: # jeżeli zaznaczono zadanie do usunięcia
99 zadanie.delete_instance() # usunięcie zadania z bazy
100 del zadania[i] # usunięcie zadania z danych modelu
101 else:
102 zadanie.tresc = z[1]
103 zadanie.wykonane = z[3]
104 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.3.8. Materiały
Źródła:
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:
2025-04-12 o 10:21 w Sphinx 7.3.7
- Autorzy: