4.3. ToDoPw
Realizacja prostej listy zadań jako aplikacji okienkowej z wykorzystaniem Pythona 3 i biblioteki PyQt6. Aplikacja umożliwi dodawanie, usuwanie, edycję i oznaczanie jako wykonane zadań, zapisywanych w bazie SQLite obsługiwanej za pomocą systemu ORM Peewee. Przykład wykorzystuje programowanie obiektowe (ang. Object Oriented Programing) i ilustruje technikę programowania model/widok (ang. Model/View Programming).
W wybranym katalogu przygotuj środowisko wirtualne Pythona. Zainstaluj bibliotekę PyQt6 oraz pakiet Peewee w aktywowanym środowisku:
(.venv) pip install pyqt6 peewee
Uwaga
Wymagana wiedza:
Znajomość Pythona w stopniu średnim.
Znajomość podstaw projektowania interfejsu z wykorzystaniem biblioteki Qt (zob. scenariusze Kalkulator i Widżety).
Znajomość podstaw systemu ORM Peewee.
4.3.1. Interfejs
Zaczynamy od utworzenia pliku o nazwie gui.py, który posłuży
do definiowania składników interfejsu. Wklejamy do niego poniższy kod:
1from PyQt6.QtWidgets import QTableView, QPushButton
2from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout
3
4
5class UiWidget:
6 """ Klasa definiująca GUI """
7
8 def __init__(self):
9 # tabelaryczny widok danych
10 self.widok = QTableView()
11
12 # przyciski Push ###
13 self.loguj_btn = QPushButton("Za&loguj")
14 self.koniec_btn = QPushButton("&Koniec")
15
16 # układ przycisków Push ###
17 uklad = QHBoxLayout()
18 uklad.addWidget(self.loguj_btn)
19 uklad.addWidget(self.koniec_btn)
20
21 # główny układ okna ###
22 ukladV = QVBoxLayout(self)
23 ukladV.addWidget(self.widok)
24 ukladV.addLayout(uklad)
25
26 # właściwości widżetu ###
27 self.setWindowTitle("Prosta lista zadań")
28 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:
1from PyQt6.QtWidgets import QApplication, QWidget
2from PyQt6.QtWidgets import QMessageBox, QInputDialog
3from gui_z0 import UiWidget
4
5
6class Zadania(QWidget, UiWidget):
7
8 def __init__(self, parent=None):
9 super().__init__(parent)
10
11 self.loguj_btn.clicked.connect(self.loguj)
12 self.koniec_btn.clicked.connect(self.koniec)
13
14 def loguj(self):
15 login, ok = QInputDialog.getText(self, 'Logowanie', 'Podaj login:')
16 if ok:
17 haslo, ok = QInputDialog.getText(self, 'Logowanie', 'Podaj haslo:')
18 if ok:
19 if not login or not haslo:
20 QMessageBox.warning(
21 self, 'Błąd', 'Pusty login lub hasło!', QMessageBox.StandardButton.Ok)
22 return
23 QMessageBox.information(
24 self, 'Dane logowania',
25 'Podano: ' + login + ' ' + haslo, QMessageBox.StandardButton.Ok)
26
27 def koniec(self):
28 self.close()
29
30
31if __name__ == '__main__':
32 import sys
33 app = QApplication(sys.argv)
34 okno = Zadania()
35 okno.show()
36 okno.move(350, 200)
37 sys.exit(app.exec())
Podobnie jak w poprzednich scenariuszach klasa Zadania dziedziczy z klasy UiWidget,
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.
Ćwiczenie
Uruchom skrypt
todopw.py.
4.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 PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QDialog, QDialogButtonBox
from PyQt6.QtWidgets import QLabel, QLineEdit
from PyQt6.QtWidgets import QGridLayout
Na końcu pliku gui.py wstawiamy:
35class LoginDialog(QDialog):
36 """ Okno dialogowe logowania """
37
38 def __init__(self, parent=None):
39 super().__init__()
40
41 # etykiety, pola edycyjne i przyciski ###
42 login_lbl = QLabel('Login')
43 haslo_lbl = QLabel('Hasło')
44 self.login = QLineEdit()
45 self.haslo = QLineEdit()
46 self.haslo.setEchoMode(QLineEdit.EchoMode.Password)
47 self.przyciski = QDialogButtonBox(
48 (QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel))
49
50 # układ główny ###
51 uklad = QGridLayout(self)
52 uklad.addWidget(login_lbl, 0, 0)
53 uklad.addWidget(self.login, 0, 1)
54 uklad.addWidget(haslo_lbl, 1, 0)
55 uklad.addWidget(self.haslo, 1, 1)
56 uklad.addWidget(self.przyciski, 2, 0, 2, 0)
57
58 # sygnały i sloty ###
59 self.przyciski.accepted.connect(self.accept)
60 self.przyciski.rejected.connect(self.reject)
61
62 # właściwości widżetu ###
63 self.setModal(True)
64 self.setWindowTitle('Logowanie')
65
66 def login_haslo(self):
67 return (self.login.text().strip(),
68 self.haslo.text().strip())
69
70 # metoda statyczna, tworzy dialog i zwraca (login, haslo, ok)
71 @staticmethod
72 def get_login_haslo(self, parent=None):
73 dialog = LoginDialog(parent)
74 dialog.login.setFocus()
75 ok = dialog.exec()
76 login, haslo = dialog.login_haslo()
77 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 get_login_haslo() (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.
Omawiana metoda utworzy okno dialogowe (dialog = LoginDialog(parent))
i aktywuje pole loginu. Następnie wyświetli okno i zapisze odpowiedź użytkownika
(wciśnięty przycisk) w zmiennej: ok = dialog.exec().
Po zamknięciu okna pobierzemy wpisane dane za pomocą metody pomocniczej login_haslo()
i zwrócimy je, o ile użytkownik zamknie okno przyciskiem OK.
W pliku todopw.py uzupełniamy importy:
from gui import Ui_Widget, LoginDialog
– i zmieniamy funkcję loguj():
19
20 if not login or not haslo:
21 QMessageBox.warning(self, 'Błąd',
22 'Pusty login lub hasło!', QMessageBox.StandardButton.Ok)
23 return
24
25 QMessageBox.information(self,
26 'Dane logowania', 'Podano: ' + login + ' ' + haslo, QMessageBox.StandardButton.Ok)
27
28 def koniec(self):
29 self.close()
30
Przetestuj działanie nowego okna dialogowego.
4.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.
4.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”.
4.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.
4.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.
4.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ę.
4.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:
2026-05-30 o 19:12 w Sphinx 7.3.7
- Autorzy: