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 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:
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

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:
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:
from gui import Ui_Widget, LoginDialog
– i zmieniamy funkcję loguj()
:
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.


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 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ę:
import baza
Dalej uzupełniamy funkcję loguj()
:
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()
:
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.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 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
:
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:
from tabmodel import TabModel
Następnie tworzymy jego instancję. Uzupełniamy fragment uruchamiający aplikację
o kod: model = TabModel()
:
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ę:
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”.

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 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()
:
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:
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:
self.dodajBtn.setEnabled(True)
W pliku baza.py
dopisujemy jeszcze wspomnianą funkcję dodajZadanie()
:
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.

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 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:
90 | pola = ['Id', 'Zadanie', 'Dodano', 'Zrobione', 'Usuń']
|
W pliku todopw.py
uzupełniamy jeszcze kod tworzący instancję modelu:
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.

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 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()
:
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:
self.zapiszBtn.setEnabled(True)
Pozostaje dopisanie na końcu pliku baza.py
funkcji zapisującej zmiany:
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.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: | 2022-05-22 o 19:52 w Sphinx 1.5.3 |
---|---|
Autorzy: | Patrz plik “Autorzy” |