📝 Notatki o uczniu
Uczeń: Maks
Technik programista · 4 rok · Przygotowanie do INF.04
Dodaj notatkę
Historia notatek
Plan na dziś — 27 września 2026
90 minut na Google Meet. Zakres szkolnego sprawdzianu: tablice, CheckBox, RadioButton. Java, projekt Tablice (com.example.tablice) w Android Studio. Sesja kończy się sprawdzianem próbnym na 20 punktów.
R.id. na czerwono, to zniknie po udanym buildzie.Rozkład minut
| Minuty | Co robimy | Kto co pokazuje | Materiał |
|---|---|---|---|
| 0–8 | Diagnoza ustna, w tle leci Gradle sync. Pięć pytań: od jakiego indeksu zaczyna się tablica, co znaczy length, czym różni się CheckBox od RadioButtona, po co RadioGroup, co zwraca getCheckedRadioButtonId() gdy nic nie zaznaczono. Zapisz, na czym się zawaha — to są punkty, które utracicie na sprawdzianie. |
Rozmowa, bez ekranu | — |
| 8–30 | Tablice w kodzie (nie ListView — to inny temat). Teoria skrótem: deklaracja, indeksy, length, pętla, tablice równoległe, referencja. Zadania 11.1–11.6, z czego 11.3 i 11.6 koniecznie — to typowe „znajdź błąd" i „co wypisze". |
Czat: treści. Twój ekran: omówienie po próbie | 11. Tablice |
| 30–52 | CheckBox i RadioButton. Najpierw paleta: gdzie te kontrolki siedzą (kategoria Buttons) i dlaczego Plain Text wstawia <EditText>, a nie TextView. Potem XML i Java obu kontrolek, tabela porównawcza, cztery pułapki. Zadania 12.1–12.3 i 12.6 pisemnie, 12.4 w Android Studio. |
Czat, potem ekran ucznia | 12. CheckBox, RadioButton |
| 52–70 | Ćwiczenie #06 „Zapisy na warsztaty" — pełna aplikacja na 20 pkt: dwie tablice równoległe (nazwy i ceny), tablica CheckBox[], RadioGroup spięta z tablicą zniżek przez indexOfChild, suma, najdroższy z zaznaczonych, kwota po zniżce. Uczeń pisze sam, Ty patrzysz; wzorzec pokazujesz dopiero po próbie. Treść zadania wklejasz z instrukcji na czat. |
Ekran ucznia przez cały blok | #06 — instrukcjaKroki |
| 70–85 | Sprawdzian próbny. 12 pytań zamkniętych na czacie (uczeń odsyła jedną linią) plus dwa zadania kodowe. Sprawdzasz z klucza od razu. | Czat w obie strony | Sprawdzian próbny — treść i klucz |
| 85–90 | Omówienie błędów i zadanie domowe. Nie omawiaj wszystkiego — tylko to, co uczeń stracił, plus pytania 5, 7 i 10, jeśli je pomylił. | Twój ekran | — |
Jak odbierać odpowiedzi
| Typ zadania | Kanał zwrotny | Dlaczego tak |
|---|---|---|
| „Co wypisze", wartości, indeksy | czat, jedna linia na zadanie | Zapis zostaje uczniowi jako materiał do powtórki wieczorem przed sprawdzianem |
| Fragmenty kodu (11.2, 11.4, 11.5, 12.2) | czat — wklejony kod | Widzisz wcięcia i średniki, a uczeń ćwiczy pisanie kodu bez podpowiedzi edytora, dokładnie jak na kartce |
| Zadania w Android Studio (12.4, 12.5, ankieta) | udostępniony ekran | Liczy się też to, jak szuka błędu w Logcacie, a tego z gotowego kodu nie zobaczysz |
| Sprawdzian, część zamknięta | 1b 2c 3a … jedną linią | Sprawdzasz w kilkanaście sekund, uczeń nie widzi klucza |
Pięć pytań na diagnozę (blok 0–8)
- Co wstawi do XML-a pozycja Plain Text z palety? (
EditText— nazwa w palecie to nie nazwa klasy) - Od jakiego indeksu zaczyna się tablica i jaki ma ostatni? (0 oraz
length - 1) lengthczylength()— kiedy które? (tablica bez nawiasów, String z nawiasami)- Kiedy CheckBox, a kiedy RadioButton? (wiele odpowiedzi vs dokładnie jedna)
- Po co RadioButtonom RadioGroup? (bez niej da się zaznaczyć wszystkie naraz)
- Co zwraca
getCheckedRadioButtonId(), gdy nic nie wybrano? (-1, nienull)
Zadanie domowe — do wklejenia na koniec sesji
Przed sprawdzianem:
1. Przepisz ankietę od zera, bez zaglądania w kod - z pamięci. Jeśli
utkniesz, zajrzyj, zamknij i zacznij ten fragment jeszcze raz.
2. Zadania 11.3 i 11.6 (znajdź błąd, referencje tablic) - zrób ponownie
na kartce. To dwa najczęstsze pytania "na myślenie".
3. Trzy zdania do zapamiętania na pamięć:
- ostatni indeks to length - 1
- getCheckedRadioButtonId() zwraca -1, gdy nic nie zaznaczono
- tablica przypisana do drugiej zmiennej to ta sama tablica, nie kopia
4. Nazwy z palety: Plain Text = EditText (pole do wpisywania),
TextView = tylko wyświetla, CheckBox i RadioButton siedzą
w kategorii Buttons.
Czego dziś nie robimy
- ArrayList i RecyclerView — poza zakresem sprawdzianu, a mieszanie
size()zlengthtydzień przed testem tylko zaszkodzi. - Spinnera — też wybiera jedną opcję, ale nauczyciel wymienił RadioButton; wraca przy okazji ćwiczenia #04.
- Materiału maturalnego (teoria + SQL) przygotowanego na tę sesję — leży gotowy w module Matura, wraca po sprawdzianie.
Poprzednie sesje
| Data | Materiał, na którym pracowaliśmy | Stan |
|---|---|---|
| 05.09 | Zadanie 8 z arkusza CKE (SQL) — setup MySQL i phpMyAdmin, Runda 1 i 2 ćwiczeń; nowe ćwiczenie #05 (EAN-13) | do potwierdzenia, ile ćwiczeń Rundy 2 uczeń zrobił sam |
| 19.09 | GUI PyQt6 na strukturze form.ui — kroki w ćwiczeniach #02 i #05, pułapka z kolejnością w return | otwarte: czy zbudował formularz i podłączył sygnały |
| 27.09 | Android pod szkolny sprawdzian: tablice (6 zadań), CheckBox i RadioButton (5 zadań), mini-aplikacja, sprawdzian próbny na 20 pkt | dziś |
Plan na 4 października 2026
90 minut na Google Meet, w całości na algorytmikę: sortowania, wyszukiwanie binarne, złożoność obliczeniowa — kamień milowy planu rocznego, przesuwany dwukrotnie. Pracujecie w jednym notatniku Colab, który sam sprawdza odpowiedzi — nie na kartce pokazywanej w kamerze.
Wiadomość do ucznia przed sesją
Cześć, wracamy do matury: sortowania, wyszukiwanie binarne i złożoność
obliczeniowa. To dział, w którym na maturze trzeba umieć dwie rzeczy:
prześledzić algorytm krok po kroku i napisać go w Pythonie.
Pracujemy w jednym notatniku Colab - wyślę Ci link na czacie przed
sesją. Otwierasz go w przeglądarce, piszemy w nim obaj naraz, a kod
uruchamiasz jednym skrótem Ctrl+Enter. Nic nie instalujesz, nie musisz
mieć Pythona u siebie.
Notatnik sam sprawdza odpowiedzi: wpisujesz tablicę po każdym przebiegu
i od razu widzisz, który wiersz się nie zgadza.
Przygotuj:
1. Okno obok Meeta na notatnik (przy jednym ekranie podziel go na pół).
2. Kartka i długopis pomocniczo - wyniki i tak wpisujemy do notatnika,
bo zostaje Ci po lekcji jako materiał do powtórki.
Rozkład minut
| Minuty | Co robimy | Kto co pokazuje | Materiał |
|---|---|---|---|
| 0–5 | Start. Wrzucasz link do notatnika na czat, uczeń otwiera go obok Meeta i uruchamia pierwszą komórkę (#scrollTo=setup). Jedno zdanie o celu sesji: „dziś masz umieć rozpisać trzy sortowania i powiedzieć, ile który robi porównań i zamian". |
Notatnik — obaj w środku | — |
| 5–40 | Trzy sortowania O(n²). Zadania 4.1–4.3 na jednej tablicy 8, 3, 5, 1, 9, 2 — ten sam zestaw danych trzy razy pokazuje różnicę między metodami lepiej niż trzy osobne przykłady. Pierwsze rozpisujecie razem, drugie uczeń sam przy Twoim milczeniu, trzecie na czas. Po każdym: liczba porównań i zamian. |
Uczeń wpisuje w notatniku, sprawdzarka odpowiada od razu | Zadania 4.1–4.3 |
| 40–55 | Wyszukiwanie binarne (4.5): przebieg dla wartości istniejącej i nieistniejącej, liczba kroków, warunek posortowania. Uczeń wpisuje trójki (lewy, środek, prawy), a kurczący się zakres rysujesz na udostępnionym ekranie. |
Notatnik + Twój ekran na rysunek | Zadanie 4.5 |
| 55–68 | Złożoność (4.4): uczeń przypisuje klasy algorytmom, a komórka wypisuje gotową tabelę operacji dla n = 100, 1000 i 50 000. Tu widać, dlaczego przy maturalnym pliku wybór algorytmu decyduje o tym, czy program skończy: 2,5 miliarda operacji kontra 780 tysięcy. | Notatnik | Zadanie 4.4 |
| 68–87 | Implementacja w Pythonie (4.6): sortowanie przez wybór z licznikiem porównań, pisana w tym samym notatniku. Testy sprawdzają trzy rzeczy naraz: poprawność sortowania, licznik równy 15 i to, czy funkcja nie zepsuła listy przekazanej w argumencie. | Notatnik | Zadanie 4.6 |
| 87–90 | Zadanie domowe i termin arkusza próbnego. Zadanie 4.7 plus data arkusza próbnego #1 wpisana do kalendarza na sesji, nie „kiedyś". Notatnik zostaje uczniowi — to jego notatka z lekcji, ze sprawdzarkami działającymi też w domu. | Czat | — |
Gdy zabraknie czasu
| Blok | Co tnij | Czego nie tnij |
|---|---|---|
| Sortowania | Przez wstawianie (4.3) omów ustnie na przykładzie kart w ręce, bez pełnego rozpisywania | Bąbelkowe i przez wybór rozpisane w całości — na nich stoi różnica porównania kontra zamiany |
| Złożoność | Quicksort i mergesort tylko jako wiersz w tabeli | Porównanie liczby operacji dla n = 1000 — bez tego klasy złożoności zostają abstrakcją |
| Python | Uczeń pisze szkielet, resztę kończy w domu | Licznik porównań i wzór n(n-1)/2 |
Zadanie 4.7 (bąbelkowe z flagą) i tak idzie do domu — nie wrzucaj go do sesji, nawet jeśli zostanie pięć minut. Lepiej wykorzystać je na powtórzenie wyniku 15 / 9 / 5 z pamięci.
Jedno narzędzie na całą sesję: notatnik Colab
Kartka pokazywana w kamerze ma trzy wady przy lekcji zdalnej: źle widać, nie da się nic poprawić bez przepisywania całości i nie zostaje po niej ślad. Wszystko, co dziś robicie „na papierze", wchodzi do jednego notatnika, w którym pracujecie równocześnie — jak w dokumencie Google, tylko że komórki z kodem dają się uruchomić.
Notatnik ma komplet zadań 4.1–4.7 i sprawdza odpowiedzi automatycznie: uczeń wpisuje stan tablicy po każdym przebiegu, naciska Ctrl+Enter i od razu widzi, który wiersz się nie zgadza. Ty nie musisz sprawdzać ręcznie, a uczeń dostaje informację zwrotną w sekundę, nie po dwóch minutach czekania.
Przygotowanie — raz, pięć minut przed pierwszą sesją
| Krok | Co robisz |
|---|---|
| 1 | Pobierasz plik: sortowania.ipynb |
| 2 | Wchodzisz na colab.research.google.com → Plik → Prześlij notatnik → wskazujesz pobrany plik |
| 3 | Udostępnij (prawy górny róg) → Każdy z linkiem → uprawnienie Edytor → Kopiuj link |
| 4 | Link wklejasz uczniowi na czat Meeta. Ten sam notatnik służy przez całą sesję — uczeń niczego nie instaluje i nie zakłada konta |
Link do konkretnego zadania
Colab obsługuje kotwice: dopisanie #scrollTo= i identyfikatora komórki otwiera notatnik dokładnie w tym zadaniu. Dzięki temu nie mówisz „przewiń niżej, jeszcze niżej" — wklejasz link i uczeń jest na miejscu.
https://colab.research.google.com/drive/TWOJ_IDENTYFIKATOR#scrollTo=zad41
| Blok sesji | Zadanie | Kotwica do dopisania |
|---|---|---|
| start | dane i sprawdzarki — uczeń uruchamia jako pierwsze | #scrollTo=setup |
| 5–40 | 4.1 — bąbelkowe | #scrollTo=zad41 |
| 4.2 — przez wybór | #scrollTo=zad42 | |
| 4.3 — przez wstawianie | #scrollTo=zad43 | |
| 55–68 | 4.4 — złożoność | #scrollTo=zad44 |
| 40–55 | 4.5 — wyszukiwanie binarne | #scrollTo=zad45 |
| 68–87 | 4.6 — implementacja | #scrollTo=zad46 |
| do domu | 4.7 — bąbelkowe z flagą | #scrollTo=zad47 |
Po pierwsze: uprawnienie Czytelnik zamiast Edytor przy udostępnianiu — uczeń zobaczy notatnik, ale nic w nim nie wpisze, a Ty zauważysz to dopiero w trzeciej minucie zadania. Sprawdź to przed sesją, otwierając link w oknie incognito.
Po drugie: pisanie w notatniku za ucznia. Twoja rola to pytanie „co teraz?" i poprawianie dopiero wtedy, gdy sam nie znajdzie błędu. Gdy komórki wypełnia korepetytor, lekcja wygląda sprawnie, a uczeń wychodzi bez umiejętności rozpisania algorytmu samodzielnie — czyli bez tego, co punktuje CKE.
Rysunki (kurczący się zakres przy wyszukiwaniu binarnym, w październiku drzewa) robisz na udostępnionym ekranie w dowolnym edytorze — to jedyna rzecz, której notatnik nie zastąpi, i jedyny moment, gdy sięgasz po coś poza nim.
Czego pilnować w tym dziale
- Liczba porównań a liczba zamian to dwie różne odpowiedzi. W bąbelkowym i przez wybór porównań jest tyle samo (15 dla sześciu elementów), zamian 9 kontra 5. Uczeń, który podaje jedną liczbę na oba pytania, traci punkt.
- „Po przebiegu" nie znaczy „po zamianie". Najczęstszy błąd w rozpisywaniu: uczeń notuje tablicę po każdej zamianie i gubi się w połowie.
- Binarne tylko na posortowanych — na nieposortowanych nie wyrzuci błędu, tylko zwróci złą odpowiedź.
- Nie wchodź w quicksort z implementacją. Dziś wystarczy idea dziel i zwyciężaj oraz złożoność. Implementacja rekurencyjna ma sens dopiero po module o rekurencji.
Materiał, który czeka gotowy
| Temat | Gdzie | Kiedy wraca |
|---|---|---|
| Teoria za 6% arkusza: systemy liczbowe, logika, sieci, kompresja (21 zadań) | Matura → Teoria, moduły 1, 2, 9, 10 | następna sesja albo praca własna z arkusza PDF |
SQL na bazie przychodnia (12 zapytań + 2 domowe) | Matura → Teoria → Bazy danych i SQL | po sortowaniach, przed arkuszem próbnym |
| Android: tablice, CheckBox, RadioButton, ćwiczenie #06 | Android → Teoria 11, 12 i ćwiczenie 6 | zrobione 27.09 — materiał do powtórki po stronie ucznia |
GUI PyQt6 na strukturze form.ui (z 19.09) | Python → ćwiczenia #02 i #05, kroki GUI | zdjęte z planu sesji — uczeń kończy sam, sprawdzamy przy najbliższym zadaniu z GUI |
Poprzednie sesje
| Data | Materiał | Stan |
|---|---|---|
| 19.09 | GUI PyQt6 na strukturze form.ui | zdjęte z planu sesji — praca własna ucznia |
| 27.09 | Android pod szkolny sprawdzian: tablice, CheckBox, RadioButton, ćwiczenie #06 na 20 pkt, sprawdzian próbny | zrobione |
| 4.10 | Sortowania, wyszukiwanie binarne, złożoność (7 zadań) | dziś |
Plan na 15 listopada 2026
90 minut na Google Meet. Cały arkusz INF.04-01-25.01-SG ze stycznia 2025: część I (klasa z tablicą w Pythonie) i część II (galeria zdjęć w React). Uczeń pisze u siebie i udostępnia ekran. Każdą część zamyka sprawdzarką. Cel: obie aplikacje działające i sprawdzone, zrzuty i zipy mogą przejść do domu.
pliki3.zip bez hasła (12 zdjęć i dane.txt). Na egzaminie archiwum ma hasło K@tegorie). Na sesji go nie potrzebuje, ale niech raz zobaczy je zapisane, bo kończy się nawiasem.Wiadomość do ucznia na start
Dziś cały arkusz INF.04 ze stycznia 2025, ten z maila: najpierw
część I w Pythonie, potem część II w React. Pobierz dwie sprawdzarki:
1. https://moretutor.domoresoft.com/sprawdz_tablice.py
(do folderu z main.py, uruchamiasz: python sprawdz_tablice.py main.py)
2. https://moretutor.domoresoft.com/sprawdz_galeria.js
(wklejasz do konsoli przeglądarki przy otwartej galerii)
Rozkład minut
| Minuty | Co robimy | Kto co pokazuje | Materiał |
|---|---|---|---|
| 0–5 | Start i rozgrzewka. Pytania 1 i 2 z rozgrzewki Pythona ustnie: dwa podkreślenia i else: return -1. Na nich stoją dwie pułapki części I. |
Ekran ucznia | Rozgrzewka |
| 5–10 | Czytanie części I. Uczeń sam wypisuje, co ma robić każda z czterech metod: co wypisuje, co zwraca. Porównujecie z tabelą z kroku 5. | Ekran ucznia | Instrukcja I |
| 10–35 | Budowa części I, kroki 1–6. Przy kroku 4 zatrzymaj go i zapytaj o tablicę [3, 8, 5, 8], zanim napisze pętlę. |
Uczeń pisze, Ty pytasz | Budowa I |
| 35–42 | Sprawdzarka do 29/29, potem celowe self._tablica i powrót. Nagłówek dokumentacji pod szukaj. |
Terminal ucznia | Kroki 7–8 |
| 42–47 | Czytanie części II z obrazami 2–4. Pytanie kontrolne: „jaki kolor ma przycisk i skąd to wiesz?” Odpowiedź: zielony, tylko z ilustracji. | Arkusz na ekranie | Instrukcja II |
| 47–80 | Budowa części II, kroki 1–6. npm install leci w tle, gdy uczeń kopiuje dane z dane.txt. Przy kroku 6 najpierw niech napisze downloads++, jeśli sam na to wpadnie, i zobaczy, że ekran stoi. |
Uczeń pisze, Ty pytasz | Budowa II |
| 80–86 | Sprawdzarka w konsoli do 24/24. | Przeglądarka ucznia | Krok 7 |
| 86–90 | Do domu. Zrzuty konsola1 i web1–web4, plik egzamin, konsola.zip i web.zip, oba sprawdziany. Część I od zera na czas, limit 15 minut, czas do dziennika. |
Czat | Sprawdzian I Sprawdzian II |
Gdy zabraknie czasu
| Blok | Co tnij | Czego nie tnij |
|---|---|---|
| Część I | Nagłówek dokumentacji do domu | Sprawdzarkę: to ona pokazuje wymaganie o liczbie elementów, którego nie widać na ekranie |
| Część II | Sprawdzarka i zrzuty do domu, sam ją uruchomi | Krok 6 z nową tablicą. Bez niego licznik nie działa i punkt przepada |
Czego pilnować w tym arkuszu
- Pola z dwoma podkreśleniami. Jedno podkreślenie nie blokuje klasy potomnej, a arkusz wymaga tego wprost.
- Pętle po
self.__liczba_elementow. Nigdylen, nigdysum. - Metody liczące zwracają, program główny wypisuje. Przy -1 program główny milczy.
- Zielony przycisk i przełączniki w rzędzie. Oba wynikają z ilustracji, nie z listy wymagań.
- Nowa tablica w
setZdjecia. Mutacja wygląda prawie dobrze i dlatego jest groźna.
Pliki
| Plik | Dla kogo | Gdzie |
|---|---|---|
Arkusz PDF i pliki3.zip | uczeń, wysłane mailem 15.11 | arkusze.pl |
sprawdz_tablice.py | uczeń, część I | moretutor.domoresoft.com/sprawdz_tablice.py |
sprawdz_galeria.js | uczeń, część II, konsola przeglądarki | moretutor.domoresoft.com/sprawdz_galeria.js |
Wzorcowy main.py | Ty, po próbie ucznia | Python → ćwiczenie #08 → Gotowy kod |
Wzorcowy App.jsx | Ty, po próbie ucznia | React → ćwiczenie #1 → Gotowy kod |
Poprzednie sesje
| Data | Materiał | Stan |
|---|---|---|
| 4.10 | Matura: sortowania, wyszukiwanie binarne, złożoność | przebieg niezapisany w dzienniku |
| 8.11 | INF.04 czerwiec 2026: generator haseł i menedżer haseł w React | czas „na czas” niezapisany w dzienniku |
| 15.11 | INF.04 styczeń 2025: klasa z tablicą i galeria zdjęć | dziś |
Plan na 8 listopada 2026
90 minut na Google Meet. Egzamin zawodowy INF.04, czerwiec 2026, część I: aplikacja konsolowa, czyli generator bezpiecznych haseł w Pythonie. Uczeń pisze u siebie w PyCharm albo VS Code, tak jak na egzaminie, i udostępnia ekran. Program sprawdza sprawdzarką, która wywołuje jego funkcję 10 000 razy. Cel sesji: część I razem z dokumentacją w 40 minut, samodzielnie.
Wiadomość do ucznia przed sesją
Cześć, dziś robimy egzamin zawodowy INF.04 z czerwca 2026, część I:
aplikację konsolową, czyli generator bezpiecznych haseł. Piszesz u siebie,
w PyCharm albo VS Code, tak jak na egzaminie, i udostępniasz mi ekran.
Przed sesją pobierz dwa pliki:
1. https://moretutor.domoresoft.com/Web1.7z
(to archiwum z egzaminu, hasło dostaniesz na lekcji, tak jak na egzaminie)
2. https://moretutor.domoresoft.com/sprawdz_hasla.py
(sprawdzarka, która testuje Twój program)
Przygotuj:
- działającego Pythona i środowisko: sprawdź, że pusty plik z print("test")
się uruchamia
- program do rozpakowania .7z z hasłem: na Macu Keka albo The Unarchiver
Rozkład minut
| Minuty | Co robimy | Kto co pokazuje | Materiał |
|---|---|---|---|
| 0–5 | Start i rozgrzewka. Uczeń udostępnia ekran z otwartym IDE. Jedno zdanie o celu: „po tej lekcji robisz część I arkusza sam w 40 minut”. Pytania 1 i 4 z rozgrzewki zadajesz ustnie, bo na nich stoją dwie pułapki dzisiejszego zadania. | Ekran ucznia | Rozgrzewka |
| 5–15 | Czytanie arkusza. Wklejasz na czat treść części I. Uczeń czyta i sam wypisuje w pliku wymagania.txt listę rzeczy, za które dostaje punkty. Potem porównujecie ją z tabelą „Co sprawdza egzaminator”. Zwykle brakuje mu „każdy znak w losowym miejscu” i „hasło w zmiennej napisowej”. |
Ekran ucznia, Twoja tabela | Instrukcja |
| 15–50 | Budowa, kroki 1–6. Folder egzaminacyjny, archiwum z hasłem, zbiory znaków, losuj_znaki, dwa etapy generuj_haslo, program główny. Przy kroku 5 zatrzymujesz go i rozpisujecie trzy obroty pętli w tabeli, zanim cokolwiek uruchomi. |
Uczeń pisze, Ty pytasz | Budowa |
| 50–58 | Sprawdzarka (krok 7). Uruchomienie do 15/15. Potem uczeń celowo psuje krok 3 na randint(0, len(zbior)), widzi IndexError w około połowie wywołań i cofa zmianę. |
Terminal ucznia | Krok 7 |
| 58–72 | Dokumentacja i oddanie (krok 8). Nagłówek według listingu 1, zrzut całego ekranu z paskiem zadań, egzamin.txt, konsola.zip z całym projektem i kopia main.py. Na koniec uczeń pokazuje Ci drzewo folderów i porównujecie je ze schematem z kroku 1. |
Ekran ucznia | Krok 8 |
| 72–87 | Na czas, bez podglądu. Nowy pusty folder i nowy plik, a gotowy kod zamknięty. Limit 12 minut na sam program, potem sprawdzarka. Mierzysz czas i zapisujesz go do dziennika. To jest jedyny dzisiejszy pomiar, który mówi coś o egzaminie. | Uczeń sam, Ty milczysz | — |
| 87–90 | Do domu i sprawdzian. Wariant 16-znakowy z sekcji „Gotowy kod” oraz część II tego samego arkusza (aplikacja webowa w React na szablon.txt, zrzuty web1, web2, web3). Sprawdzian z 5 pytań uczeń robi w domu albo na starcie następnej sesji. |
Czat | Sprawdzian |
Gdy zabraknie czasu
| Blok | Co tnij | Czego nie tnij |
|---|---|---|
| Czytanie arkusza | Lista wymagań ustnie zamiast w pliku | Porównanie z tabelą egzaminatora: to ona uczy czytać arkusz |
| Dokumentacja | Zip i egzamin.txt idą do domu | Nagłówek według listingu 1 i jeden zrzut całego ekranu. To osobny rezultat oceniany przez egzaminatora |
| Na czas | Cały blok do domu, z czasem zmierzonym przez ucznia | — |
Kroku 5 (wstawianie w losowe miejsce) nie skracaj do pokazania gotowego kodu. To jedyny fragment zadania, którego nie da się zrobić „na pamięć z lekcji o pętlach”, i na nim najczęściej przepada punkt.
Czego pilnować w tym zadaniu
- Dwa
randint, dwa różne zakresy. Indeks znaku:0 .. len(zbior) - 1. Pozycja w haśle:0 .. len(haslo). Uczeń, który „na wszelki wypadek” wszędzie odejmuje jedynkę, wywala program na pierwszym obrocie. return, nieprint. Funkcja zwraca hasło, a program główny je wypisuje. Na ekranie wygląda to identycznie, w punktacji nie.- Żadnego
shuffle,sample,choices,secrets. Sprawdzarka zgłasza je jako błąd. - Polskie znaki kopiowane z
znaki.txt. Jedenźzamiastżi zbiór nie zgadza się z arkuszem. - Pisze uczeń. Gdy zaczynasz dyktować, przerwij i zapytaj, co ma się wydarzyć w następnej linijce.
Pliki
| Plik | Dla kogo | Gdzie |
|---|---|---|
Web1.7z (hasło *LoGiny&H@sla8) | uczeń, przed sesją | moretutor.domoresoft.com/Web1.7z |
sprawdz_hasla.py | uczeń, przed sesją | moretutor.domoresoft.com/sprawdz_hasla.py |
| Arkusz PDF | Ty, treść na czat | arkusze.pl |
Wzorcowy main.py | Ty, po próbie ucznia | Python → ćwiczenie #07 → Gotowy kod |
| Część II: menedżer haseł w React | Ty i uczeń, po części I | React → ćwiczenie #07 |
sprawdz_web.js | uczeń, wkleja do konsoli przeglądarki | moretutor.domoresoft.com/sprawdz_web.js |
Poprzednie sesje
| Data | Materiał | Stan |
|---|---|---|
| 27.09 | Android pod szkolny sprawdzian: tablice, CheckBox, RadioButton, ćwiczenie #06 na 20 pkt | zrobione |
| 4.10 | Matura: sortowania, wyszukiwanie binarne, złożoność (7 zadań, notatnik Colab) | przebieg niezapisany w dzienniku |
| 8.11 | INF.04 czerwiec 2026, część I: generator haseł | dziś |
Dashboard
Narzędzie korepetytora — egzaminy, ćwiczenia, pełne prowadzenie lekcji.
Sesje
Dziś — 15.11
90 min na Meet · INF.04 styczeń 2025, cały arkusz · klasa z tablicą w Pythonie (29 wymagań) · galeria zdjęć w React (24 wymagania)
Poprzednia — 8.11
90 min · INF.04 czerwiec 2026 · generator haseł w Pythonie · menedżer haseł w React
Przedmioty
⚛️ React — INF-04
4 ćwiczenia · 12 modułów teorii · Egzamin zawodowy
🐍 Python — INF-04
7 ćwiczeń · 10 modułów teorii · Egzamin zawodowy
🎓 Matura z informatyki
4 ćwiczenia · 10 modułów teorii · Egzamin maturalny
🅰️ Angular — INF-04
4 ćwiczenia · 12 modułów teorii · Egzamin zawodowy
🖥️ JavaFX — INF-04
4 ćwiczenia · 10 modułów teorii · Aplikacje desktopowe
📱 Android — INF-04
4 ćwiczenia · 10 modułów teorii · Aplikacje mobilne
React — egzaminy / ćwiczenia
#01 — Galeria zdjęć
INF.04 styczeń 2025, część II · switch · map z warunkiem · nowa tablica przy pobraniu · sprawdzarka w konsoli
#02 — Lista zadań (Todo)
Ćwiczenie · controlled input · dodawanie/usuwanie · toggle · select
#03 — Tracker wydatków
Ćwiczenie · .reduce() · number input · wyszukiwarka · Bootstrap card
#04 — Dziennik ocen
Ćwiczenie · edycja elementu · .sort() · .find() · dynamic select
Python — egzaminy / ćwiczenia
#01 — Rejestr produktów
Ćwiczenie własne · open/split · SQLite · PyQt6 GUI
#02 — Dziennik ucznia
Oceny · średnia · filtr przedmiot · SQLite · PyQt6
#03 — Wypożyczalnia książek
Katalog · wypożyczenie · UPDATE · SQLite · PyQt6
#04 — Stacja pogodowa
Pomiary · min/max/avg · daty · SQLite · PyQt6
#05 — Logowanie z nawigacją menu
QStackedWidget · QMenu · navbar · open() · sprawdzanie danych · PyQt6 GUI
#08 — Klasa z tablicą
INF.04 styczeń 2025, część I · pola prywatne · wyszukiwanie · średnia · sprawdzarka 29 wymagań
#09 — Loteria
INF.04 czerwiec 2025, część I · 6 z 49 bez powtórzeń · lista list · zliczanie · bez zmiennych globalnych · sprawdzarka 16 wymagań
Matura — egzaminy / ćwiczenia
#01 — Analiza algorytmu
Pseudokod · śledzenie zmiennych · wynik algorytmu · złożoność
#02 — Programowanie Python
Plik tekstowy · przetwarzanie danych · statystyki · wynik do pliku
#03 — Arkusz kalkulacyjny
Formuły · JEŻELI · LICZ.JEŻELI · WYSZUKAJ.PIONOWO · wykresy
#04 — Bazy danych SQL
SELECT · JOIN · GROUP BY · HAVING · podzapytania · CREATE VIEW
#05 — Kody produktów
Napisy · cyfra kontrolna EAN-13 · palindromy · duplikaty · PyQt6 GUI
📖 Baza wiedzy — React
| Moduł | Temat | Kluczowe pojęcia |
|---|---|---|
| 1 | Co to React? | Biblioteka UI, Virtual DOM, komponenty |
| 2 | Środowisko | Node.js, Vite, npm, struktura projektu |
| 3 | JSX | className, klamry {}, ternary, Fragment |
| 4 | Komponenty | Funkcja → JSX, export/import, PascalCase |
| 5 | Props | Dane rodzic→dziecko, destrukturyzacja, read-only |
| 6 | State | useState, setX, re-render, controlled input |
| 7 | Eventy | onClick, onChange, onSubmit, e.preventDefault() |
| 8 | Tablice | .map(), key, .filter(), .find(), .sort(), spread |
| 9 | Warunkowy rendering | ternary, &&, wczesny return, klasy warunkowe |
| 10 | useEffect | Efekty uboczne, zależności, cleanup, fetch |
| 11 | Formularze | Controlled inputs, select, checkbox, walidacja |
| 12 | Struktura projektu | Import/eksport, foldery, komendy npm, spread |
📝 Ściąga React
16 kart ze wzorcami kodu
🐛 Typowe błędy
10 najczęstszych błędów
📖 Baza wiedzy — Python
| Moduł | Temat | Kluczowe pojęcia |
|---|---|---|
| 1 | Podstawy Python | Interpreter, typy, zmienne, input/print, f-string |
| 2 | Instrukcje warunkowe | if/elif/else, and/or/not, in, zagnieżdżone |
| 3 | Pętle | for, while, range(), break/continue, enumerate |
| 4 | Funkcje | def, return, parametry domyślne, lambda |
| 5 | Stringi | Metody, f-string, slicing, split/join |
| 6 | Listy | append/sort/pop, comprehension, slicing |
| 7 | Słowniki i krotki | dict, tuple, set, .items(), unpacking |
| 8 | Pliki | with open(), read/write, CSV, split(";") |
| 9 | PyQt6 | QApplication, QLabel, QLineEdit, QPushButton, QListWidget, QGridLayout |
| 10 | SQLite | connect, CREATE, INSERT, SELECT, ?, fetchall |
📝 Ściąga Python
16 kart ze wzorcami kodu
🐛 Typowe błędy
10 najczęstszych błędów Python
📖 Baza wiedzy — Matura z informatyki
| Moduł | Temat | Kluczowe pojęcia |
|---|---|---|
| 1 | Systemy liczbowe | BIN, OCT, DEC, HEX, konwersje, arytmetyka |
| 2 | Logika i algebra Boole'a | AND, OR, NOT, XOR, tablice prawdy, bramki |
| 3 | Algorytmy i pseudokod | Śledzenie, zmienne, warunek stopu, złożoność |
| 4 | Sortowanie i wyszukiwanie | Bubble, selection, insertion, binary search |
| 5 | Rekurencja | Silnia, Fibonacci, wieże Hanoi, drzewo wywołań |
| 6 | Python — pliki i dane | open(), split(), listy, przetwarzanie tekstowe |
| 7 | Arkusz kalkulacyjny | JEŻELI, LICZ.JEŻELI, SUMA.JEŻELI, WYSZUKAJ.PIONOWO |
| 8 | Bazy danych i SQL | SELECT, JOIN, GROUP BY, HAVING, podzapytania |
| 9 | Sieci komputerowe | Adresacja IP, maski, podsieci, DNS, protokoły |
| 10 | Teoria informacji | Kodowanie, ASCII, UTF-8, kompresja, entropia |
📝 Ściąga Matura
Kluczowe wzory i formuły maturalne
🐛 Typowe błędy
10 najczęstszych błędów na maturze
📊 Mapa umiejętności — co uczy każde zadanie
| Umiejętność | #01 Galeria | #02 Todo | #03 Wydatki | #04 Oceny |
|---|---|---|---|---|
| useState (boolean) | ✅ | ✅ | — | — |
| useState (tablica obiektów) | ✅ | ✅ | ✅ | ✅ |
| useState (string — controlled input) | — | ✅ | ✅ | ✅ |
| useState (number — input type number) | — | — | ✅ | — |
| .filter() | ✅ | ✅ | ✅ | ✅ |
| .filter() po tekście (.includes) | — | — | ✅ | — |
| .map() w JSX | ✅ | ✅ | ✅ | ✅ |
| .reduce() — suma / średnia | — | — | ✅ | ✅ |
| spread — zmiana pola obiektu | ✅ | ✅ | — | ✅ |
| spread — dodanie do tablicy | — | ✅ | ✅ | ✅ |
| usuwanie z tablicy (.filter by id) | — | ✅ | ✅ | ✅ |
| toggle boolean | — | ✅ | — | — |
| warunkowe klasy CSS | — | ✅ | ✅ | ✅ |
| warunkowe renderowanie (alert) | — | — | ✅ | ✅ |
| <select> + onChange | — | ✅ | ✅ | ✅ |
| input type="number" + parseFloat | — | — | ✅ | — |
| .toFixed(2) formatowanie | — | — | ✅ | ✅ |
| .toLowerCase() + .includes() | — | — | ✅ | — |
| Bootstrap switch | ✅ | — | — | — |
| Bootstrap table / badge | — | ✅ | ✅ | ✅ |
| Bootstrap card / alert | — | — | ✅ | ✅ |
| Date.now() jako ID | — | ✅ | ✅ | ✅ |
| edycja istniejącego elementu | — | — | — | ✅ NOWE |
| .find() — wyszukanie po id | — | — | — | ✅ NOWE |
| .sort() — sortowanie tablicy | — | — | — | ✅ NOWE |
| [...new Set()] — unikalne wartości | — | — | — | ✅ NOWE |
| new Date().toISOString() — bieżąca data | — | — | — | ✅ NOWE |
| warunkowy tekst/styl przycisku | — | — | — | ✅ NOWE |
| select z dynamicznymi opcjami | — | — | — | ✅ NOWE |
🔧 Reset i narzędzia
🗑 Resetuj postęp kroków
Odznacz wszystkie checkboxy kroków we wszystkich ćwiczeniach
🔄 Resetuj sprawdziany
Wyczyść wyniki i odpowiedzi we wszystkich testach
📕 Ukryj rozwiązania
Schowaj wszystkie odsłonięte rozwiązania zadań w modułach teorii
⚠️ Reset WSZYSTKIEGO
Kroki + sprawdziany + rozwiązania — pełny reset do stanu początkowego
1. Co to jest React?
Stary sposób vs React
<div id="licznik">0</div>
<button onclick="zwieksz()">Kliknij</button>
<script>
let wartosc = 0;
function zwieksz() {
wartosc++;
document.getElementById('licznik').textContent = wartosc;
}
</script>
import { useState } from 'react';
function Licznik() {
const [wartosc, setWartosc] = useState(0);
return (
<div>
<p>{wartosc}</p>
<button onClick={() => setWartosc(wartosc + 1)}>Kliknij</button>
</div>
);
}
Virtual DOM
Zmiana danych → React tworzy nowy Virtual DOM
→ Porównuje: stary vs nowy → Aktualizuje TYLKO zmienione elementy
Architektura komponentowa
Strona
├── Navbar (Logo + Menu)
├── Treść (Post × 3)
└── Footer
📌 Zadanie: Opisz słownie
Napisz własnymi słowami (2-3 zdania na każdy punkt):
1. Co robi Virtual DOM i dlaczego jest szybszy niż bezpośrednia manipulacja DOM?
2. Na czym polega architektura komponentowa — dlaczego dzielimy interfejs na małe kawałki?
1. Virtual DOM to lekka kopia prawdziwego DOM-u trzymana w pamięci.
Gdy stan się zmienia, React tworzy nową wersję Virtual DOM,
porównuje ją ze starą (diffing) i aktualizuje w prawdziwym DOM
tylko te elementy, które faktycznie się zmieniły.
2. Architektura komponentowa polega na dzieleniu interfejsu na
niezależne, wielokrotnego użytku kawałki (np. Nagłówek, Przycisk,
Karta). Każdy komponent odpowiada za jedną rzecz — to ułatwia
pisanie, testowanie i utrzymanie aplikacji.
2. Środowisko
Sprawdzenie Node.js
node --version # potrzebne v18+
npm --version
Tworzenie projektu Vite
cd ~/Desktop
npm create vite@latest egzamin -- --template react
cd egzamin
npm install
npm install bootstrap
mkdir -p public/assets
npm run dev
Struktura projektu
egzamin/
├── node_modules/ ← pakiety (NIE dotykamy)
├── public/assets/ ← obrazy galerii
├── src/
│ ├── App.css ← style
│ ├── App.jsx ← główny komponent
│ └── index.js ← punkt wejścia
├── index.html
└── package.json
Punkt wejścia
// src/index.js
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(<App />)
📌 Zadanie: Stwórz projekt z pamięci
Bez zaglądania do notatek wpisz z pamięci komendy, które:
1. Tworzą nowy projekt React za pomocą Vite (szablon React)
2. Wchodzą do folderu projektu
3. Instalują zależności
4. Uruchamiają serwer deweloperski
npm create vite@latest moj-projekt -- --template react
cd moj-projekt
npm install
npm run dev
3. JSX
JSX = JavaScript XML — składnia HTML-podobna wewnątrz JS.
Transformacja
const el = <h1 className="tytul">Cześć!</h1>;
// kompiluje się do:
const el = React.createElement("h1", { className: "tytul" }, "Cześć!");
5 zasad JSX
// ❌ return ( <h1>A</h1> <p>B</p> );
// ✅ return ( <> <h1>A</h1> <p>B</p> </> );
// ❌ <br> ✅ <br />class jest słowem kluczowym JS → w JSX: className.
| HTML | JSX |
|---|---|
| onclick | onClick |
| for | htmlFor |
<p style={{ color: 'red', fontSize: '16px' }}>Tekst</p>Wyrażenia w klamrach {}
const imie = 'Maks';
return (
<div>
<h1>Cześć, {imie}!</h1>
<p>Wynik: {2 + 2}</p>
{zalogowany ? 'Tak' : 'Nie'}
{show && <p>Widoczne!</p>}
</div>
);
📌 Zadanie: Napraw błędy JSX
Poniższy kod zawiera 3 błędy JSX. Znajdź je i napisz poprawioną wersję:
function Wizytowka() {
return (
<div>
<h1 class="tytul">Jan Kowalski</h1>
<img src="foto.jpg">
<label for="email">Email:</label>
</div>
);
}
function Wizytowka() {
return (
<div>
<h1 className="tytul">Jan Kowalski</h1>
<img src="foto.jpg" />
<label htmlFor="email">Email:</label>
</div>
);
}
// Błąd 1: class → className
// Błąd 2: <img> zamknięty → <img />
// Błąd 3: for → htmlFor
📌 Zadanie: Wyrażenia w JSX
Napisz komponent Powitanie, który:
— ma zmienną imie ze swoim imieniem
— ma zmienną godzina z aktualną godziną (new Date().getHours())
— wyświetla w JSX: „Cześć, {imię}! Jest godzina {godzina}."
function Powitanie() {
const imie = "Maks";
const godzina = new Date().getHours();
return <p>Cześć, {imie}! Jest godzina {godzina}.</p>;
}
4. Komponenty
Komponent = funkcja JS zwracająca JSX. Nazwa od wielkiej litery.
function Naglowek() {
return <header><h1>Moja Aplikacja</h1></header>;
}
function App() {
return <div><Naglowek /><p>Treść</p></div>;
}
Podział na pliki
// src/components/Naglowek.jsx
export default function Naglowek() {
return <header><h1>App</h1></header>;
}
// src/App.jsx
import Naglowek from './components/Naglowek';
📌 Zadanie: Komponent Karta
Napisz komponent Karta, który renderuje div z klasą "karta" zawierający:
— nagłówek <h2> z tytułem „Mój projekt"
— paragraf <p> z opisem „To jest opis mojego projektu."
— przycisk z tekstem „Szczegóły"
function Karta() {
return (
<div className="karta">
<h2>Mój projekt</h2>
<p>To jest opis mojego projektu.</p>
<button>Szczegóły</button>
</div>
);
}
export default Karta;
5. Props
Props = dane przekazywane do komponentu z zewnątrz.
<Karta nazwa="Laptop" cena={3999} />
function Karta({ nazwa, cena }) {
return <div><h3>{nazwa}</h3><p>{cena} zł</p></div>;
}
state.📌 Zadanie: Komponent z propsami
Stwórz komponent Produkt, który przyjmuje dwa propsy: nazwa i cena.
Następnie użyj go 3 razy w komponencie App z różnymi danymi.
function Produkt({ nazwa, cena }) {
return (
<div className="produkt">
<h3>{nazwa}</h3>
<p>Cena: {cena} zł</p>
</div>
);
}
function App() {
return (
<div>
<Produkt nazwa="Laptop" cena={3500} />
<Produkt nazwa="Myszka" cena={89} />
<Produkt nazwa="Klawiatura" cena={249} />
</div>
);
}
6. State (useState)
import { useState } from 'react';
const [wartosc, setWartosc] = useState(0);
// ↑ ↑ ↑
// aktualna setter start
Licznik
function Licznik() {
const [n, setN] = useState(0);
return (
<div>
<p>{n}</p>
<button onClick={() => setN(n + 1)}>+</button>
</div>
);
}
Controlled input
const [imie, setImie] = useState('');
<input value={imie} onChange={(e) => setImie(e.target.value)} />
Zasady Hooks
- Tylko na górze komponentu
- Konwencja:
[coś, setCoś] - Można mieć wiele useState
📌 Zadanie: Licznik z przyciskami
Zbuduj komponent Licznik, który:
— przechowuje liczbę w stanie (początkowa wartość 0)
— ma przycisk + zwiększający o 1
— ma przycisk − zmniejszający o 1
— ma przycisk Reset ustawiający na 0
— wyświetla aktualną wartość między przyciskami
import { useState } from "react";
function Licznik() {
const [liczba, setLiczba] = useState(0);
return (
<div>
<button onClick={() => setLiczba(liczba - 1)}>−</button>
<span> {liczba} </span>
<button onClick={() => setLiczba(liczba + 1)}>+</button>
<br />
<button onClick={() => setLiczba(0)}>Reset</button>
</div>
);
}
export default Licznik;
7. Eventy (onClick, onChange, onSubmit)
React obsługuje zdarzenia przez camelCase propsy: onClick, onChange, onSubmit, onFocus, onKeyDown…
Klik — handler inline vs referencyjna
// inline (krótka akcja)
<button onClick={() => console.log('klik')}>Klik</button>
// referencyjna (lepsza czytelność)
function App() {
function handleClick() {
alert('Kliknięto!');
}
return <button onClick={handleClick}>Klik</button>;
}
onClick={handleClick} — bez nawiasów! Gdybyś napisał onClick={handleClick()}, funkcja wykonałaby się od razu przy renderze.onChange — wartość inputa
function Szukaj() {
const [q, setQ] = useState('');
return (
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Szukaj…"
/>
);
}
onSubmit — formularze
function Form() {
function handleSubmit(e) {
e.preventDefault(); // ← blokuje przeładowanie
console.log('wysłano!');
}
return (
<form onSubmit={handleSubmit}>
<input />
<button type="submit">Wyślij</button>
</form>
);
}
Przekazywanie argumentów do handlera
// Gdy potrzebujesz przekazać id lub inne dane:
<button onClick={() => usunElement(id)}>Usuń</button>
Lista ważnych eventów
| Event | Opis | Typowe użycie |
|---|---|---|
onClick | Kliknięcie | Przyciski, karty |
onChange | Zmiana wartości | Input, select |
onSubmit | Wysłanie formularza | Form |
onKeyDown | Naciśnięcie klawisza | Enter do zatwierdzenia |
onFocus / onBlur | Focus / utrata | Walidacja pól |
onMouseEnter / onMouseLeave | Hover | Tooltipy |
📌 Zadanie: Podgląd tekstu na żywo
Stwórz komponent Podglad, który:
— ma pole <input> tekstowe
— pod spodem wyświetla w <p> dokładnie to, co użytkownik wpisuje w czasie rzeczywistym
Użyj zdarzenia onChange i hooka useState.
import { useState } from "react";
function Podglad() {
const [tekst, setTekst] = useState("");
return (
<div>
<input
type="text"
value={tekst}
onChange={(e) => setTekst(e.target.value)}
placeholder="Wpisz coś..."
/>
<p>Podgląd: {tekst}</p>
</div>
);
}
export default Podglad;
8. Tablice i .map()
Renderowanie list w React = .map() na tablicy + key na każdym elemencie.
Podstawowy .map()
const owoce = ['Jabłko', 'Banan', 'Gruszka'];
function Lista() {
return (
<ul>
{owoce.map((owoc, i) => (
<li key={i}>{owoc}</li>
))}
</ul>
);
}
id z danych. index → tylko gdy lista się nie zmienia..map() z obiektami
const uzytkownicy = [
{ id: 1, imie: 'Jan', wiek: 25 },
{ id: 2, imie: 'Ola', wiek: 22 },
];
function Uzytkownicy() {
return (
<div>
{uzytkownicy.map(u => (
<div key={u.id}>
<h3>{u.imie}</h3>
<p>Wiek: {u.wiek}</p>
</div>
))}
</div>
);
}
.filter() — filtrowanie listy
// Pełnoletni użytkownicy
{uzytkownicy
.filter(u => u.wiek >= 18)
.map(u => <p key={u.id}>{u.imie}</p>)}
.find() — jeden element
const jan = uzytkownicy.find(u => u.imie === 'Jan');
// → { id: 1, imie: 'Jan', wiek: 25 }
.reduce() — agregacja
const suma = zamowienia.reduce((acc, z) => acc + z.kwota, 0);
[...new Set()] — unikalne wartości
const kategorie = [...new Set(produkty.map(p => p.kategoria))];
// → ['elektronika', 'ubrania', 'jedzenie']
.sort() — sortowanie
// numeryczne rosnąco
[...tablica].sort((a, b) => a.cena - b.cena)
// alfabetyczne
[...tablica].sort((a, b) => a.nazwa.localeCompare(b.nazwa))
.sort() mutuje tablicę! W React zawsze rób kopię: [...arr].sort()📌 Zadanie: Lista produktów
Mając tablicę produktów, wyrenderuj listę <ul> pokazującą nazwę i cenę każdego produktu. Pamiętaj o key.
const produkty = [
{ id: 1, nazwa: "Laptop", cena: 3500 },
{ id: 2, nazwa: "Telefon", cena: 1200 },
{ id: 3, nazwa: "Słuchawki", cena: 150 },
{ id: 4, nazwa: "Ładowarka", cena: 45 },
];
function ListaProduktow() {
const produkty = [
{ id: 1, nazwa: "Laptop", cena: 3500 },
{ id: 2, nazwa: "Telefon", cena: 1200 },
{ id: 3, nazwa: "Słuchawki", cena: 150 },
{ id: 4, nazwa: "Ładowarka", cena: 45 },
];
return (
<ul>
{produkty.map((p) => (
<li key={p.id}>{p.nazwa} — {p.cena} zł</li>
))}
</ul>
);
}
📌 Zadanie: Filtrowanie produktów
Rozszerz poprzednie zadanie — wyświetl tylko produkty, których cena jest wyższa niż 100 zł. Użyj .filter() przed .map().
function DrogieProdukty() {
const produkty = [
{ id: 1, nazwa: "Laptop", cena: 3500 },
{ id: 2, nazwa: "Telefon", cena: 1200 },
{ id: 3, nazwa: "Słuchawki", cena: 150 },
{ id: 4, nazwa: "Ładowarka", cena: 45 },
];
return (
<ul>
{produkty
.filter((p) => p.cena > 100)
.map((p) => (
<li key={p.id}>{p.nazwa} — {p.cena} zł</li>
))}
</ul>
);
}
9. Warunkowy rendering
W React decydujesz co wyświetlić w zależności od stanu. Trzy główne techniki:
Ternary operator (? :)
function Status({ zalogowany }) {
return (
<p>
{zalogowany ? 'Witaj z powrotem!' : 'Proszę się zalogować'}
</p>
);
}
Short-circuit &&
function Alert({ wiadomosc }) {
return (
<div>
{wiadomosc && <p className="alert">{wiadomosc}</p>}
</div>
);
}
// Jeśli wiadomosc = '' lub null → nic nie renderuje
0 && <Cos/> — React wyświetli 0. Lepiej: {count > 0 && ...}Wczesny return
function Karta({ dane }) {
if (!dane) return <p>Ładowanie...</p>;
return (
<div>
<h2>{dane.tytul}</h2>
<p>{dane.opis}</p>
</div>
);
}
Klasy warunkowe
// Dynamiczna klasa CSS
<div className={`karta ${aktywna ? 'aktywna' : ''}`}>
...
</div>
// Styl inline warunkowy
<p style={{ color: blad ? 'red' : 'green' }}>{tekst}</p>
Renderowanie listy z warunkiem
// Pokaż "brak wyników" gdy lista pusta
{elementy.length === 0
? <p>Brak wyników</p>
: elementy.map(el => <Karta key={el.id} {...el} />)
}
📌 Zadanie: Warunkowa wiadomość
Napisz komponent StatusZamowienia, który przyjmuje prop oplacone (boolean).
— Jeśli true → wyświetla zielony tekst „✅ Zamówienie opłacone"
— Jeśli false → wyświetla czerwony tekst „❌ Oczekuje na płatność"
Użyj operatora trójargumentowego (ternary).
function StatusZamowienia({ oplacone }) {
return (
<p style={{ color: oplacone ? "green" : "red" }}>
{oplacone
? "✅ Zamówienie opłacone"
: "❌ Oczekuje na płatność"}
</p>
);
}
// Użycie:
// <StatusZamowienia oplacone={true} />
// <StatusZamowienia oplacone={false} />
10. useEffect
useEffect obsługuje efekty uboczne: pobieranie danych, subskrypcje, timery, zmiana tytułu strony.
Składnia
import { useEffect } from 'react';
useEffect(() => {
// kod efektu
return () => {
// cleanup (opcjonalny)
};
}, [zależności]);
Tablica zależności
| Zapis | Kiedy się odpala |
|---|---|
useEffect(() => {...}) | Po każdym renderze |
useEffect(() => {...}, []) | Tylko raz — mount |
useEffect(() => {...}, [x, y]) | Gdy zmieni się x lub y |
Przykład: tytuł strony
useEffect(() => {
document.title = `Masz ${count} wiadomości`;
}, [count]);
Przykład: fetch danych
function Posty() {
const [posty, setPosty] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(res => res.json())
.then(data => {
setPosty(data.slice(0, 10));
setLoading(false);
});
}, []); // pusta tablica = raz na mount
if (loading) return <p>Ładowanie...</p>;
return (
<ul>{posty.map(p => <li key={p.id}>{p.title}</li>)}</ul>
);
}
Cleanup — sprzątanie
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(timer); // ← cleanup
}, []);
📌 Zadanie: Dynamiczny tytuł strony
Napisz komponent TytulStrony, który:
— ma licznik (useState) z przyciskiem +
— używa useEffect, żeby przy każdej zmianie licznika aktualizować document.title na „Kliknięcia: X"
Pamiętaj o tablicy zależności!
import { useState, useEffect } from "react";
function TytulStrony() {
const [licznik, setLicznik] = useState(0);
useEffect(() => {
document.title = `Kliknięcia: ${licznik}`;
}, [licznik]);
return (
<div>
<p>Licznik: {licznik}</p>
<button onClick={() => setLicznik(licznik + 1)}>+</button>
</div>
);
}
export default TytulStrony;
11. Formularze
W React formularze opierają się na controlled components — wartość pola jest w state, a zmiany obsługuje onChange.
Input tekstowy
const [imie, setImie] = useState('');
<input
type="text"
value={imie}
onChange={(e) => setImie(e.target.value)}
placeholder="Wpisz imię"
/>
Select (dropdown)
const [kolor, setKolor] = useState('niebieski');
<select value={kolor} onChange={(e) => setKolor(e.target.value)}>
<option value="niebieski">Niebieski</option>
<option value="zielony">Zielony</option>
<option value="czerwony">Czerwony</option>
</select>
Dynamiczny select
const opcje = ['A', 'B', 'C'];
<select value={wybrany} onChange={(e) => setWybrany(e.target.value)}>
{opcje.map(o => <option key={o} value={o}>{o}</option>)}
</select>
Textarea
const [opis, setOpis] = useState('');
<textarea value={opis} onChange={(e) => setOpis(e.target.value)} />
Checkbox
const [zgoda, setZgoda] = useState(false);
<label>
<input
type="checkbox"
checked={zgoda}
onChange={(e) => setZgoda(e.target.checked)}
/>
Akceptuję regulamin
</label>
Kompletny formularz
function Formularz() {
const [dane, setDane] = useState({ imie: '', email: '', wiadomosc: '' });
function handleChange(e) {
setDane({ ...dane, [e.target.name]: e.target.value });
}
function handleSubmit(e) {
e.preventDefault();
console.log(dane);
}
return (
<form onSubmit={handleSubmit}>
<input name="imie" value={dane.imie} onChange={handleChange} />
<input name="email" value={dane.email} onChange={handleChange} />
<textarea name="wiadomosc" value={dane.wiadomosc} onChange={handleChange} />
<button type="submit">Wyślij</button>
</form>
);
}
[e.target.name] to computed property name — jeden handler do wielu pól!Prosta walidacja
const [blad, setBlad] = useState('');
function handleSubmit(e) {
e.preventDefault();
if (!dane.imie.trim()) {
setBlad('Imię jest wymagane');
return;
}
setBlad('');
// wyślij dane...
}
{blad && <p style={{color:'red'}}>{blad}</p>}
📌 Zadanie: Formularz rejestracji
Zbuduj komponent Rejestracja z formularzem zawierającym:
— pole „Imię" (input text)
— pole „Email" (input email)
— przycisk „Zarejestruj się"
Po wysłaniu formularza wyświetl w konsoli obiekt z danymi: { imie, email }. Użyj kontrolowanych inputów.
import { useState } from "react";
function Rejestracja() {
const [imie, setImie] = useState("");
const [email, setEmail] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
console.log({ imie, email });
};
return (
<form onSubmit={handleSubmit}>
<label>Imię:
<input type="text" value={imie}
onChange={(e) => setImie(e.target.value)} />
</label><br />
<label>Email:
<input type="email" value={email}
onChange={(e) => setEmail(e.target.value)} />
</label><br />
<button type="submit">Zarejestruj się</button>
</form>
);
}
export default Rejestracja;
12. Struktura projektu React
Dobra organizacja plików to klucz do utrzymywalnego kodu. Oto standard Vite + React:
Typowa struktura
moj-projekt/
├── node_modules/ ← zależności (nie ruszać)
├── public/ ← pliki statyczne (favicon, obrazy)
├── src/
│ ├── assets/ ← obrazy, czcionki, dane
│ ├── components/ ← komponenty (.jsx)
│ │ ├── Header.jsx
│ │ ├── Footer.jsx
│ │ └── Card.jsx
│ ├── App.jsx ← główny komponent
│ ├── App.css ← style główne
│ ├── index.js ← punkt wejścia
│ └── index.css ← style globalne
├── index.html ← szablon HTML
├── package.json ← zależności + skrypty
└── vite.config.js ← konfiguracja Vite
Cykl życia plików
| Plik | Rola |
|---|---|
index.js | Montuje <App/> do drzewa DOM |
App.jsx | Główny komponent — tu składasz całą aplikację |
index.html | Jedyny HTML — <div id="root"> |
package.json | Lista zależności + skrypty npm run dev |
Import komponentu
// App.jsx
import Header from './components/Header';
import Card from './components/Card';
function App() {
return (
<>
<Header />
<Card title="Hello" />
</>
);
}
Import danych z pliku
// src/assets/dane.js (eksport)
export const produkty = [
{ id: 1, nazwa: 'Laptop', cena: 3500 },
{ id: 2, nazwa: 'Mysz', cena: 120 },
];
// App.jsx (import)
import { produkty } from './assets/dane';
Import CSS
import './App.css'; // ← import globalny
// albo CSS Modules:
import styles from './Card.module.css';
<div className={styles.card}>...</div>
Komendy do zapamiętania
| Komenda | Co robi |
|---|---|
npm create vite@latest | Tworzy nowy projekt |
npm install | Instaluje zależności |
npm run dev | Uruchamia serwer dev |
npm run build | Buduje produkcję |
npm run preview | Podgląd buildu |
Spread operator w React
// Kopiowanie tablicy (immutable update)
const nowa = [...stara, nowyElement];
// Kopiowanie obiektu
const nowy = { ...stary, imie: 'Nowe' };
// Usunięcie elementu
const bez = lista.filter(el => el.id !== idDoUsuniecia);
// Aktualizacja jednego elementu
const zaktualizowane = lista.map(el =>
el.id === id ? { ...el, gotowe: true } : el
);
📌 Zadanie: Import i eksport
Masz następującą strukturę plików:
src/
App.jsx
components/
Naglowek.jsx
Stopka.jsx
ui/
Przycisk.jsx
Napisz:
1. Eksport domyślny w każdym z trzech komponentów
2. Poprawne importy wszystkich trzech komponentów w App.jsx
// components/Naglowek.jsx
function Naglowek() {
return <header>Nagłówek strony</header>;
}
export default Naglowek;
// components/Stopka.jsx
function Stopka() {
return <footer>Stopka strony</footer>;
}
export default Stopka;
// components/ui/Przycisk.jsx
function Przycisk() {
return <button>Kliknij</button>;
}
export default Przycisk;
// App.jsx
import Naglowek from "./components/Naglowek";
import Stopka from "./components/Stopka";
import Przycisk from "./components/ui/Przycisk";
function App() {
return (
<>
<Naglowek />
<Przycisk />
<Stopka />
</>
);
}
export default App;
13. React Router
React to SPA (Single Page Application) — cała strona to jeden plik HTML. React Router pozwala robić wiele "podstron" bez przeładowania strony.
Po co router?
| Bez routera | Z routerem |
|---|---|
| Jedna strona, wszystko w App.js | Osobne "podstrony": /home, /about, /contact |
| Ukrywasz/pokazujesz komponenty stanem | URL zmienia się automatycznie |
| Przycisk "Wstecz" nie działa | Przycisk "Wstecz" działa normalnie |
| Nie można udostępnić linka do konkretnej strony | Każda podstrona ma swój URL |
Instalacja
npm install react-router-dom
To jedyna dodatkowa paczka — React nie ma routera wbudowanego (w odróżnieniu od Angulara).
.js. Vite obsługuje to automatycznie. W naszych projektach używamy .js — tak jak widać w edytorze po utworzeniu projektu.Konfiguracja w index.js
import { BrowserRouter } from 'react-router-dom';
import App from './App';
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Definiowanie tras w App.js
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
function App() {
return (
<div>
<h1>Moja strona</h1>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
);
}
| Element | Co robi |
|---|---|
<Routes> | Kontener — sprawdza URL i renderuje pasującą trasę |
<Route path="/" element={...} /> | Jedna trasa: gdy URL = "/", pokaż komponent Home |
path="/about" | Ścieżka URL (to co widać w pasku przeglądarki) |
element={<About />} | Komponent do wyrenderowania |
Nawigacja — <Link> zamiast <a>
import { Link } from 'react-router-dom';
function Navbar() {
return (
<nav>
<Link to="/">Strona główna</Link>
<Link to="/about">O nas</Link>
<Link to="/contact">Kontakt</Link>
</nav>
);
}
Strona 404 — gdy nic nie pasuje
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<h2>404 — Nie znaleziono strony</h2>} />
</Routes>
path="*" — łapie wszystkie adresy które nie pasują do żadnej trasy.
Aktywny link — NavLink
import { NavLink } from 'react-router-dom';
<NavLink to="/" className={({ isActive }) => isActive ? "active" : ""}>
Strona główna
</NavLink>
NavLink działa jak Link, ale wie czy jest aktywny. Dzięki temu możesz podświetlić aktualny link w menu.
useEffect + Router — tytuł strony
Częsty pattern: zmiana document.title po wejściu na podstronę. Do tego potrzebujemy useEffect + hooka useLocation:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function About() {
const location = useLocation();
useEffect(() => {
document.title = "O nas — MiniStrona";
}, []); // [] = tylko przy montowaniu
return <h2>O nas</h2>;
}
useEffect(() => {...}, []) odpala się raz — gdy komponent się pojawia. Idealny moment na zmianę tytułu, pobranie danych z API, itp.useEffect + fetch — dane z API
Na podstronie możesz pobrać dane z zewnętrznego API przy wejściu:
import { useState, useEffect } from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <p>Ładowanie...</p>;
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
| Element | Co robi |
|---|---|
useEffect(() => {...}, []) | Odpala fetch raz — przy wejściu na stronę |
useState(true) dla loading | Pokazuje "Ładowanie..." dopóki dane nie przyjdą |
.then(res => res.json()) | Zamienia odpowiedź HTTP na obiekt JS |
setLoading(false) | Ukrywa "Ładowanie..." i pokazuje listę |
Podsumowanie
| Import | Co robi |
|---|---|
BrowserRouter | Opakowuje aplikację — włącza routing |
Routes | Kontener na trasy |
Route | Jedna trasa: path + element |
Link | Nawigacja bez przeładowania strony |
NavLink | Link z info czy jest aktywny |
Zadanie: Opisz słowami
Masz aplikację z 3 podstronami: Strona główna (/), Produkty (/products), Kontakt (/contact).
Wymień z pamięci:
1. Co trzeba zainstalować?
2. Co trzeba zmienić w index.js?
3. Jak wygląda App.js z trasami?
4. Jak zrobić menu nawigacyjne?
1. npm install react-router-dom
2. Opakowac <App /> w <BrowserRouter> w index.js
3. W App.js:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/contact" element={<Contact />} />
</Routes>
4. Uzyc <Link to="/products">Produkty</Link> zamiast <a>
14. Biblioteki w React + CRUD (axios)
W tej lekcji nauczysz się instalować biblioteki z npm i używać ich w React, oraz wykonać kompletny CRUD — czyli 4 operacje na danych z serwera (Create, Read, Update, Delete).
Co to jest biblioteka?
Biblioteka to gotowy kod który ktoś napisał i udostępnił przez npm (Node Package Manager). Zamiast pisać wszystko od zera — instalujesz paczkę i jej używasz.
| Biblioteka | Co robi |
|---|---|
react | Sam React (już mamy) |
react-router-dom | Routing (poprzednia lekcja) |
bootstrap | Gotowe style CSS |
axios | Wysyłanie zapytań HTTP do API ← dziś |
moment / dayjs | Praca z datami |
chart.js | Wykresy |
Instalacja biblioteki
npm install nazwa-biblioteki
Po instalacji biblioteka pojawia się w package.json w sekcji dependencies. Importujemy ją w plikach .js:
import axios from 'axios';
Co to jest API?
API (Application Programming Interface) to "punkt kontaktu" z serwerem. Wysyłamy zapytanie HTTP i dostajemy dane w formacie JSON.
Twoja apka ──── GET /posts ────► Serwer
◄──── [JSON dane] ────
Mock API — JSONPlaceholder
Do nauki używamy JSONPlaceholder — darmowe fałszywe API, które udaje że ma dane. Idealne do ćwiczeń:
https://jsonplaceholder.typicode.com/posts
Można robić wszystkie operacje CRUD — serwer odpowiada tak jakby zapisał, ale w rzeczywistości nic nie zmienia. Idealny do nauki.
CRUD — 4 operacje
CRUD to skrót od 4 podstawowych operacji na danych:
| Litera | Operacja | HTTP Method | Co robi |
|---|---|---|---|
| C | Create | POST | Tworzy nowy zasób |
| R | Read | GET | Czyta dane (lista lub jeden) |
| U | Update | PUT / PATCH | Aktualizuje zasób |
| D | Delete | DELETE | Usuwa zasób |
axios vs fetch — czemu axios?
| Cecha | fetch (wbudowany) | axios (biblioteka) |
|---|---|---|
| Krócej | 2 kroki: fetch().then(r=>r.json()) | 1 krok: axios.get() — od razu daje dane |
| JSON automatycznie | Ręcznie res.json() | Automatycznie |
| Obsługa błędów | 404 NIE rzuca błędu (musisz sprawdzić) | 404/500 automatycznie rzuca błąd |
| POST/PUT | Trzeba ręcznie ustawić headers + JSON.stringify | Wystarczy obiekt |
Operacja READ — GET (axios.get)
import axios from 'axios';
import { useState, useEffect } from 'react';
function ListaPostow() {
const [posty, setPosty] = useState([]);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/posts')
.then(response => setPosty(response.data));
}, []);
return (
<ul>
{posty.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
response.data — axios od razu rozpakowuje JSON. Nie musisz robić .json() jak w fetch.Operacja CREATE — POST (axios.post)
function dodajPost() {
const nowy = {
title: "Mój nowy post",
body: "Treść posta",
userId: 1
};
axios.post('https://jsonplaceholder.typicode.com/posts', nowy)
.then(response => {
console.log("Utworzono:", response.data);
// response.data ma wszystko + nowe id z serwera
setPosty([...posty, response.data]);
});
}
Drugi argument (nowy) to dane do wysłania. Serwer odpowiada utworzonym zasobem (z dodanym id).
Operacja UPDATE — PUT (axios.put)
function edytujPost(id) {
const zmieniony = {
title: "Zmieniony tytuł",
body: "Nowa treść",
userId: 1
};
axios.put(`https://jsonplaceholder.typicode.com/posts/${id}`, zmieniony)
.then(response => {
// Zaktualizuj listę lokalnie
setPosty(posty.map(p => p.id === id ? response.data : p));
});
}
/posts/5 — to ten post zostanie nadpisany.Operacja DELETE — DELETE (axios.delete)
function usunPost(id) {
axios.delete(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then(() => {
// Usuń lokalnie z listy
setPosty(posty.filter(p => p.id !== id));
});
}
DELETE nie wysyła danych — tylko URL z ID. Serwer odpowiada pustą odpowiedzią (status 200).
Loading state — pokaż "Ładowanie..."
Zapytania do API trwają czas. Powinniśmy pokazać użytkownikowi że coś się dzieje:
const [posty, setPosty] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/posts')
.then(response => {
setPosty(response.data);
setLoading(false); // koniec ładowania
});
}, []);
if (loading) return <p>Ładowanie...</p>;
return <ul>{posty.map(...)}</ul>;
Obsługa błędów — try / catch (async/await)
Jeśli serwer padnie albo nie ma internetu — zapytanie wywali błąd. Trzeba go obsłużyć:
// Wersja z .then().catch()
axios.get(url)
.then(response => setPosty(response.data))
.catch(error => alert("Błąd: " + error.message));
// Wersja z async/await + try/catch
async function pobierz() {
try {
const response = await axios.get(url);
setPosty(response.data);
} catch (error) {
alert("Błąd: " + error.message);
}
}
await czeka na odpowiedź zanim przejdzie dalej.Pełen wzorzec CRUD
// READ
axios.get(URL).then(r => setPosty(r.data));
// CREATE
axios.post(URL, dane).then(r => setPosty([...posty, r.data]));
// UPDATE
axios.put(`${URL}/${id}`, dane).then(r =>
setPosty(posty.map(p => p.id === id ? r.data : p))
);
// DELETE
axios.delete(`${URL}/${id}`).then(() =>
setPosty(posty.filter(p => p.id !== id))
);
Podsumowanie
| Pojęcie | Co warto pamiętać |
|---|---|
npm install | Pobiera bibliotekę i zapisuje w package.json |
import x from 'lib' | Włącza bibliotekę w pliku |
| API | "Punkt kontaktu" z serwerem przez HTTP |
| JSON | Format danych (wygląda jak obiekt JS) |
| CRUD | 4 operacje: Create / Read / Update / Delete |
axios.get/post/put/delete | 4 metody — po jednej na każdą operację |
response.data | Tu są dane z serwera (axios automatycznie parsuje JSON) |
| Loading state | Pokaż użytkownikowi że trwa ładowanie |
| try / catch | Obsługa błędów (brak internetu, 404, 500) |
Zadanie: Opisz co zrobi ten kod
function App() {
const [users, setUsers] = useState([]);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users')
.then(r => setUsers(r.data));
}, []);
function dodajUsera() {
axios.post('https://jsonplaceholder.typicode.com/users', {
name: "Jan Kowalski"
}).then(r => setUsers([...users, r.data]));
}
return (
<div>
<button onClick={dodajUsera}>Dodaj</button>
{users.map(u => <p key={u.id}>{u.name}</p>)}
</div>
);
}
Co robi:
- Przy starcie (
useEffectz []) pobiera listę użytkowników z API i zapisuje w stanie - Renderuje przycisk "Dodaj" oraz listę imion użytkowników
- Po kliknięciu "Dodaj" wysyła POST do API z nowym userem ("Jan Kowalski")
- Gdy serwer odpowie — dodaje nowego usera do listy (z otrzymanym
id)
Operacje CRUD: READ (pobieranie listy) + CREATE (dodawanie). Brakuje: UPDATE i DELETE.
Rozgrzewka — Mini-strona (Router)
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
1. Czym jest SPA?
Single Page Application — jedna strona HTML, zawartość zmienia się dynamicznie bez przeładowania.
2. Dlaczego używamy <Link> zamiast <a>?
<a> przeładowuje całą stronę. <Link> zmienia URL i komponent bez przeładowania — szybciej, bez migania.
3. Jak złapać adres który nie pasuje do żadnej trasy?
Dodajemy <Route path="*" element={<NotFound />} /> — gwiazdka łapie wszystko.
4. Jaka jest różnica między Link a NavLink?
NavLink wie czy jest aktywny (isActive) — można podświetlić aktualny link w menu. Link tego nie umie.
5. Kiedy odpala się useEffect z pustą tablicą zależności []?
Raz — przy montowaniu komponentu (gdy pojawia się na ekranie). Idealny do: zmiany tytułu strony, pobrania danych z API.
Mini-strona — Instrukcja
Treść zadania
Wykonaj mini-stronę z nawigacją używając React Router:
- 3 podstrony: Strona główna (/), O nas (/about), Kontakt (/contact)
- Menu nawigacyjne widoczne na każdej stronie
- Aktywny link podświetlony innym kolorem (NavLink)
- Strona 404 dla nieistniejących adresów
- Na stronie Kontakt — formularz z polami: imię, email, wiadomość
- useEffect — zmiana tytułu karty przeglądarki na każdej podstronie
- Na stronie "O nas" — pobranie danych z API (fetch + useEffect)
- Bootstrap do stylowania
Mini-strona — Budowa
npm create vite@latest mini-strona -- --template react
cd mini-strona
npm install
npm install react-router-dom bootstrap
W index.js dodaj import Bootstrap:
import 'bootstrap/dist/css/bootstrap.min.css';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Utwórz folder src/pages/ i 4 pliki:
pages/Home.js:
import { useEffect } from 'react';
function Home() {
useEffect(() => {
document.title = "Home — MiniStrona";
}, []);
return (
<div>
<h2>Strona główna</h2>
<p>Witaj na naszej stronie! Używamy React Router.</p>
</div>
);
}
export default Home;
pages/About.js — z useEffect (pobiera użytkowników z API):
import { useState, useEffect } from 'react';
function About() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
document.title = "O nas — MiniStrona";
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return (
<div>
<h2>O nas</h2>
<p>Jesteśmy zespołem uczącym się Reacta!</p>
<h3>Nasz zespół</h3>
{loading ? <p>Ładowanie...</p> : (
<ul className="list-group">
{users.slice(0, 5).map(u => (
<li key={u.id} className="list-group-item">
<strong>{u.name}</strong> — {u.email}
</li>
))}
</ul>
)}
</div>
);
}
export default About;
pages/Contact.js: (z formularzem — krok 5)
pages/NotFound.js:
function NotFound() {
return (
<div className="text-center mt-5">
<h2>404</h2>
<p>Nie znaleziono strony!</p>
</div>
);
}
export default NotFound;
Utwórz src/components/Navbar.js:
import { NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav className="navbar navbar-expand navbar-dark bg-dark mb-4">
<div className="container">
<span className="navbar-brand">MiniStrona</span>
<div className="navbar-nav">
<NavLink to="/" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Home</NavLink>
<NavLink to="/about" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>O nas</NavLink>
<NavLink to="/contact" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Kontakt</NavLink>
</div>
</div>
</nav>
);
}
export default Navbar;
active podświetla aktywny link. NavLink dostarcza info czy dany link jest aktualnie aktywny.
Utwórz src/pages/Contact.js:
import { useState, useEffect } from 'react';
function Contact() {
const [dane, setDane] = useState({ imie: '', email: '', wiadomosc: '' });
const [wyslano, setWyslano] = useState(false);
useEffect(() => {
document.title = "Kontakt — MiniStrona";
}, []);
function handleChange(e) {
setDane({ ...dane, [e.target.name]: e.target.value });
}
function handleSubmit(e) {
e.preventDefault();
setWyslano(true);
}
if (wyslano) {
return (
<div className="alert alert-success">
Dziękujemy, {dane.imie}! Wiadomość została wysłana.
</div>
);
}
return (
<div>
<h2>Kontakt</h2>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Imię</label>
<input type="text" className="form-control" name="imie"
value={dane.imie} onChange={handleChange} required />
</div>
<div className="mb-3">
<label className="form-label">Email</label>
<input type="email" className="form-control" name="email"
value={dane.email} onChange={handleChange} required />
</div>
<div className="mb-3">
<label className="form-label">Wiadomość</label>
<textarea className="form-control" name="wiadomosc" rows="4"
value={dane.wiadomosc} onChange={handleChange} required />
</div>
<button type="submit" className="btn btn-primary">Wyślij</button>
</form>
</div>
);
}
export default Contact;
[e.target.name].
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
function App() {
return (
<div>
<Navbar />
<div className="container">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export default App;
npm run dev
Sprawdź:
| Test | Oczekiwany wynik |
|---|---|
| Otwórz http://localhost:5173 | Strona główna + menu |
| Kliknij "O nas" | Lista użytkowników z API, tytuł karty = "O nas — MiniStrona" |
| Kliknij "Kontakt" | Formularz, URL = /contact |
| Wypełnij formularz i wyślij | Zielony alert "Dziękujemy" |
| Wpisz w URL /xyz | Strona 404 |
| Przycisk "Wstecz" w przeglądarce | Wraca do poprzedniej strony |
| Aktywny link w menu | Podświetlony na biało |
Mini-strona — Gotowy kod
Struktura plików
src/
├── components/
│ └── Navbar.js
├── pages/
│ ├── Home.js
│ ├── About.js
│ ├── Contact.js
│ └── NotFound.js
├── App.js
└── index.js
index.js
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
App.js
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
function App() {
return (
<div>
<Navbar />
<div className="container">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export default App;
components/Navbar.js
import { NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav className="navbar navbar-expand navbar-dark bg-dark mb-4">
<div className="container">
<span className="navbar-brand">MiniStrona</span>
<div className="navbar-nav">
<NavLink to="/" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Home</NavLink>
<NavLink to="/about" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>O nas</NavLink>
<NavLink to="/contact" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Kontakt</NavLink>
</div>
</div>
</nav>
);
}
export default Navbar;
pages/Home.js
import { useEffect } from 'react';
function Home() {
useEffect(() => {
document.title = "Home — MiniStrona";
}, []);
return (
<div>
<h2>Strona główna</h2>
<p>Witaj na naszej stronie! Używamy React Router.</p>
</div>
);
}
export default Home;
pages/About.js
import { useState, useEffect } from 'react';
function About() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
document.title = "O nas — MiniStrona";
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return (
<div>
<h2>O nas</h2>
<p>Jesteśmy zespołem uczącym się Reacta!</p>
<h3>Nasz zespół</h3>
{loading ? <p>Ładowanie...</p> : (
<ul className="list-group">
{users.slice(0, 5).map(u => (
<li key={u.id} className="list-group-item">
<strong>{u.name}</strong> — {u.email}
</li>
))}
</ul>
)}
</div>
);
}
export default About;
pages/Contact.js
import { useState, useEffect } from 'react';
function Contact() {
const [dane, setDane] = useState({ imie: '', email: '', wiadomosc: '' });
const [wyslano, setWyslano] = useState(false);
useEffect(() => {
document.title = "Kontakt — MiniStrona";
}, []);
function handleChange(e) {
setDane({ ...dane, [e.target.name]: e.target.value });
}
function handleSubmit(e) {
e.preventDefault();
setWyslano(true);
}
if (wyslano) {
return (
<div className="alert alert-success">
Dziękujemy, {dane.imie}! Wiadomość została wysłana.
</div>
);
}
return (
<div>
<h2>Kontakt</h2>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Imię</label>
<input type="text" className="form-control" name="imie"
value={dane.imie} onChange={handleChange} required />
</div>
<div className="mb-3">
<label className="form-label">Email</label>
<input type="email" className="form-control" name="email"
value={dane.email} onChange={handleChange} required />
</div>
<div className="mb-3">
<label className="form-label">Wiadomość</label>
<textarea className="form-control" name="wiadomosc" rows="4"
value={dane.wiadomosc} onChange={handleChange} required />
</div>
<button type="submit" className="btn btn-primary">Wyślij</button>
</form>
</div>
);
}
export default Contact;
pages/NotFound.js
function NotFound() {
return (
<div className="text-center mt-5">
<h2>404</h2>
<p>Nie znaleziono strony!</p>
</div>
);
}
export default NotFound;
Sprawdzian — Mini-strona (Router)
5 pytań · 5 minut · minimum 60% żeby zdać
<Link> zamiast <a>?path="*" w <Route>?NavLink różni się od Link?<Navbar /> żeby był widoczny na każdej podstronie?Rozgrzewka — Hodowla świnek morskich
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
1. Czemu React nie może gadać bezpośrednio z MySQL?
Bo to dwa różne protokoły. Przeglądarka mówi tylko HTTP, MySQL używa swojego protokołu binarnego. Plus dane logowania do bazy nie mogą trafić do przeglądarki (każdy by je zobaczył w F12). Potrzebny pośrednik = backend (PHP/Node/Python).
2. Co oznacza skrót CRUD?
Create (tworzenie), Read (czytanie), Update (aktualizacja), Delete (usuwanie). 4 podstawowe operacje na danych — w HTTP to POST / GET / PUT / DELETE.
3. Co to PDO i czemu nie używamy mysql_query?
PDO (PHP Data Objects) to nowoczesny sposób łączenia z bazą — bezpieczny przed SQL injection dzięki prepare() + execute([params]). mysql_query jest stary, niebezpieczny i usunięty z PHP 7+.
4. Co to CORS i czemu PHP musi go ustawić?
Przeglądarka domyślnie blokuje zapytania z jednej domeny (np. localhost:3000) do innej (localhost:80). Backend musi powiedzieć: "spokojnie, pozwalam" przez header Access-Control-Allow-Origin: *.
5. Co to useNavigate?
Hook z react-router-dom — pozwala programowo zmienić stronę. Np. po dodaniu świnki: navigate('/swinki') wraca na listę bez przeładowania.
6. Co robi useParams?
Hook z react-router-dom — czyta parametry z URL. Dla trasy /swinki/:id/edytuj i URL /swinki/5/edytuj daje { id: "5" } (uwaga — jako string!).
Hodowla świnek morskich — Instrukcja
Treść zadania
Wykonaj aplikację React do zarządzania hodowlą świnek morskich. Aplikacja używa:
- React Router + Navbar — 5 podstron z nawigacją
- axios — komunikacja z backendem REST API
- PHP + MySQL (XAMPP) — realny backend i baza, nie mock
- Dwie tabele MySQL:
rasyiswinki(relacjarasy_id → rasy.id) - CRUD na świnkach: lista / dodaj / edytuj / usuń
- Read-only lista ras (z drugiej tabeli)
- Bootstrap do stylowania
Schemat bazy danych (z hodowla.sql)
Tabela rasy | Tabela swinki |
|---|---|
id (int, PK)rasa (string)
|
id (int, PK)rasy_id (int, FK → rasy.id)data_ur (date)miot (string)opis (string)imie (string)cena (int)
|
Strony aplikacji (Router)
| URL | Komponent | Co robi |
|---|---|---|
/ | Home | Powitanie + statystyki (ile świnek, ile ras, średnia cena) |
/swinki | ListaSwinek | READ — tabela świnek z nazwą rasy + Edytuj/Usuń |
/swinki/dodaj | DodajSwinke | CREATE — formularz nowej świnki |
/swinki/:id/edytuj | EdytujSwinke | UPDATE — formularz z wypełnionymi danymi |
/rasy | Rasy | READ — lista ras (read-only) |
* | NotFound | 404 |
Czego się nauczysz
| Pojęcie | Co robi |
|---|---|
| Architektura 3-warstwowa | Frontend (React) → Backend (PHP) → Baza (MySQL) |
| PHP + PDO | Bezpieczne łączenie się z MySQL i obsługa metod HTTP |
| CORS | Header pozwalający Reactowi (port 3000) gadać z Apache (port 80) |
Query string ?id= | Przekazanie ID do PHP bez konfiguracji serwera — działa wszędzie |
| axios + Router | CRUD rozłożony na osobne strony Reacta |
useNavigate | Programowa zmiana strony (po zapisie wraca na listę) |
useParams | Czytanie ID z URL (np. /swinki/5/edytuj) |
| Foreign key + JOIN | "JOIN" w React: rasy.find(r => r.id === swinka.rasy_id) |
Hodowla świnek morskich — Budowa
npx create-react-app hodowla
cd hodowla
npm install axios bootstrap react-router-dom
W src/index.js dodaj imports + opakuj App w BrowserRouter:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Wymagania: XAMPP uruchomiony (Apache + MySQL ze startu) i baza hodowla z tabelami rasy + swinki (już masz w phpMyAdmin).
W folderze Apache (htdocs) utwórz folder hodowla-api i 3 pliki:
htdocs/hodowla-api/
├── db.php ← połączenie z MySQL (PDO)
├── swinki.php ← CRUD na tabeli swinki
└── rasy.php ← READ na tabeli rasy
C:\xampp\htdocs\, macOS XAMPP /Applications/XAMPP/xamppfiles/htdocs/, MAMP /Applications/MAMP/htdocs/.
db.php — połączenie z bazą:
<?php
$host = '127.0.0.1';
$db = 'hodowla';
$user = 'root';
$pass = ''; // domyślnie XAMPP nie ma hasła
$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";
try {
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => $e->getMessage()]);
exit;
}
swinki.php — pełen CRUD (4 metody HTTP):
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
require 'db.php';
$method = $_SERVER['REQUEST_METHOD'];
$id = $_GET['id'] ?? null;
switch ($method) {
case 'GET':
if ($id) {
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
} else {
$stmt = $pdo->query('SELECT * FROM swinki');
echo json_encode($stmt->fetchAll());
}
break;
case 'POST':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare('INSERT INTO swinki (rasy_id, data_ur, miot, opis, imie, cena) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->execute([$data['rasy_id'], $data['data_ur'], $data['miot'], $data['opis'], $data['imie'], $data['cena']]);
$newId = $pdo->lastInsertId();
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$newId]);
echo json_encode($stmt->fetch());
break;
case 'PUT':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare('UPDATE swinki SET rasy_id=?, data_ur=?, miot=?, opis=?, imie=?, cena=? WHERE id=?');
$stmt->execute([$data['rasy_id'], $data['data_ur'], $data['miot'], $data['opis'], $data['imie'], $data['cena'], $id]);
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
break;
case 'DELETE':
$stmt = $pdo->prepare('DELETE FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode(['ok' => true]);
break;
}
rasy.php — read-only:
<?php
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json; charset=utf-8');
require 'db.php';
$stmt = $pdo->query('SELECT * FROM rasy ORDER BY rasa');
echo json_encode($stmt->fetchAll());
swinki.php oraz swinki.php?id=5. To działa identycznie na każdym serwerze (XAMPP/MAMP, Windows/macOS/Linux) bez konfiguracji Apache — query string jest uniwersalny, a URL rewrite jest kruchy i zależny od ustawień hostingu.
Test backendu — otwórz w przeglądarce http://localhost/hodowla-api/swinki.php — powinieneś zobaczyć JSON z listą świnek z bazy MySQL.
Access-Control-Allow-Origin: * mówi przeglądarce: "React z portu 3000 może się ze mną łączyć". Bez tego React dostałby błąd CORS w F12 Console.
Utwórz src/components/Navbar.js:
import { NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav className="navbar navbar-expand navbar-dark bg-dark mb-4">
<div className="container">
<span className="navbar-brand">🐹 Hodowla</span>
<div className="navbar-nav">
<NavLink to="/" end className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Home</NavLink>
<NavLink to="/swinki" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Świnki</NavLink>
<NavLink to="/rasy" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Rasy</NavLink>
<NavLink to="/swinki/dodaj" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>+ Dodaj</NavLink>
</div>
</div>
</nav>
);
}
export default Navbar;
end przy NavLink "Home" — bez tego "Home" byłby zawsze aktywny (bo / matchuje wszystkie URL-e).
Wyczyść src/App.js i wpisz:
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import ListaSwinek from './pages/ListaSwinek';
import DodajSwinke from './pages/DodajSwinke';
import EdytujSwinke from './pages/EdytujSwinke';
import Rasy from './pages/Rasy';
import NotFound from './pages/NotFound';
const API_URL = 'http://localhost/hodowla-api';
function App() {
return (
<div>
<Navbar />
<div className="container">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/swinki" element={<ListaSwinek />} />
<Route path="/swinki/dodaj" element={<DodajSwinke />} />
<Route path="/swinki/:id/edytuj" element={<EdytujSwinke />} />
<Route path="/rasy" element={<Rasy />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export { API_URL };
export default App;
API_URL wskazuje na backend PHP w XAMPP. Cała reszta Reacta nie wie skąd są dane — to magia separacji warstw.
Utwórz src/pages/Home.js:
import { useState, useEffect } from 'react';
import axios from 'axios';
import { API_URL } from '../App';
function Home() {
const [swinki, setSwinki] = useState([]);
const [rasy, setRasy] = useState([]);
useEffect(() => {
axios.get(`${API_URL}/swinki.php`).then(r => setSwinki(r.data));
axios.get(`${API_URL}/rasy.php`).then(r => setRasy(r.data));
}, []);
const sredniaCena = swinki.length === 0 ? 0 :
swinki.reduce((suma, s) => suma + s.cena, 0) / swinki.length;
return (
<div>
<h1>🐹 Hodowla świnek morskich</h1>
<p className="lead">Witaj w panelu zarządzania hodowlą.</p>
<div className="row">
<div className="col-md-4">
<div className="card text-center p-3">
<h2>{swinki.length}</h2>
<p className="text-muted">świnek w hodowli</p>
</div>
</div>
<div className="col-md-4">
<div className="card text-center p-3">
<h2>{rasy.length}</h2>
<p className="text-muted">dostępnych ras</p>
</div>
</div>
<div className="col-md-4">
<div className="card text-center p-3">
<h2>{sredniaCena.toFixed(0)} zł</h2>
<p className="text-muted">średnia cena</p>
</div>
</div>
</div>
</div>
);
}
export default Home;
.reduce() sumuje wszystkie ceny. Dzielenie przez liczbę świnek = średnia. .toFixed(0) = bez miejsc po przecinku.
Utwórz src/pages/ListaSwinek.js:
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import { API_URL } from '../App';
function ListaSwinek() {
const [swinki, setSwinki] = useState([]);
const [rasy, setRasy] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
axios.get(`${API_URL}/swinki.php`),
axios.get(`${API_URL}/rasy.php`)
]).then(([s, r]) => {
setSwinki(s.data);
setRasy(r.data);
setLoading(false);
});
}, []);
// "JOIN" przez find — szukamy rasy po rasy_id
function nazwaRasy(rasy_id) {
const r = rasy.find(x => x.id === rasy_id);
return r ? r.rasa : '?';
}
function usunSwinke(id) {
if (!window.confirm('Na pewno usunąć?')) return;
axios.delete(`${API_URL}/swinki.php?id=${id}`)
.then(() => setSwinki(swinki.filter(s => s.id !== id)));
}
if (loading) return <p>Ładowanie...</p>;
return (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<h2>Lista świnek</h2>
<Link to="/swinki/dodaj" className="btn btn-success">+ Dodaj świnkę</Link>
</div>
<table className="table table-striped">
<thead>
<tr>
<th>Imię</th><th>Rasa</th><th>Data ur.</th><th>Miot</th><th>Cena</th><th></th>
</tr>
</thead>
<tbody>
{swinki.map(s => (
<tr key={s.id}>
<td><strong>{s.imie}</strong></td>
<td>{nazwaRasy(s.rasy_id)}</td>
<td>{s.data_ur}</td>
<td>{s.miot}</td>
<td>{s.cena} zł</td>
<td>
<Link to={`/swinki/${s.id}/edytuj`} className="btn btn-sm btn-warning me-2">Edytuj</Link>
<button className="btn btn-sm btn-danger" onClick={() => usunSwinke(s.id)}>Usuń</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default ListaSwinek;
nazwaRasy(rasy_id) szuka rasy o danym id przez .find(). To samo co SQL JOIN, tylko po stronie frontu.
Utwórz src/pages/DodajSwinke.js. Pełny kod jest w zakładce „Gotowy kod" (sekcja FRONTEND — src/pages/DodajSwinke.js) — skopiuj go stamtąd.
Co ten komponent robi (zrozum zanim skopiujesz):
useStatetrzyma obiektdanez 6 polami formularza (imie, rasy_id, data_ur, miot, opis, cena)useEffectpobiera listę ras (rasy.php) do wypełnienia<select>handleChange— jeden handler na wszystkie pola (poname); dlarasy_idicenarobiNumber(value)handleSubmit—axios.postdoswinki.php, potemnavigate('/swinki')
<input> i <select> zawsze dają string. Dla rasy_id i cena trzeba zamienić na liczbę, inaczej „JOIN" w liście się rozjedzie (4 ≠ "4").
navigate('/swinki') — programowo wracamy na listę. Tak samo działa „Anuluj".
Utwórz src/pages/EdytujSwinke.js. Pełny kod jest w zakładce „Gotowy kod" (sekcja FRONTEND — src/pages/EdytujSwinke.js) — skopiuj go stamtąd.
EdytujSwinke to prawie ten sam formularz co DodajSwinke — różnice:
| DodajSwinke (CREATE) | EdytujSwinke (UPDATE) |
|---|---|
| pusty formularz na start | najpierw GET swinki.php?id=X wypełnia formularz |
axios.post | axios.put z tym samym id |
| — | useParams() czyta :id z URL |
| — | if (!dane) return Ładowanie dopóki nie przyjdą dane |
useParams()— czyta:idz URL (zawsze string)useEffect([id])— pobiera dane świnki PO ID przy wejściu na stronęuseNavigate()— po PUT wraca na listę
/swinki/5/edytuj. EdytujSwinke czyta id=5, pobiera GET swinki.php?id=5, wypełnia formularz. Po zapisie PUT swinki.php?id=5.
Utwórz src/pages/Rasy.js (lista ras read-only):
import { useState, useEffect } from 'react';
import axios from 'axios';
import { API_URL } from '../App';
function Rasy() {
const [rasy, setRasy] = useState([]);
useEffect(() => {
axios.get(`${API_URL}/rasy.php`).then(r => setRasy(r.data));
}, []);
return (
<div>
<h2>Dostępne rasy świnek morskich</h2>
<ul className="list-group">
{rasy.map(r => (
<li key={r.id} className="list-group-item">
<strong>#{r.id}</strong> — {r.rasa}
</li>
))}
</ul>
</div>
);
}
export default Rasy;
Utwórz src/pages/NotFound.js:
import { Link } from 'react-router-dom';
function NotFound() {
return (
<div className="text-center mt-5">
<h1>404</h1>
<p>Nie znaleziono strony!</p>
<Link to="/" className="btn btn-primary">Wróć do Home</Link>
</div>
);
}
export default NotFound;
Wymagane uruchomione:
- XAMPP — Apache + MySQL (zielone)
- React:
npm start(port 3000)
Otwórz dwa okna obok siebie:
- Lewe: React na
http://localhost:3000/swinki - Prawe: phpMyAdmin →
hodowla.swinki
F12 → Network i sprawdź każdy scenariusz:
| Akcja w React | Network | phpMyAdmin (po refresh) |
|---|---|---|
| Otwórz / | GET /swinki + GET /rasy | — |
| Kliknij "Świnki" | GET /swinki + GET /rasy | Tabela tych samych świnek |
| Dodaj świnkę "Pinki" | POST /swinki (200) | Nowy wiersz w tabeli swinki |
| Edytuj cenę świnki | PUT /swinki/{id} (200) | Zaktualizowana wartość w kolumnie cena |
| Usuń świnkę | DELETE /swinki/{id} (200) | Wiersz znika z tabeli |
| Odśwież stronę React | — | Dane zostają — to MySQL! |
| Dodaj wiersz w phpMyAdmin | — | Po refresh Reacta nowa świnka pojawia się w UI |
| Wpisz w URL /xyz | — | Strona 404 |
Hodowla świnek morskich — Gotowy kod
Struktura projektu (2 części)
FRONTEND (React):
hodowla/
├── package.json
└── src/
├── index.js ← BrowserRouter
├── App.js ← Routes + Navbar
├── components/
│ └── Navbar.js
└── pages/
├── Home.js
├── ListaSwinek.js
├── DodajSwinke.js
├── EdytujSwinke.js
├── Rasy.js
└── NotFound.js
BACKEND (PHP w htdocs):
htdocs/hodowla-api/
├── db.php ← połączenie PDO z MySQL
├── swinki.php ← CRUD na tabeli swinki
└── rasy.php ← READ na tabeli rasy
BACKEND — db.php
<?php
$host = '127.0.0.1';
$db = 'hodowla';
$user = 'root';
$pass = '';
$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";
try {
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => $e->getMessage()]);
exit;
}
BACKEND — swinki.php
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
require 'db.php';
$method = $_SERVER['REQUEST_METHOD'];
$id = $_GET['id'] ?? null;
switch ($method) {
case 'GET':
if ($id) {
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
} else {
$stmt = $pdo->query('SELECT * FROM swinki');
echo json_encode($stmt->fetchAll());
}
break;
case 'POST':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare('INSERT INTO swinki (rasy_id, data_ur, miot, opis, imie, cena) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->execute([$data['rasy_id'], $data['data_ur'], $data['miot'], $data['opis'], $data['imie'], $data['cena']]);
$newId = $pdo->lastInsertId();
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$newId]);
echo json_encode($stmt->fetch());
break;
case 'PUT':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare('UPDATE swinki SET rasy_id=?, data_ur=?, miot=?, opis=?, imie=?, cena=? WHERE id=?');
$stmt->execute([$data['rasy_id'], $data['data_ur'], $data['miot'], $data['opis'], $data['imie'], $data['cena'], $id]);
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
break;
case 'DELETE':
$stmt = $pdo->prepare('DELETE FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode(['ok' => true]);
break;
}
BACKEND — rasy.php
<?php
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json; charset=utf-8');
require 'db.php';
$stmt = $pdo->query('SELECT * FROM rasy ORDER BY rasa');
echo json_encode($stmt->fetchAll());
FRONTEND — src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
FRONTEND — src/App.js
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import ListaSwinek from './pages/ListaSwinek';
import DodajSwinke from './pages/DodajSwinke';
import EdytujSwinke from './pages/EdytujSwinke';
import Rasy from './pages/Rasy';
import NotFound from './pages/NotFound';
const API_URL = 'http://localhost/hodowla-api';
function App() {
return (
<div>
<Navbar />
<div className="container">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/swinki" element={<ListaSwinek />} />
<Route path="/swinki/dodaj" element={<DodajSwinke />} />
<Route path="/swinki/:id/edytuj" element={<EdytujSwinke />} />
<Route path="/rasy" element={<Rasy />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export { API_URL };
export default App;
FRONTEND — src/components/Navbar.js
import { NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav className="navbar navbar-expand navbar-dark bg-dark mb-4">
<div className="container">
<span className="navbar-brand">🐹 Hodowla</span>
<div className="navbar-nav">
<NavLink to="/" end className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")}>Home</NavLink>
<NavLink to="/swinki" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")}>Świnki</NavLink>
<NavLink to="/rasy" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")}>Rasy</NavLink>
<NavLink to="/swinki/dodaj" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")}>+ Dodaj</NavLink>
</div>
</div>
</nav>
);
}
export default Navbar;
FRONTEND — src/pages/Home.js
import { useState, useEffect } from 'react';
import axios from 'axios';
import { API_URL } from '../App';
function Home() {
const [swinki, setSwinki] = useState([]);
const [rasy, setRasy] = useState([]);
useEffect(() => {
axios.get(`${API_URL}/swinki.php`).then(r => setSwinki(r.data));
axios.get(`${API_URL}/rasy.php`).then(r => setRasy(r.data));
}, []);
const sredniaCena = swinki.length === 0 ? 0 :
swinki.reduce((s, x) => s + x.cena, 0) / swinki.length;
return (
<div>
<h1>🐹 Hodowla świnek morskich</h1>
<p className="lead">Witaj w panelu zarządzania hodowlą.</p>
<div className="row">
<div className="col-md-4"><div className="card text-center p-3">
<h2>{swinki.length}</h2><p className="text-muted">świnek</p>
</div></div>
<div className="col-md-4"><div className="card text-center p-3">
<h2>{rasy.length}</h2><p className="text-muted">ras</p>
</div></div>
<div className="col-md-4"><div className="card text-center p-3">
<h2>{sredniaCena.toFixed(0)} zł</h2><p className="text-muted">średnia cena</p>
</div></div>
</div>
</div>
);
}
export default Home;
FRONTEND — src/pages/ListaSwinek.js
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import { API_URL } from '../App';
function ListaSwinek() {
const [swinki, setSwinki] = useState([]);
const [rasy, setRasy] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
axios.get(`${API_URL}/swinki.php`),
axios.get(`${API_URL}/rasy.php`)
]).then(([s, r]) => {
setSwinki(s.data);
setRasy(r.data);
setLoading(false);
});
}, []);
function nazwaRasy(rasy_id) {
const r = rasy.find(x => x.id === rasy_id);
return r ? r.rasa : '?';
}
function usunSwinke(id) {
if (!window.confirm('Na pewno usunąć?')) return;
axios.delete(`${API_URL}/swinki.php?id=${id}`)
.then(() => setSwinki(swinki.filter(s => s.id !== id)));
}
if (loading) return <p>Ładowanie...</p>;
return (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<h2>Lista świnek</h2>
<Link to="/swinki/dodaj" className="btn btn-success">+ Dodaj świnkę</Link>
</div>
<table className="table table-striped">
<thead><tr>
<th>Imię</th><th>Rasa</th><th>Data ur.</th><th>Miot</th><th>Cena</th><th></th>
</tr></thead>
<tbody>
{swinki.map(s => (
<tr key={s.id}>
<td><strong>{s.imie}</strong></td>
<td>{nazwaRasy(s.rasy_id)}</td>
<td>{s.data_ur}</td>
<td>{s.miot}</td>
<td>{s.cena} zł</td>
<td>
<Link to={`/swinki/${s.id}/edytuj`} className="btn btn-sm btn-warning me-2">Edytuj</Link>
<button className="btn btn-sm btn-danger" onClick={() => usunSwinke(s.id)}>Usuń</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default ListaSwinek;
FRONTEND — src/pages/DodajSwinke.js
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { API_URL } from '../App';
function DodajSwinke() {
const [rasy, setRasy] = useState([]);
const [dane, setDane] = useState({
imie: '', rasy_id: 1, data_ur: '', miot: '', opis: '', cena: 0
});
const navigate = useNavigate();
useEffect(() => {
axios.get(`${API_URL}/rasy.php`).then(r => setRasy(r.data));
}, []);
function handleChange(e) {
const { name, value } = e.target;
// rasy_id i cena to liczby
const wartosc = (name === 'rasy_id' || name === 'cena') ? Number(value) : value;
setDane({ ...dane, [name]: wartosc });
}
function handleSubmit(e) {
e.preventDefault();
axios.post(`${API_URL}/swinki.php`, dane).then(() => {
navigate('/swinki');
});
}
return (
<div>
<h2>Dodaj nową świnkę</h2>
<form onSubmit={handleSubmit}>
<div className="mb-2">
<label className="form-label">Imię</label>
<input name="imie" value={dane.imie} onChange={handleChange}
className="form-control" required />
</div>
<div className="mb-2">
<label className="form-label">Rasa</label>
<select name="rasy_id" value={dane.rasy_id} onChange={handleChange}
className="form-select">
{rasy.map(r => (
<option key={r.id} value={r.id}>{r.rasa}</option>
))}
</select>
</div>
<div className="mb-2">
<label className="form-label">Data urodzenia</label>
<input type="date" name="data_ur" value={dane.data_ur}
onChange={handleChange} className="form-control" required />
</div>
<div className="mb-2">
<label className="form-label">Miot</label>
<input name="miot" value={dane.miot} onChange={handleChange}
className="form-control" />
</div>
<div className="mb-2">
<label className="form-label">Opis</label>
<textarea name="opis" value={dane.opis} onChange={handleChange}
className="form-control" rows="2" />
</div>
<div className="mb-3">
<label className="form-label">Cena (zł)</label>
<input type="number" name="cena" value={dane.cena}
onChange={handleChange} className="form-control" />
</div>
<button type="submit" className="btn btn-success me-2">Zapisz</button>
<button type="button" className="btn btn-secondary"
onClick={() => navigate('/swinki')}>Anuluj</button>
</form>
</div>
);
}
export default DodajSwinke;
FRONTEND — src/pages/EdytujSwinke.js
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import axios from 'axios';
import { API_URL } from '../App';
function EdytujSwinke() {
const { id } = useParams(); // czyta :id z URL (string!)
const navigate = useNavigate();
const [rasy, setRasy] = useState([]);
const [dane, setDane] = useState(null);
useEffect(() => {
Promise.all([
axios.get(`${API_URL}/swinki.php?id=${id}`),
axios.get(`${API_URL}/rasy.php`)
]).then(([s, r]) => {
setDane(s.data);
setRasy(r.data);
});
}, [id]);
function handleChange(e) {
const { name, value } = e.target;
const wartosc = (name === 'rasy_id' || name === 'cena') ? Number(value) : value;
setDane({ ...dane, [name]: wartosc });
}
function handleSubmit(e) {
e.preventDefault();
axios.put(`${API_URL}/swinki.php?id=${id}`, dane).then(() => {
navigate('/swinki');
});
}
if (!dane) return <p>Ładowanie...</p>;
return (
<div>
<h2>Edytuj świnkę: {dane.imie}</h2>
<form onSubmit={handleSubmit}>
<div className="mb-2">
<label className="form-label">Imię</label>
<input name="imie" value={dane.imie} onChange={handleChange}
className="form-control" required />
</div>
<div className="mb-2">
<label className="form-label">Rasa</label>
<select name="rasy_id" value={dane.rasy_id} onChange={handleChange}
className="form-select">
{rasy.map(r => (
<option key={r.id} value={r.id}>{r.rasa}</option>
))}
</select>
</div>
<div className="mb-2">
<label className="form-label">Data urodzenia</label>
<input type="date" name="data_ur" value={dane.data_ur}
onChange={handleChange} className="form-control" />
</div>
<div className="mb-2">
<label className="form-label">Miot</label>
<input name="miot" value={dane.miot} onChange={handleChange}
className="form-control" />
</div>
<div className="mb-2">
<label className="form-label">Opis</label>
<textarea name="opis" value={dane.opis} onChange={handleChange}
className="form-control" rows="2" />
</div>
<div className="mb-3">
<label className="form-label">Cena</label>
<input type="number" name="cena" value={dane.cena}
onChange={handleChange} className="form-control" />
</div>
<button type="submit" className="btn btn-primary me-2">Zapisz zmiany</button>
<button type="button" className="btn btn-secondary"
onClick={() => navigate('/swinki')}>Anuluj</button>
</form>
</div>
);
}
export default EdytujSwinke;
FRONTEND — src/pages/Rasy.js
import { useState, useEffect } from 'react';
import axios from 'axios';
import { API_URL } from '../App';
function Rasy() {
const [rasy, setRasy] = useState([]);
useEffect(() => {
axios.get(`${API_URL}/rasy.php`).then(r => setRasy(r.data));
}, []);
return (
<div>
<h2>Dostępne rasy</h2>
<ul className="list-group">
{rasy.map(r => (
<li key={r.id} className="list-group-item">
<strong>#{r.id}</strong> — {r.rasa}
</li>
))}
</ul>
</div>
);
}
export default Rasy;
FRONTEND — src/pages/NotFound.js
import { Link } from 'react-router-dom';
function NotFound() {
return (
<div className="text-center mt-5">
<h1>404</h1>
<p>Nie znaleziono strony!</p>
<Link to="/" className="btn btn-primary">Wróć do Home</Link>
</div>
);
}
export default NotFound;
Co warto rozszerzyć (zadania domowe)
- Dodaj filtrowanie świnek po rasie (select w nagłówku tabeli)
- Sortowanie po cenie (rosnąco/malejąco)
- Walidacja: cena nie może być ujemna, imię min. 2 znaki
- Strona szczegółów świnki
/swinki/:id(cały opis + duże zdjęcie) - CRUD też dla ras (dodaj/edytuj/usuń rasę)
- Wynieś logikę API do
src/api.js— funkcjegetSwinki(),dodajSwinke()itd. - Backend w innym języku (Node+Express, Python+Flask) — React zostaje bez zmian, tylko
API_URL
Backend PHP + MySQL — szczegóły i debugging
Główne kroki budowy zawierają już pełen kod PHP. Tutaj znajdziesz diagram architektury, tabelę typowych błędów i głębsze wyjaśnienia. Otwórz tę sekcję jeśli coś nie działa albo chcesz lepiej zrozumieć co się dzieje pod spodem.
Architektura — kto z kim gada
[React :3000] ──HTTP──► [Apache/PHP :80] ──SQL──► [MySQL :3306]
(frontend) (pośrednik) (baza)
axios.get('/swinki')
↓
swinki.php (PHP czyta tabelę)
↓
SELECT * FROM swinki
↓
zwraca JSON do React
Wymagania (XAMPP — uczeń już ma)
- Apache uruchomiony (XAMPP Control Panel → Start)
- MySQL uruchomiony (XAMPP Control Panel → Start)
- Baza
hodowlaz tabelamirasyiswinki(już jest) - Folder Apache:
C:\xampp\htdocs\— tu trafią pliki PHP
W folderze htdocs utwórz folder hodowla-api. To będzie nasz backend. Powstaną 3 pliki:
htdocs/hodowla-api/
├── db.php ← połączenie z MySQL (PDO)
├── swinki.php ← CRUD na tabeli swinki
└── rasy.php ← READ na tabeli rasy
http://localhost/hodowla-api/ w przeglądarce. Jeśli pokaże listę plików (lub 403) — Apache działa.
<?php
$host = '127.0.0.1';
$db = 'hodowla';
$user = 'root';
$pass = ''; // domyślnie XAMPP nie ma hasła
$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";
try {
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => 'Database connection failed: ' . $e->getMessage()]);
exit;
}
PDO— nowoczesny sposób łączenia z bazą w PHP (bezpieczny przed SQL injection)$user='root',$pass=''— domyślne dane XAMPP. Na produkcji nigdy!FETCH_ASSOC— wyniki jako tablice asocjacyjne (klucze = nazwy kolumn)try/catch— jeśli baza padnie, zwracamy 500 + JSON z błędem (zamiast crasha)
<?php
// CORS — pozwól Reactowi (z portu 3000) gadać do Apache (port 80)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Type: application/json; charset=utf-8');
// Preflight (przeglądarka wysyła OPTIONS przed PUT/DELETE — odpowiadamy 204)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
require 'db.php';
$method = $_SERVER['REQUEST_METHOD'];
$id = $_GET['id'] ?? null;
switch ($method) {
case 'GET':
if ($id) {
// GET /swinki?id=5 — jedna świnka
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
} else {
// GET /swinki — wszystkie
$stmt = $pdo->query('SELECT * FROM swinki');
echo json_encode($stmt->fetchAll());
}
break;
case 'POST':
// POST /swinki — dodaj nową
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare(
'INSERT INTO swinki (rasy_id, data_ur, miot, opis, imie, cena)
VALUES (?, ?, ?, ?, ?, ?)'
);
$stmt->execute([
$data['rasy_id'], $data['data_ur'], $data['miot'],
$data['opis'], $data['imie'], $data['cena']
]);
$newId = $pdo->lastInsertId();
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$newId]);
echo json_encode($stmt->fetch());
break;
case 'PUT':
// PUT /swinki?id=5 — edytuj
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare(
'UPDATE swinki SET rasy_id=?, data_ur=?, miot=?, opis=?, imie=?, cena=?
WHERE id=?'
);
$stmt->execute([
$data['rasy_id'], $data['data_ur'], $data['miot'],
$data['opis'], $data['imie'], $data['cena'], $id
]);
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
break;
case 'DELETE':
// DELETE /swinki?id=5 — usuń
$stmt = $pdo->prepare('DELETE FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode(['ok' => true]);
break;
default:
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
prepare() + execute([...]). Wartości są przekazywane jako parametry, nie wklejane do tekstu zapytania. To jest bezpieczne.
json_decode(..., true) zamienia na tablicę asocjacyjną.
<?php
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json; charset=utf-8');
require 'db.php';
$stmt = $pdo->query('SELECT * FROM rasy ORDER BY rasa');
echo json_encode($stmt->fetchAll());
Krócej, bo rasy są tylko do czytania — nie dodajemy/edytujemy/usuwamy ras z tej apki.
Można by zrobić "ładne URL-e" (/swinki/5) przez Apache mod_rewrite i plik .htaccess. Świadomie tego nie robimy.
| Podejście | URL | Zależność |
|---|---|---|
| .htaccess + mod_rewrite | /swinki/5 | Wymaga AllowOverride All + mod_rewrite + restart Apache. Różni się Windows/macOS/MAMP. Kruche. |
| Query string (nasze) | /swinki.php?id=5 | Zero konfiguracji. Działa na każdym serwerze, zawsze. |
?id=) jest częścią standardu HTTP i działa bez wyjątku wszędzie. W realnym projekcie wybieramy rozwiązanie odporne na środowisko, nie ładniejsze ale kruche.
Dlatego React woła wprost: axios.get(`${API_URL}/swinki.php`) oraz axios.get(`${API_URL}/swinki.php?id=${id}`). PHP czyta ID przez $_GET['id'] — i to wszystko, żadnego pliku .htaccess.
Zanim podpinamy React — sprawdź że PHP samo działa. Otwórz w przeglądarce:
| URL | Co powinno pokazać |
|---|---|
http://localhost/hodowla-api/rasy.php | JSON z listą wszystkich ras |
http://localhost/hodowla-api/swinki.php | JSON z wszystkimi świnkami |
http://localhost/hodowla-api/swinki.php?id=1 | JSON jednej świnki (Crejzy) |
Jeśli widzisz błąd: sprawdź czy Apache + MySQL działają (XAMPP Control Panel), czy plik db.php ma poprawne dane logowania.
W src/App.js:
// było (json-server):
const API_URL = 'http://localhost:3001';
// teraz (PHP + MySQL):
const API_URL = 'http://localhost/hodowla-api';
Wyłącz json-server (nie potrzebujemy) i uruchom React:
npm start
Otwórz dwa okna obok siebie:
- Lewe: React na
http://localhost:3000/swinki - Prawe: phpMyAdmin →
hodowla.swinki
| Akcja w React | Co zobaczysz w phpMyAdmin (po odświeżeniu) |
|---|---|
| Dodaj świnkę "Pinki" | Nowy wiersz w tabeli swinki |
| Edytuj cenę świnki | Zaktualizowana wartość w kolumnie cena |
| Usuń świnkę | Wiersz znika z tabeli |
| Odśwież stronę React | Dane zostają — to NIE jest pamięć przeglądarki, to MySQL! |
| Dodaj wiersz w phpMyAdmin | Po refreshu Reacta — nowa świnka pojawia się w UI |
Typowe błędy i jak je naprawić
| Błąd | Przyczyna | Naprawa |
|---|---|---|
| CORS error w F12 Console | PHP nie wysłał headera Access-Control-Allow-Origin | Sprawdź czy header('Access-Control-Allow-Origin: *') jest na początku PHP |
| 404 Not Found | Zły URL — brak .php albo zła ścieżka folderu | Sprawdź że wołasz /hodowla-api/swinki.php (z .php!), folder w htdocs poprawny |
| 500 co jakiś czas (działa→sypie→refresh) | Limit połączeń MySQL pod serią requestów | W my.cnf: max_connections=300 + restart MySQL. Usuń <React.StrictMode> (dubluje requesty w dev) |
| "Database connection failed" | MySQL nie działa lub złe hasło | XAMPP → Start MySQL, sprawdź $user/$pass w db.php |
| 500 Internal Server Error | Błąd składni PHP lub w SQL | Otwórz C:\xampp\apache\logs\error.log — jest tam dokładny komunikat |
| POST/PUT zwraca null w PHP | Brak php://input + json_decode | axios wysyła JSON w body — czytamy: json_decode(file_get_contents('php://input'), true) |
| Polskie znaki = krzaki | Brak charset=utf8mb4 | Sprawdź DSN w db.php oraz Content-Type: application/json; charset=utf-8 |
Co teraz wiesz
- Jak React + axios łączy się z dowolnym backendem REST (json-server, PHP, Node, Python — wszystkie identycznie)
- Jak napisać REST API w PHP używając PDO (bezpiecznie, bez SQL injection)
- Co to CORS i czemu PHP musi go ustawić
- Czemu query string (
?id=) jest pewniejszy niż URL rewrite (.htaccess) - Architektura 3-warstwowa: Frontend → Backend → Baza
- Możesz teraz spiąć React z dowolną tabelą MySQL — schemat zawsze taki sam
Sprawdzian — Hodowla świnek (CRUD + Router)
5 pytań · 5 minut · minimum 60% żeby zdać
swinka.rasy_id i tablicę rasy?useParams() z react-router-dom dla URL /swinki/5/edytuj i trasy /swinki/:id/edytuj?<select name="rasy_id">. Bez konwersji wartość po onChange to "4" (string), a w db.json rasy_id to liczba 4. Co się dzieje przy find(r => r.id === swinka.rasy_id)?Rozgrzewka — Menedżer haseł
Pięć pytań, każde o jedną linijkę dzisiejszego komponentu. Kliknij pytanie, żeby zobaczyć odpowiedź.
1. Dlaczego w JSX piszemy className i htmlFor, a nie class i for?
JSX to JavaScript, a class i for są w nim słowami zarezerwowanymi. Arkusz przypomina o tym w tabeli 1: „In React render method use className instead of class”. Szablon z szablon.txt trzeba przerobić właśnie w tych dwóch miejscach i domknąć <input />.
2. Co daje e.target.value dla checkboxa?
Zawsze napis "on", niezależnie od tego, czy pole jest zaznaczone. Stan checkboxa czytamy z e.target.checked, które daje true albo false.
3. Co się stanie po kliknięciu przycisku type="submit" bez e.preventDefault()?
Przeglądarka wyśle formularz po staremu, czyli przeładuje stronę. React zaczyna wtedy od zera i podsumowanie znika w ułamku sekundy. Uczeń widzi „mignięcie” i myśli, że przycisk nie działa.
4. Jak zablokować pole w zależności od stanu?
Atrybutem disabled z wartością logiczną: disabled={generuj}. Gdy generuj jest true, pole jest szare i nie da się w nim pisać.
5. Jak wyświetlić jeden z dwóch tekstów zależnie od warunku?
Operatorem warunkowym: {warunek ? 'tekst A' : 'tekst B'}. Dziś trzeci akapit pokazuje albo „Hasło automatycznie generowane”, albo „Hasło: …”.
Menedżer haseł — Instrukcja
Druga część tego samego arkusza co generator haseł w Pythonie. Jeden komponent React z formularzem i podsumowaniem, ostylowany Bootstrapem na podstawie szablon.txt z archiwum Web1.7z. Na egzaminie można wybrać Angular albo React. Robimy React, bo uczeń zna go z wcześniejszych ćwiczeń.
Jak ma wyglądać (ilustracje z arkusza)
| Stan | Formularz | Podsumowanie pod przyciskiem |
|---|---|---|
| 1. Start | pola puste, „Wygeneruj hasło” zaznaczone, pole hasła szare | Adres: / Login: / Hasło automatycznie generowane |
| 2. Zapisz z zaznaczonym polem wyboru | poczta.pl, Ola, pole hasła szare | Adres: poczta.pl / Login: Ola / Hasło automatycznie generowane |
| 3. Zapisz z odznaczonym polem wyboru | poczta.pl, Ola, hasło w kropkach | Adres: poczta.pl / Login: Ola / Hasło: sYt@kU*& |
Pierwszy akapit jest niebieski i większy (text-primary lead). Etykieta pola wyboru brzmi „Wygeneruj hasło”. Tego tekstu nie ma w wymaganiach, jest tylko na ilustracji.
Wymagania i gdzie są w kodzie
| Wymaganie z arkusza | W kodzie |
|---|---|
| Jeden komponent, dwa bloki | App.jsx: <div> z formularzem i <div> z akapitami |
| h1 „Menedżer haseł - wykonał” + numer zdającego | <h1>Menedżer haseł - wykonał 00000000000</h1> |
| Etykiety powiązane z polami | htmlFor="adres" i id="adres", tak samo login i hasło |
| Podpowiedzi w polach | placeholder="Adres strony WWW", placeholder="Twój login" |
| Pole wyboru domyślnie zaznaczone | useState(true) + checked={generuj} |
| Pole hasła ukrywa znaki | type="password" |
| Zaznaczone, czyli hasło nieaktywne | disabled={generuj} |
| Niebieski przycisk „Zapisz” | className="btn btn-primary m-2" |
| Podsumowanie wypełniane po zatwierdzeniu | osobny stan zapisane, ustawiany tylko w zapisz() |
Akapity: p-2, pierwszy dodatkowo text-primary lead | trzy <p> pod formularzem |
| Trzeci akapit zależny od pola wyboru | {zapisane.generuj ? ... : ...} |
| web.zip, zmodyfikowane pliki, zrzuty web1–web3 | krok 8 |
Pliki: Web1.7z (hasło *LoGiny&H@sla8) i sprawdz_web.js, czyli sprawdzarka z kroku 7.
Menedżer haseł — Budowa krok po kroku
Uczeń pisze u siebie i udostępnia ekran. Cel na egzamin to część II w około 60 minut razem ze zrzutami. Z 180 minut zostaje wtedy zapas na poprawki.
W folderze webowa (albo obok, a do webowa trafi potem zip):
npm create vite@latest web -- --template react
cd web
npm install
npm install bootstrap
npm run dev
Po npm run dev otwórz adres z terminala (http://localhost:5173). Na razie widać stronę startową Vite.
create-react-app jest od 2025 roku wycofany i z nowym Reactem potrafi wywrócić się na instalacji zależności. Vite zakłada projekt w kilka sekund, a jego node_modules ma około 70 MB i 900 plików. Ma to znaczenie przy pakowaniu web.zip.W src/main.jsx usuń linię import './index.css'. Plik ma wyglądać tak:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
index.css z szablonu
Style Vite wyśrodkowują stronę, zmieniają czcionkę i w trybie ciemnym systemu (częsty na Macu) dają ciemne tło. Bootstrap działa, ale aplikacja nie wygląda jak na ilustracji i egzaminator może uznać wygląd za niezgodny. Jedna usunięta linijka załatwia sprawę.Całą zawartość src/App.jsx zastępujesz własnym kodem w następnych krokach. Import App.css znika razem z nią.
import { useState } from 'react';
import 'bootstrap/dist/css/bootstrap.css';
function App() {
const [adres, setAdres] = useState('');
const [login, setLogin] = useState('');
const [haslo, setHaslo] = useState('');
const [generuj, setGeneruj] = useState(true);
const [zapisane, setZapisane] = useState({ adres: '', login: '', haslo: '', generuj: true });
return (
<div>
<h1>Menedżer haseł - wykonał 00000000000</h1>
</div>
);
}
export default App;
Cztery pierwsze stany trzymają to, co jest teraz w polach. Piąty, zapisane, trzyma to, co było w polach w chwili kliknięcia „Zapisz”. Podsumowanie czyta wyłącznie z zapisane.
{adres} zamiast {zapisane.adres}, tekst zmienia się przy każdej literze, czyli przed zatwierdzeniem. Na ekranie wygląda to nawet efektownie, ale wymaganie nie jest spełnione.Import Bootstrapa jest dokładnie taki jak w tabeli 1 arkusza. Numer zdającego wpisujesz w h1 (na ćwiczeniach dowolny).
Otwórz szablon.txt z archiwum i skopiuj go pod h1:
<form>
<div class="form-group m-4">
<label for="exampleInputEmail1">Email address</label>
<input type="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
</div>
<div class="form-group m-4">
<label for="exampleInputPassword1">Password</label>
<input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<button type="submit" class="btn ... m-2">Submit</button>
</form>
Pięć zmian, każda wynika z innego wymagania:
| W szablonie | W JSX | Dlaczego |
|---|---|---|
class= | className= | składnia JSX (tabela 1 arkusza) |
for= | htmlFor= | składnia JSX, powiązanie etykiety z polem |
<input ...> | <input ... /> | JSX wymaga domknięcia |
id="exampleInputEmail1" | id="adres" | znaczące nazewnictwo |
btn ... | btn btn-primary | niebieski przycisk |
Pola adresu i loginu po przeróbce:
<form onSubmit={zapisz}>
<div className="form-group m-4">
<label htmlFor="adres">Adres WWW:</label>
<input type="text" className="form-control" id="adres" placeholder="Adres strony WWW"
value={adres} onChange={(e) => setAdres(e.target.value)} />
</div>
<div className="form-group m-4">
<label htmlFor="login">Login:</label>
<input type="text" className="form-control" id="login" placeholder="Twój login"
value={login} onChange={(e) => setLogin(e.target.value)} />
</div>
value + onChange zawsze razem
Samo value={adres} bez onChange daje pole, w którym nie da się nic wpisać, a React ostrzega o tym w konsoli. Typ pola adresu to text, a nie email z szablonu, bo wpisujemy adres strony, a nie e-mail. <div className="form-group m-4">
<input type="checkbox" id="generuj"
checked={generuj} onChange={(e) => setGeneruj(e.target.checked)} />
<label htmlFor="generuj" className="ms-1">Wygeneruj hasło</label>
<br />
<label htmlFor="haslo">Hasło:</label>
<input type="password" className="form-control" id="haslo" disabled={generuj}
value={haslo} onChange={(e) => setHaslo(e.target.value)} />
</div>
<button type="submit" className="btn btn-primary m-2">Zapisz</button>
</form>
e.target.value w checkboxie
Dla checkboxa value to zawsze "on", czyli niepusty napis traktowany jak prawda. Po odznaczeniu pole hasła zostaje szare i nie da się wpisać hasła, więc ilustracji 3 nie da się odtworzyć. Przy checkboxie czytasz e.target.checked.Sprawdź w przeglądarce: odznacz, wpisz hasło, zaznacz znowu. Pole szarzeje, a wpisane kropki zostają, co jest zgodne z ilustracjami.
Funkcja obsługi formularza, nad return:
function zapisz(e) {
e.preventDefault();
setZapisane({ adres: adres, login: login, haslo: haslo, generuj: generuj });
}
Blok drugi, pod blokiem z formularzem:
<div>
<p className="p-2 text-primary lead">Adres: {zapisane.adres}</p>
<p className="p-2">Login: {zapisane.login}</p>
<p className="p-2">
{zapisane.generuj ? 'Hasło automatycznie generowane' : 'Hasło: ' + zapisane.haslo}
</p>
</div>
| Wymaganie | Klasa Bootstrap (tabela 1 arkusza) |
|---|---|
| margines wewnętrzny 0,5 rem | p-2 (p = padding, 2 = .5rem) |
| tekst niebieski | text-primary |
| tekst wiodący | lead |
e.preventDefault()
Strona się przeładowuje, React startuje od nowa i podsumowanie wraca do stanu początkowego. Uczeń widzi mignięcie i myśli, że setZapisane nie działa.- Aplikacja otwarta w przeglądarce (
npm run dev). - F12 (macOS:
Cmd + Opt + I), zakładka Console. - Otwórz
sprawdz_web.js, skopiuj całość, wklej do konsoli i Enter. Chrome za pierwszym razem każe wpisaćallow pasting.
OK nagłówek h1 „Menedżer haseł - wykonał” + numer zdającego
OK etykieta „Adres WWW:” powiązana z polem
...
OK podsumowanie NIE zmienia się w trakcie pisania
OK zaznaczone: „Hasło automatycznie generowane”
OK po odznaczeniu pole hasła aktywne
OK odznaczone: „Hasło: <hasło>”
Wynik: 21/21 wymagań spełnionych
Sprawdzarka sama wpisuje poczta.pl, Ola i sYt@kU*& oraz dwa razy klika „Zapisz”. Na koniec aplikacja jest w stanie z ilustracji 3. Przed zrzutami odśwież stronę (F5).
Zrzuty do folderu dokumentacja. Za każdym razem cały ekran z paskiem zadań (na Macu z Dockiem), z przeglądarką i widocznym IDE albo terminalem z npm run dev:
| Plik | Stan |
|---|---|
web1.png | stan początkowy, zaraz po otwarciu |
web2.png | wpisane dane, pole wyboru zaznaczone, po kliknięciu „Zapisz” |
web3.png | pole wyboru odznaczone, wpisane hasło, po kliknięciu „Zapisz” |
Folder webowa:
web.zip, czyli spakowany cały folder projektu, łącznie znode_modules, bo arkusz mówi „archiwum całego folderu projektu”. Przy Vite to około 70 MB, a pakowanie trwa poniżej minuty. Najpierw zatrzymajnpm run dev(Ctrl + C).- kopie zmodyfikowanych plików:
App.jsximain.jsx
egzamin.txt: dopisz środowisko i język z tej części:
System operacyjny: Windows 11 Pro
Środowisko programistyczne: PyCharm Community, Visual Studio Code
Języki programowania: Python, JavaScript (React)
Menedżer haseł — Gotowy kod
Kompletny komponent. Daje 21/21 w sprawdzarce i odtwarza ilustracje 1–3 z arkusza. Pokazujesz go po samodzielnej próbie ucznia.
src/App.jsx
import { useState } from 'react';
import 'bootstrap/dist/css/bootstrap.css';
function App() {
const [adres, setAdres] = useState('');
const [login, setLogin] = useState('');
const [haslo, setHaslo] = useState('');
const [generuj, setGeneruj] = useState(true);
const [zapisane, setZapisane] = useState({ adres: '', login: '', haslo: '', generuj: true });
function zapisz(e) {
e.preventDefault();
setZapisane({ adres: adres, login: login, haslo: haslo, generuj: generuj });
}
return (
<div>
<div>
<h1>Menedżer haseł - wykonał 00000000000</h1>
<form onSubmit={zapisz}>
<div className="form-group m-4">
<label htmlFor="adres">Adres WWW:</label>
<input type="text" className="form-control" id="adres" placeholder="Adres strony WWW"
value={adres} onChange={(e) => setAdres(e.target.value)} />
</div>
<div className="form-group m-4">
<label htmlFor="login">Login:</label>
<input type="text" className="form-control" id="login" placeholder="Twój login"
value={login} onChange={(e) => setLogin(e.target.value)} />
</div>
<div className="form-group m-4">
<input type="checkbox" id="generuj"
checked={generuj} onChange={(e) => setGeneruj(e.target.checked)} />
<label htmlFor="generuj" className="ms-1">Wygeneruj hasło</label>
<br />
<label htmlFor="haslo">Hasło:</label>
<input type="password" className="form-control" id="haslo" disabled={generuj}
value={haslo} onChange={(e) => setHaslo(e.target.value)} />
</div>
<button type="submit" className="btn btn-primary m-2">Zapisz</button>
</form>
</div>
<div>
<p className="p-2 text-primary lead">Adres: {zapisane.adres}</p>
<p className="p-2">Login: {zapisane.login}</p>
<p className="p-2">
{zapisane.generuj ? 'Hasło automatycznie generowane' : 'Hasło: ' + zapisane.haslo}
</p>
</div>
</div>
);
}
export default App;
src/main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
Jak to czytać
- Pięć stanów: cztery dla pól formularza i jeden (
zapisane) dla tego, co pokazuje podsumowanie. zapisz()blokuje przeładowanie strony i kopiuje bieżące pola dozapisane.- Checkbox steruje dwiema rzeczami:
disabledpola hasła teraz i treścią trzeciego akapitu po zapisie.
Menedżer haseł — Sprawdzian
5 pytań · 5 minut · minimum 60%, żeby zdać
id="login"?{adres} zamiast {zapisane.adres}. Co jest nie tak?Rozgrzewka — Galeria zdjęć
Pięć pytań, każde o jedną linijkę dzisiejszego komponentu. Kliknij pytanie, żeby zobaczyć odpowiedź.
1. Co zwraca [1, 2, 3].map((x) => x * 10) i do czego służy map w JSX?
[10, 20, 30], czyli nową tablicę tej samej długości. W JSX map zamienia tablicę obiektów na tablicę elementów, np. bloków zdjęć. To jest „pętla” z wymagań arkusza.
2. Co wyświetli React dla {false && <p>Tekst</p>}?
Nic. Gdy lewa strona && jest fałszem, całe wyrażenie daje false, a React go nie rysuje. Tak dziś ukrywamy zdjęcia z wyłączonej kategorii. To jest „warunek” z wymagań arkusza.
3. Co robi { ...zdjecie, downloads: zdjecie.downloads + 1 }?
Tworzy nowy obiekt: kopiuje wszystkie pola zdjęcia, a downloads nadpisuje wartością większą o 1. Stary obiekt zostaje bez zmian. React porównuje obiekty po referencji, więc tylko nowy obiekt w nowej tablicy daje mu znać, że ma przerysować ekran.
4. Czym różni się onClick={pobierz(zdjecie.id)} od onClick={() => pobierz(zdjecie.id)}?
Pierwszy zapis wywołuje pobierz od razu, w trakcie rysowania, a nie po kliknięciu. Funkcja zmienia stan, stan wymusza ponowne rysowanie i całość kończy się błędem „Too many re-renders”. Drugi zapis przekazuje funkcję, którą React wywoła dopiero po kliknięciu.
5. Plik leży w public/assets/obraz1.jpg. Jaki src wpisać?
/assets/obraz1.jpg. Zawartość public serwer udostępnia od głównego adresu, bez słowa „public” w ścieżce. Gdyby obrazy leżały w src/assets, każdy trzeba by importować osobno, a przy dwunastu plikach to strata czasu.
Galeria zdjęć — Instrukcja
Druga część tego samego arkusza co klasa z tablicą w Pythonie. Jeden komponent React: trzy przełączniki kategorii i bloki zdjęć z licznikiem pobrań. Na egzaminie można wybrać Angular albo React. Robimy React.
Treść zadania
Aplikacja front-end realizująca kategoryzację zdjęć w galerii. Materiały: pliki3.zip, hasło K@tegorie): 12 zdjęć i dane.txt.
- Jeden komponent; zdjęcia mogą być wyświetlane w dowolnej kolejności
- Obrazy w folderze
assets(egzamin/src/assetsalboegzamin/public/assets) dane.txtzawiera listę obiektów, którą należy skopiować jako elementy tablicy:id,alt,filename,category(1 kwiaty, 2 zwierzęta, 3 samochody),downloads- Nagłówek h1 „Kategorie zdjęć”
- Trzy pola switch, domyślnie włączone: Kwiaty, Zwierzęta, Samochody
- Bloki zdjęć wyświetlane warunkowo, jeden obok drugiego: zdjęcie, nagłówek h4 z liczbą pobrań, przycisk „Pobierz”
- Zdjęcia: marginesy zewnętrzne 5 px, zaokrąglone rogi; przyciski i pola switch według tabeli 1 (Bootstrap)
- Widoczne tylko zdjęcia z kategorii z włączonym przełącznikiem
- „Pobierz” zwiększa o 1 liczbę pobrań; liczba jest zapisywana w tablicy obiektów zdjęć i od razu widoczna na ekranie
- Pętle i warunki do wyświetlenia bloków; aplikacja działa poprawnie dla innej liczby zdjęć
- W folderze
webowa:web.zipz całym folderem projektu i zmodyfikowane pliki źródłowe
Jak ma wyglądać (ilustracje z arkusza)
| Obraz | Stan | Co widać |
|---|---|---|
| 2 | start | trzy niebieskie przełączniki w jednym rzędzie, 12 bloków po trzy w rzędzie, pod każdym zdjęciem „Pobrań: 35” i zielony przycisk „Pobierz” |
| 3 | wyłączone „Zwierzęta” | 3 kwiaty i 2 samochody |
| 4 | włączone tylko „Samochody”, kilka kliknięć pod Garbusem | Audi „Pobrań: 11”, Garbus „Pobrań: 333” zamiast 321 |
Kolor przycisku arkusz pokazuje tylko na ilustracjach: to btn-success. Zdjęcia mają w archiwum 400 × 266 px, więc trzy mieszczą się w rzędzie bez żadnego skalowania.
Wymagania i gdzie są w kodzie
| Wymaganie z arkusza | W kodzie | Jak łatwo to stracić |
|---|---|---|
Dane z dane.txt jako tablica | const ZDJECIA = [ ... ] i useState(ZDJECIA) | przepisywanie ręczne zamiast kopiowania |
| Trzy switche domyślnie włączone | useState(true) × 3, checked + onChange | e.target.value zamiast e.target.checked |
| Switche według tabeli 1 | form-check form-switch form-check-inline | brak form-check-inline, przełączniki jeden pod drugim |
| Pętla i warunek | zdjecia.map(...) z czyWidoczne(...) && | 12 bloków przepisanych ręcznie |
| Bloki obok siebie | className="d-flex flex-wrap" | bloki jeden pod drugim |
| Margines 5 px, zaokrąglone rogi | App.css: img { margin: 5px; border-radius: 8px; } | padding zamiast margin |
| Pobrania zapisane w tablicy | setZdjecia(zdjecia.map(...)) z nowym obiektem | mutacja downloads++, ekran się nie odświeża |
Zrzuty web1, web2, ... i web.zip | krok 8 | brak zrzutu po kliknięciu „Pobierz” |
Pliki: pliki3.zip z maila (hasło K@tegorie)) i sprawdz_galeria.js, czyli sprawdzarka z kroku 7.
Galeria zdjęć — Budowa krok po kroku
Uczeń pisze u siebie i udostępnia ekran. Cel na egzamin: część II w około 45 minut razem ze zrzutami.
- Rozpakuj
pliki3.ziphasłemK@tegorie). Hasło kończy się nawiasem zamykającym, który łatwo pominąć. - W folderze
webowautwórz projekt o nazwieegzamin, bo tak nazywa go arkusz:
npm create vite@latest egzamin -- --template react
cd egzamin
npm install
npm install bootstrap
npm run dev
create-react-app jest wycofany i z aktualnym Reactem potrafi wywrócić się na instalacji. Vite zakłada projekt w kilka sekund, a mniejszy node_modules skraca pakowanie web.zip.egzamin/
├── public/
│ └── assets/ <- tu 12 plików obraz1.jpg ... obraz12.jpg
└── src/
├── App.jsx <- cały komponent
├── App.css <- styl zdjęć
└── main.jsx <- usunąć import './index.css'
W src/main.jsx usuń linię import './index.css'. Całą zawartość App.css i App.jsx zastępujesz w następnych krokach.
index.css z szablonu
Wyśrodkowuje stronę, zmienia czcionkę, a w ciemnym motywie systemu daje ciemne tło. Galeria przestaje wyglądać jak obraz 2.Otwórz dane.txt, skopiuj 12 linii z obiektami i owiń je w const ZDJECIA = [ … ]; nad komponentem. Zapis z pliku jest poprawnym JavaScriptem, nic nie trzeba poprawiać.
import { useState } from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import './App.css';
const ZDJECIA = [
{ id: 0, alt: 'Mak', filename: 'obraz1.jpg', category: 1, downloads: 35 },
{ id: 1, alt: 'Bukiet', filename: 'obraz2.jpg', category: 1, downloads: 43 },
// ... pozostałe 10 linii z dane.txt
];
function App() {
const [zdjecia, setZdjecia] = useState(ZDJECIA);
const [kwiaty, setKwiaty] = useState(true);
const [zwierzeta, setZwierzeta] = useState(true);
const [samochody, setSamochody] = useState(true);
return (
<div>
<h1>Kategorie zdjęć</h1>
</div>
);
}
export default App;
useState(ZDJECIA), a nie samo ZDJECIA
Arkusz wymaga, żeby liczba pobrań była zapisywana w tablicy i od razu widoczna. Tablica w stanie to jedyny sposób, żeby React wiedział o zmianie i przerysował ekran.Szablon z tabeli 1 arkusza przerobiony na JSX, pod h1. Trzy razy to samo, zmienia się id, etykieta i stan:
<div className="form-check form-switch form-check-inline">
<input className="form-check-input" type="checkbox" id="kwiaty"
checked={kwiaty} onChange={(e) => setKwiaty(e.target.checked)} />
<label className="form-check-label" htmlFor="kwiaty">Kwiaty</label>
</div>
| W tabeli 1 | W JSX | Dlaczego |
|---|---|---|
class= | className= | składnia JSX |
for= | htmlFor= | składnia JSX, kliknięcie w etykietę przełącza pole |
<input ...> | <input ... /> | JSX wymaga domknięcia |
form-check form-switch | + form-check-inline | jeden rząd, jak na obrazie 2 (opis w tabeli 1) |
e.target.value
Dla checkboxa to zawsze napis "on". Po pierwszym kliknięciu stan zamienia się w napis, który JavaScript traktuje jak prawdę, i przełącznika nie da się wyłączyć. Stan pola czytasz z e.target.checked.Funkcja, która mówi, czy kategoria jest włączona (nad return):
function czyWidoczne(kategoria) {
if (kategoria === 1) return kwiaty;
if (kategoria === 2) return zwierzeta;
if (kategoria === 3) return samochody;
return false;
}
Bloki pod przełącznikami:
<div className="d-flex flex-wrap">
{zdjecia.map((zdjecie) => czyWidoczne(zdjecie.category) && (
<div key={zdjecie.id}>
<img src={'/assets/' + zdjecie.filename} alt={zdjecie.alt} />
<h4>Pobrań: {zdjecie.downloads}</h4>
<button className="btn btn-success">Pobierz</button>
</div>
))}
</div>
src/App.css, cała zawartość:
img {
margin: 5px;
border-radius: 8px;
}
key
Aplikacja działa, ale konsola przeglądarki pokazuje ostrzeżenie „Each child in a list should have a unique key”. Egzaminator, który zajrzy do konsoli, uzna to za niedbałość. key={zdjecie.id} na najbardziej zewnętrznym elemencie bloku. function pobierz(id) {
setZdjecia(zdjecia.map((zdjecie) =>
zdjecie.id === id ? { ...zdjecie, downloads: zdjecie.downloads + 1 } : zdjecie
));
}
<button className="btn btn-success" onClick={() => pobierz(zdjecie.id)}>Pobierz</button>
Rozpisz z uczniem jedno kliknięcie pod Garbusem (id 11): map przechodzi 12 zdjęć, jedenaście oddaje bez zmian, dla Garbusa tworzy nowy obiekt z downloads: 322. Wynik to nowa tablica, więc React przerysowuje ekran.
zdjecie.downloads++ i setZdjecia(zdjecia)
Liczba w tablicy rośnie, ale tablica jest ta sama, więc React uznaje, że nic się nie zmieniło, i ekran stoi. Po przełączeniu dowolnego switcha liczba nagle skacze o tyle, ile było kliknięć. Pokaż to uczniowi: sprawdzarka zgłasza wtedy „liczba pobrań rośnie o 1 na kliknięcie”, a test „po ukryciu i pokazaniu liczba zostaje” przechodzi. Dane się zmieniają, tylko React o tym nie wie.onClick={pobierz(zdjecie.id)}
Biała strona i w konsoli „Too many re-renders”. Bez () => funkcja wykonuje się w trakcie rysowania.- Aplikacja otwarta w przeglądarce (
npm run dev), okno szerokie na co najmniej dwa zdjęcia. - F12 (macOS:
Cmd + Opt + I), zakładka Console. - Otwórz
sprawdz_galeria.js, skopiuj całość, wklej do konsoli i Enter. Chrome za pierwszym razem każe wpisaćallow pasting.
OK nagłówek h1 „Kategorie zdjęć”
OK przełączniki w jednym rzędzie, jak na obrazie 2
OK wszystkie obrazy się wczytują
OK zdjęcia: margines zewnętrzny 5 px
OK przycisk zielony jak na obrazach 2-4 (btn-success)
Wyłączam „Zwierzęta” (obraz 3)
OK zostają kwiaty i samochody (5 zdjęć)
...
OK po ukryciu i pokazaniu liczba pobrań zostaje (zapisana w tablicy zdjęć)
Wynik: 24/24 wymagań spełnionych
Sprawdzarka trzy razy klika „Pobierz” pod Garbusem, więc po niej licznik pokazuje 324. Przed zrzutami odśwież stronę (F5).
Arkusz wymaga zrzutów stanu początkowego, różnych kombinacji przełączników i działania przycisku. Za każdym razem cały ekran z paskiem zadań, przeglądarka i widoczne IDE albo terminal z npm run dev:
| Plik | Stan |
|---|---|
web1.png | stan początkowy, wszystkie przełączniki włączone |
web2.png | wyłączone „Zwierzęta” |
web3.png | włączone tylko „Samochody” |
web4.png | ten sam stan po kilku kliknięciach „Pobierz” pod jednym zdjęciem |
Folder webowa: web.zip z całym folderem egzamin (z node_modules, po zatrzymaniu npm run dev przez Ctrl + C) oraz kopie zmodyfikowanych plików: App.jsx, App.css, main.jsx.
Plik egzamin w dokumentacja: ten sam co w części I, z JavaScriptem i React w wierszu języków.
Galeria zdjęć — Gotowy kod
Kompletny komponent. Daje 24/24 w sprawdzarce i odtwarza obrazy 2–4 z arkusza. Pokazujesz go po samodzielnej próbie ucznia.
src/App.jsx
import { useState } from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import './App.css';
const ZDJECIA = [
{ id: 0, alt: 'Mak', filename: 'obraz1.jpg', category: 1, downloads: 35 },
{ id: 1, alt: 'Bukiet', filename: 'obraz2.jpg', category: 1, downloads: 43 },
{ id: 2, alt: 'Dalmatyńczyk', filename: 'obraz3.jpg', category: 2, downloads: 2 },
{ id: 3, alt: 'Świnka morska', filename: 'obraz4.jpg', category: 2, downloads: 53 },
{ id: 4, alt: 'Rottweiler', filename: 'obraz5.jpg', category: 2, downloads: 43 },
{ id: 5, alt: 'Audi', filename: 'obraz6.jpg', category: 3, downloads: 11 },
{ id: 6, alt: 'kotki', filename: 'obraz7.jpg', category: 2, downloads: 22 },
{ id: 7, alt: 'Róża', filename: 'obraz8.jpg', category: 1, downloads: 33 },
{ id: 8, alt: 'Świnka morska', filename: 'obraz9.jpg', category: 2, downloads: 123 },
{ id: 9, alt: 'Foksterier', filename: 'obraz10.jpg', category: 2, downloads: 22 },
{ id: 10, alt: 'Szczeniak', filename: 'obraz11.jpg', category: 2, downloads: 12 },
{ id: 11, alt: 'Garbus', filename: 'obraz12.jpg', category: 3, downloads: 321 },
];
function App() {
const [zdjecia, setZdjecia] = useState(ZDJECIA);
const [kwiaty, setKwiaty] = useState(true);
const [zwierzeta, setZwierzeta] = useState(true);
const [samochody, setSamochody] = useState(true);
function czyWidoczne(kategoria) {
if (kategoria === 1) return kwiaty;
if (kategoria === 2) return zwierzeta;
if (kategoria === 3) return samochody;
return false;
}
function pobierz(id) {
setZdjecia(zdjecia.map((zdjecie) =>
zdjecie.id === id ? { ...zdjecie, downloads: zdjecie.downloads + 1 } : zdjecie
));
}
return (
<div>
<h1>Kategorie zdjęć</h1>
<div className="form-check form-switch form-check-inline">
<input className="form-check-input" type="checkbox" id="kwiaty"
checked={kwiaty} onChange={(e) => setKwiaty(e.target.checked)} />
<label className="form-check-label" htmlFor="kwiaty">Kwiaty</label>
</div>
<div className="form-check form-switch form-check-inline">
<input className="form-check-input" type="checkbox" id="zwierzeta"
checked={zwierzeta} onChange={(e) => setZwierzeta(e.target.checked)} />
<label className="form-check-label" htmlFor="zwierzeta">Zwierzęta</label>
</div>
<div className="form-check form-switch form-check-inline">
<input className="form-check-input" type="checkbox" id="samochody"
checked={samochody} onChange={(e) => setSamochody(e.target.checked)} />
<label className="form-check-label" htmlFor="samochody">Samochody</label>
</div>
<div className="d-flex flex-wrap">
{zdjecia.map((zdjecie) => czyWidoczne(zdjecie.category) && (
<div key={zdjecie.id}>
<img src={'/assets/' + zdjecie.filename} alt={zdjecie.alt} />
<h4>Pobrań: {zdjecie.downloads}</h4>
<button className="btn btn-success" onClick={() => pobierz(zdjecie.id)}>Pobierz</button>
</div>
))}
</div>
</div>
);
}
export default App;
src/App.css
img {
margin: 5px;
border-radius: 8px;
}
src/main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
Jak to czytać
- Cztery stany: tablica zdjęć i trzy przełączniki.
maprysuje blok dla każdego zdjęcia, aczyWidoczne(...) &&pomija zdjęcia z wyłączonej kategorii.pobierzbuduje nową tablicę z nowym obiektem klikniętego zdjęcia, więc licznik zmienia się od razu i zostaje po ukryciu kategorii.
Wariant na powtórkę w domu
Dopisz pod przełącznikami akapit „Widocznych zdjęć: N, łącznie pobrań: M”, liczony z aktualnie widocznych bloków. Bez nowego stanu: obie liczby wynikają z zdjecia i trzech przełączników.
Galeria zdjęć — Sprawdzian
5 pytań · 5 minut · minimum 60%, żeby zdać
public/assets. Jaki src ma zdjęcie?🔥 Rozgrzewka — Lista zadań
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
useState(wartośćPoczątkowa)?useState zwraca tablicę z dwoma elementami: aktualną wartością stanu oraz funkcją do jej zmiany. Zwykle destrukturyzujemy to tak: const [lista, setLista] = useState([]). Funkcja setLista powoduje ponowne wyrenderowanie komponentu z nową wartością.<form> dodajemy atrybut onSubmit={handleSubmit}. Wewnątrz funkcji handleSubmit wywołujemy e.preventDefault(), co blokuje domyślne przeładowanie strony. Następnie wykonujemy naszą logikę, np. dodanie elementu do stanu.push) — tworzymy nową za pomocą operatora spread: setLista([...lista, nowyElement]). Dzięki temu React wykrywa zmianę i przerenderowuje komponent. Aby dodać na początek: setLista([nowyElement, ...lista]).value={tekst} oraz onChange={e => setTekst(e.target.value)}. Dzięki temu React jest „jedynym źródłem prawdy" o zawartości pola — możemy ją walidować lub czyścić w dowolnym momencie.onChange na elemencie <input>?onChange jest wywoływane przy każdej zmianie wartości pola — czyli po każdym wpisanym lub usuniętym znaku. Obiekt zdarzenia e zawiera e.target.value z aktualną zawartością inputa. Typowy wzorzec: <input onChange={e => setTekst(e.target.value)} />.#02 — Lista zadań (Todo App)
Ćwiczenie autorskie w stylu egzaminacyjnym — uczy nowych koncepcji brakujących w #01.
Nowe umiejętności (vs #01 Galeria)
Treść zadania (styl egzaminacyjny)
Z zastosowaniem biblioteki React.js wykonaj aplikację internetową typu front-end realizującą funkcję listy zadań do zrobienia.
Założenia aplikacji
- Aplikacja składa się z jednego komponentu
- Dane początkowe — tablica 4 obiektów (poniżej), każdy zawiera:
- id (unikalna liczba)
- tytul (tekst zadania)
- priorytet (1 = niski, 2 = średni, 3 = wysoki)
- zrobione (true/false)
const poczatkowe = [
{ id: 1, tytul: 'Kupić mleko', priorytet: 1, zrobione: false },
{ id: 2, tytul: 'Zrobić projekt React', priorytet: 3, zrobione: false },
{ id: 3, tytul: 'Odpowiedzieć na maila', priorytet: 2, zrobione: true },
{ id: 4, tytul: 'Posprzątać biurko', priorytet: 1, zrobione: false },
]
Komponent składa się z
- Nagłówka h1: „Lista zadań"
- Formularza dodawania: pole tekstowe (controlled input), select z opcjami priorytetu, przycisk „Dodaj"
- Tabeli Bootstrap (
table table-striped) z kolumnami: Zadanie, Priorytet, Status, Akcje - W kolumnie Priorytet: badge Bootstrap (zielony=niski, żółty=średni, czerwony=wysoki)
- W kolumnie Status: tekst „Zrobione" / „Do zrobienia"
- W kolumnie Akcje: przycisk „✓ Zrobione" (toggle) i przycisk „✕ Usuń"
Stylowanie
- Zadania zrobione: tekst przekreślony (
text-decoration: line-through) i wyszarzony (opacity: 0.5) - Formularz, tabela, badge, przyciski: stylowane Bootstrap
Logika
- Dodaj: wpisz tytuł + wybierz priorytet → kliknij „Dodaj" → nowe zadanie w tablicy, pole się czyści
- Nie dodawaj pustego zadania (walidacja)
- „✓ Zrobione" toggleuje pole
zrobione(true↔false) - „✕ Usuń" usuwa zadanie z tablicy
- Na dole: licznik — „Zrobione: X z Y"
- Znaczące nazwy zmiennych i funkcji
Bootstrap — przykłady klas
<table class="table table-striped">
<span class="badge bg-success">niski</span>
<span class="badge bg-warning text-dark">średni</span>
<span class="badge bg-danger">wysoki</span>
<input class="form-control" type="text">
<select class="form-select">
<button class="btn btn-success btn-sm">
<button class="btn btn-danger btn-sm">
Dlaczego to zadanie? #01 Galeria uczyła filtrowania i zmiany pola. #02 Todo uczy DODAWANIA, USUWANIA, TOGGLE i controlled input — kompletuje zestaw CRUD. Po obu zadaniach uczeń umie wszystko co potrzebne na egzaminie.
#02 — Wprowadzenie dla nauczyciela
1. Otwarcie
„Ostatnio zrobiliśmy galerię — filtrowanie i zmiana pola w obiekcie. Dzisiaj robimy listę zadań. Nauczysz się: dodawać nowe elementy, usuwać, i toggleować stan. Po tym będziesz umiał wszystko co potrzebne na egzaminie."
2. Diagnoza — co pamięta z #01
„Przypomnij mi — jak zmienialiśmy downloads w galerii? Dlaczego nie mogliśmy zrobić z.downloads++?"
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „Bo React porównuje referencje, trzeba nowy obiekt" | „Idealnie. Dzisiaj użyjemy tego samego wzorca + nowe." |
| „Coś ze spreadem?" | „Tak! {...z, pole: nowa} — dzisiaj to powtórzymy." |
| Nie pamięta | „Ok, wrócimy do tego przy toggleowaniu. Spokojnie." |
3. Architektura
Pokaż schemat:
App:
state: zadania → tablica obiektów {id, tytul, priorytet, zrobione}
state: nowyTytul → string (controlled input)
state: nowyPriorytet → number (controlled select)
dodaj() → [...zadania, nowyObiekt] ← NOWE
usun(id) → zadania.filter(...) ← NOWE
toggle(id)→ zadania.map(z => ...) ← jak pobierz() w galerii
JSX: h1 + formularz + tabela { zadania.map(...) } + licznik
„Ile useState będziemy potrzebować?"
Odp: 3 — zadania (tablica), nowyTytul (string), nowyPriorytet (number).
„W galerii zmienialiśmy istniejący obiekt (.map + spread). Tu mamy 3 operacje — dodanie, usunięie, zmiana. Jaka metoda tablicowa służy do: a) dodania? b) usunięcia? c) zmiany?"
Odp: a) spread [...arr, nowy], b) .filter(), c) .map() + spread
4. Częste blokady
| Problem | Podpowiedź |
|---|---|
| Nie wie jak controlled input | „value={stan} + onChange. Pamiętasz z modułu 6?" |
| Dodaje ale pole się nie czyści | „Po dodaniu co trzeba zrobić z nowyTytul?" |
| Nie wie jak generować ID | „Date.now() daje unikalną liczbę — ms od 1970." |
| Usuwa ale deleteuje cały stan | „.filter() ZWRACA nową tablicę. setZadania(zadania.filter(...))" |
| Toggle nie działa | „To jest map+spread jak w galerii. Zmień pole zrobione na !z.zrobione" |
| Badge nie ma koloru | „Sprawdź klasy: bg-success, bg-warning text-dark, bg-danger" |
| Przekreślenie nie działa | „Warunkowa klasa: className={z.zrobione ? 'zrobione' : ''}" |
5. Po zbudowaniu
„Porównaj galerię z listą zadań. Co mają wspólnego, a co innego?"
Odp: Wspólne: tablica w useState, .map() w JSX, spread do zmiany. Nowe: dodawanie ([...arr, x]), usuwanie (.filter), controlled input/select, toggle.
„Dlaczego Date.now() a nie Math.random() albo po prostu zadania.length + 1?"
Odp: length+1 się powtarza po usunięciu. Math.random() teoretycznie OK ale brzydkie. Date.now() jest prosty i unikalny (ms od 1970).
„Jak byś dodał filtrowanie — pokaż tylko niezrobione?"
Odp: Dodatkowy useState(false) + .filter(z => !filtr || !z.zrobione) — jak switche w galerii!
#02 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Jeden komponent | App.jsx | ✅ |
| Nagłówek h1 | „Lista zadań" | ✅ |
| Formularz dodawania | input (text) + select (priorytet) + button „Dodaj" | ✅ |
| Controlled input | value={nowyTytul} + onChange | ✅ |
| Controlled select | value={nowyPriorytet} + onChange + Number() | ✅ |
| Dane początkowe | 4 obiekty {id, tytul, priorytet, zrobione} | ✅ |
| Tabela Bootstrap | table table-striped, 4 kolumny | ✅ |
| Badge priorytet | bg-success (niski), bg-warning (średni), bg-danger (wysoki) | ✅ |
| Dodawanie zadania | [...zadania, nowe] + Date.now() jako id | ✅ |
| Walidacja | Nie dodaje pustego (trim === '') | ✅ |
| Toggle zrobione | .map() + spread + !z.zrobione | ✅ |
| Usuwanie zadania | .filter(z => z.id !== id) | ✅ |
| CSS — przekreślenie | text-decoration: line-through + opacity: 0.5 | ✅ |
| Warunkowe klasy | className={z.zrobione ? 'zrobione' : ''} | ✅ |
| Licznik | „Zrobione: X z Y" — filter().length | ✅ |
| Znaczące nazwy | zadania, nowyTytul, dodaj, toggle, usun | ✅ |
#02 — Budowa krok po kroku
„Możemy użyć tego samego projektu Vite — wystarczy wyczyścić App.jsx i App.css."
# Jeśli nowy projekt:
npm create vite@latest todo -- --template react
cd todo
npm install
npm install bootstrap
npm run dev
„Jak w galerii — importy, dane poza komponentem, useState."
import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
import './App.css'
const poczatkowe = [
{ id: 1, tytul: 'Kupić mleko', priorytet: 1, zrobione: false },
{ id: 2, tytul: 'Zrobić projekt React', priorytet: 3, zrobione: false },
{ id: 3, tytul: 'Odpowiedzieć na maila', priorytet: 2, zrobione: true },
{ id: 4, tytul: 'Posprzątać biurko', priorytet: 1, zrobione: false },
]
function App() {
const [zadania, setZadania] = useState(poczatkowe)
return (
<div className="container mt-3">
<h1>Lista zadań</h1>
</div>
)
}
export default App
„Porównaj z galerią — widzisz ten sam wzorzec? Tablica poza komponentem, useState wewnątrz."
„Tu jest nowość — controlled input dla tekstu i select dla priorytetu. Pamiętasz formułkę? value={stan} + onChange."
Stan:
const [nowyTytul, setNowyTytul] = useState('')
const [nowyPriorytet, setNowyPriorytet] = useState(2)
JSX:
<div className="d-flex gap-2 mb-4">
<input
className="form-control"
type="text"
placeholder="Wpisz zadanie..."
value={nowyTytul}
onChange={(e) => setNowyTytul(e.target.value)}
/>
<select
className="form-select"
style={{ width: '180px' }}
value={nowyPriorytet}
onChange={(e) => setNowyPriorytet(Number(e.target.value))}
>
<option value={1}>Niski</option>
<option value={2}>Średni</option>
<option value={3}>Wysoki</option>
</select>
<button className="btn btn-primary">Dodaj</button>
</div>
„Dlaczego Number(e.target.value) w select?"
Odp: e.target.value ZAWSZE zwraca string. Priorytet to number → musimy skonwertować.
„Pamiętasz co to controlled input? Które dwa atrybuty musi mieć?"
Odp: value={stan} + onChange={handler}. React kontroluje pole.
„W galerii zmienialiśmy obiekt w tablicy. Teraz DODAJEMY nowy. Jak to zrobisz bez mutacji?"
„Masz tablicę [a, b, c]. Jak dodasz d na koniec bez push()?"
Odp: [...tablica, d] — spread + nowy element.
function dodaj() {
if (nowyTytul.trim() === '') return
const nowe = {
id: Date.now(),
tytul: nowyTytul,
priorytet: nowyPriorytet,
zrobione: false,
}
setZadania([...zadania, nowe])
setNowyTytul('')
}
Podepnij do przycisku:
<button className="btn btn-primary" onClick={dodaj}>Dodaj</button>
„Dlaczego po dodaniu robimy setNowyTytul('')?"
Odp: Czyścimy pole po dodaniu — lepszy UX.
„Dlaczego Date.now() jako id?"
Odp: Daje unikalną liczbę — milisekundy od 1970. Nie powtarza się.
Jeśli pisze zadania.push(nowe):
„push() mutuje tablicę. React tego nie zauważy! Trzeba stworzyć NOWĄ tablicę: [...zadania, nowe]"
„Teraz wyświetlamy zadania w tabeli Bootstrap. .map() jak w galerii — zamiast div z obrazem, robimy <tr> z kolumnami."
<table className="table table-striped">
<thead>
<tr>
<th>Zadanie</th>
<th>Priorytet</th>
<th>Status</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{zadania.map((z) => (
<tr key={z.id} className={z.zrobione ? 'zrobione' : ''}>
<td>{z.tytul}</td>
<td>
{z.priorytet === 1 && <span className="badge bg-success">niski</span>}
{z.priorytet === 2 && <span className="badge bg-warning text-dark">średni</span>}
{z.priorytet === 3 && <span className="badge bg-danger">wysoki</span>}
</td>
<td>{z.zrobione ? 'Zrobione' : 'Do zrobienia'}</td>
<td>
<button className="btn btn-success btn-sm me-1">✓</button>
<button className="btn btn-danger btn-sm">✕</button>
</td>
</tr>
))}
</tbody>
</table>
„Co robi className={z.zrobione ? 'zrobione' : ''}?"
Odp: Warunkowa klasa CSS — jeśli zrobione, dodaje klasę 'zrobione'. W CSS nadamy jej styl.
„Jak działają trzy warunki z badge? Co zwraca {z.priorytet === 1 && <span>}?"
Odp: Short-circuit: jeśli lewe jest true → renderuje prawe. Jeśli false → nic.
„Dwie funkcje. toggle() to ten sam wzorzec co pobierz() w galerii — .map() + spread. usun() to nowość — .filter()."
function toggle(id) {
setZadania(zadania.map((z) =>
z.id === id ? { ...z, zrobione: !z.zrobione } : z
))
}
function usun(id) {
setZadania(zadania.filter((z) => z.id !== id))
}
Podepnij do przycisków:
<button className="btn btn-success btn-sm me-1"
onClick={() => toggle(z.id)}>✓</button>
<button className="btn btn-danger btn-sm"
onClick={() => usun(z.id)}>✕</button>
„toggle() — rozpoznajesz wzorzec z galerii? Co jest inne?"
Odp: To samo: map + spread + ternary. Ale zamiast downloads+1 robimy !z.zrobione (negacja boolean).
„usun() — co zwraca .filter(z => z.id !== id)?"
Odp: Nową tablicę BEZ elementu o danym id. !== znaczy: zachowaj wszystkie OPRÓCZ tego.
Jeśli pisze splice/delete:
„splice() mutuje! .filter() zwraca NOWĄ tablicę — React to zobaczy."
„Ostatnie szlify — styl dla zrobionych zadań i licznik na dole."
App.css:
.zrobione td {
text-decoration: line-through;
opacity: 0.5;
}
Licznik (przed zamknięciem </div>):
<p className="mt-3 text-muted">
Zrobione: {zadania.filter((z) => z.zrobione).length} z {zadania.length}
</p>
„Jak działa ten licznik? Co robi .filter().length?"
Odp: filter zwraca tablicę tylko zrobionych. .length liczy ile ich jest. Computed value — przeliczane przy każdym renderze.
„Gotowe! Masz pełną aplikację Todo — dodawanie, usuwanie, toggle, filtrowanie, badge, tabela. Wszystkie operacje CRUD."
#02 — Gotowy kod
App.jsx
import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
import './App.css'
const poczatkowe = [
{ id: 1, tytul: 'Kupić mleko', priorytet: 1, zrobione: false },
{ id: 2, tytul: 'Zrobić projekt React', priorytet: 3, zrobione: false },
{ id: 3, tytul: 'Odpowiedzieć na maila', priorytet: 2, zrobione: true },
{ id: 4, tytul: 'Posprzątać biurko', priorytet: 1, zrobione: false },
]
function App() {
const [zadania, setZadania] = useState(poczatkowe)
const [nowyTytul, setNowyTytul] = useState('')
const [nowyPriorytet, setNowyPriorytet] = useState(2)
function dodaj() {
if (nowyTytul.trim() === '') return
const nowe = {
id: Date.now(),
tytul: nowyTytul,
priorytet: nowyPriorytet,
zrobione: false,
}
setZadania([...zadania, nowe])
setNowyTytul('')
}
function toggle(id) {
setZadania(zadania.map((z) =>
z.id === id ? { ...z, zrobione: !z.zrobione } : z
))
}
function usun(id) {
setZadania(zadania.filter((z) => z.id !== id))
}
return (
<div className="container mt-3">
<h1>Lista zadań</h1>
<div className="d-flex gap-2 mb-4">
<input className="form-control" type="text" placeholder="Wpisz zadanie..."
value={nowyTytul} onChange={(e) => setNowyTytul(e.target.value)} />
<select className="form-select" style={{ width: '180px' }}
value={nowyPriorytet} onChange={(e) => setNowyPriorytet(Number(e.target.value))}>
<option value={1}>Niski</option>
<option value={2}>Średni</option>
<option value={3}>Wysoki</option>
</select>
<button className="btn btn-primary" onClick={dodaj}>Dodaj</button>
</div>
<table className="table table-striped">
<thead>
<tr><th>Zadanie</th><th>Priorytet</th><th>Status</th><th>Akcje</th></tr>
</thead>
<tbody>
{zadania.map((z) => (
<tr key={z.id} className={z.zrobione ? 'zrobione' : ''}>
<td>{z.tytul}</td>
<td>
{z.priorytet === 1 && <span className="badge bg-success">niski</span>}
{z.priorytet === 2 && <span className="badge bg-warning text-dark">średni</span>}
{z.priorytet === 3 && <span className="badge bg-danger">wysoki</span>}
</td>
<td>{z.zrobione ? 'Zrobione' : 'Do zrobienia'}</td>
<td>
<button className="btn btn-success btn-sm me-1" onClick={() => toggle(z.id)}>✓</button>
<button className="btn btn-danger btn-sm" onClick={() => usun(z.id)}>✕</button>
</td>
</tr>
))}
</tbody>
</table>
<p className="mt-3 text-muted">
Zrobione: {zadania.filter((z) => z.zrobione).length} z {zadania.length}
</p>
</div>
)
}
export default App
App.css
.zrobione td {
text-decoration: line-through;
opacity: 0.5;
}
📝 Sprawdzian — Lista zadań
5 pytań · 5 minut · minimum 60% żeby zdać
useState("")?e.preventDefault()?id z tablicy w stanie?🔥 Rozgrzewka — Tracker wydatków
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
useEffect i do czego służy?useEffect to hook pozwalający wykonać „efekt uboczny" po wyrenderowaniu komponentu — np. pobranie danych, zapis do localStorage czy ustawienie timera. Przyjmuje dwa argumenty: funkcję do wykonania oraz tablicę zależności, która kontroluje, kiedy efekt się uruchomi.localStorage w przeglądarce?localStorage pozwala zapisywać dane w przeglądarce, które przetrwają odświeżenie strony. Zapisujemy przez localStorage.setItem("klucz", JSON.stringify(dane)), a odczytujemy przez JSON.parse(localStorage.getItem("klucz")). Przechowuje tylko stringi, dlatego obiekty i tablice musimy serializować przez JSON..filter() i jak jej użyć do usunięcia elementu?.filter() tworzy nową tablicę zawierającą tylko elementy spełniające warunek. Aby usunąć element o danym id: setWydatki(wydatki.filter(w => w.id !== idDoUsuniecia)). Oryginalna tablica nie jest modyfikowana — to ważne w React..reduce() i jak obliczyć sumę wartości?.reduce() „redukuje" tablicę do pojedynczej wartości, przetwarzając elementy po kolei. Aby zsumować kwoty: wydatki.reduce((suma, w) => suma + w.kwota, 0). Drugi argument (0) to wartość początkowa akumulatora.useEffect uruchomił się tylko raz, przy pierwszym renderze?useEffect(() => { ... }, []). Pusta tablica oznacza, że efekt nie zależy od żadnej wartości, więc wykona się tylko po pierwszym wyrenderowaniu komponentu — idealnie do wczytania danych z localStorage.#03 — Tracker Wydatków (Expense Tracker)
Ćwiczenie autorskie w stylu egzaminacyjnym — uczy nowych koncepcji brakujących w #01 i #02.
Nowe umiejętności (vs #01 + #02)
Treść zadania (styl egzaminacyjny)
Z zastosowaniem biblioteki React.js wykonaj aplikację internetową typu front-end realizującą funkcję trackera wydatków osobistych.
Założenia aplikacji
- Aplikacja składa się z jednego komponentu
- Dane początkowe — tablica 5 obiektów (poniżej), każdy zawiera:
- id (unikalna liczba)
- nazwa (opis wydatku)
- kwota (liczba zmiennoprzecinkowa, np. 127.50)
- kategoria (string: „jedzenie" / „transport" / „rozrywka" / „edukacja")
const poczatkowe = [
{ id: 1, nazwa: 'Zakupy spożywcze', kwota: 127.50, kategoria: 'jedzenie' },
{ id: 2, nazwa: 'Bilet miesięczny', kwota: 99.00, kategoria: 'transport' },
{ id: 3, nazwa: 'Netflix', kwota: 43.00, kategoria: 'rozrywka' },
{ id: 4, nazwa: 'Obiad w restauracji', kwota: 65.00, kategoria: 'jedzenie' },
{ id: 5, nazwa: 'Książka React', kwota: 89.00, kategoria: 'edukacja' },
]
Komponent składa się z
- Nagłówka h1: „Tracker wydatków"
- Karty podsumowania (
Bootstrap card): łączna suma wydatków, liczba pozycji, średnia kwota - Pola wyszukiwania: input tekstowy filtrujący wydatki po nazwie (case-insensitive)
- Formularza dodawania: pole tekstowe (nazwa), pole liczbowe (kwota, type="number"), select (kategoria), przycisk „Dodaj"
- Tabeli Bootstrap (
table table-hover) z kolumnami: Nazwa, Kwota, Kategoria, Akcje - W kolumnie Kategoria: badge Bootstrap (zielony=jedzenie, niebieski=transport, fioletowy=rozrywka, żółty=edukacja)
- W kolumnie Akcje: przycisk „Usuń" (btn-danger)
- Alert Bootstrap (warning): wyświetlany gdy suma wydatków przekroczy 1000 zł
Stylowanie
- Karta podsumowania: Bootstrap
card, wyśrodkowane wartości - Kwoty wyświetlane z 2 miejscami po przecinku i dopiskiem „zł"
- Formularz, tabela, badge, przyciski, alert: stylowane Bootstrap
Logika
- Dodaj: wpisz nazwę + kwotę + wybierz kategorię → kliknij „Dodaj" → nowy wydatek, pola się czyszczą
- Nie dodawaj pustej nazwy ani kwoty ≤ 0 (walidacja)
- „Usuń" — usuwa wydatek z tablicy
- Wyszukiwarka filtruje po nazwie (case-insensitive,
.toLowerCase().includes()) - Suma:
.reduce()— oblicza łączną kwotę, liczbę pozycji, średnią - Alert: jeśli suma > 1000 → Bootstrap alert-warning
- Znaczące nazwy zmiennych i funkcji
Bootstrap — przykłady klas
<div class="card">
<div class="card-body">
<h5 class="card-title">...</h5>
<p class="card-text">...</p>
</div>
</div>
<div class="alert alert-warning">Uwaga!</div>
<input class="form-control" type="number" step="0.01">
<span class="badge bg-success">jedzenie</span>
Dlaczego to zadanie? #01 uczyła filtrowania + zmiany pola, #02 uczyła CRUD (dodawanie/usuwanie/toggle). #03 uczy OBLICZEŃ na tablicy (.reduce), wyszukiwarki tekstowej, number input i warunkowego renderowania alertu. Po trzech zadaniach uczeń pokrywa 100% wzorców potrzebnych na egzaminie.
#03 — Wprowadzenie dla nauczyciela
1. Otwarcie
„W galerii robiliśmy filtrowanie i zmianę pola. W liście zadań — dodawanie, usuwanie, toggle. Dziś robimy tracker wydatków, który uczy OBLICZEŃ na tablicy — sumowania, średniej — i wyszukiwarki tekstowej. To ostatni brakujący klocek."
2. Diagnoza — co pamięta z #01 i #02
„Szybkie powtórzenie. Jak dodajesz element do tablicy w React bez mutacji?"
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „[...zadania, nowy]" | „Idealnie — dzisiaj użyjemy tego samego." |
| „setZadania coś tam..." | „Tak, a wewnątrz: spread + nowy element. Powtórzymy." |
| Nie pamięta | „Ok, wrócimy do tego. To ten sam wzorzec co w Todo." |
„A jak usuwałeś element?"
Odp: .filter(z => z.id !== id)
3. Architektura
Pokaż schemat:
App:
state: wydatki → tablica obiektów {id, nazwa, kwota, kategoria}
state: nowaNazwa → string (controlled input)
state: nowaKwota → string (controlled input type number)
state: nowaKategoria → string (controlled select)
state: szukaj → string (wyszukiwarka)
computed: widoczne = wydatki.filter(w => w.nazwa.toLowerCase().includes(...))
computed: suma = wydatki.reduce((s, w) => s + w.kwota, 0) ← NOWE!
computed: srednia = suma / wydatki.length
dodaj() → [...wydatki, nowy] + parseFloat(nowaKwota)
usun(id) → wydatki.filter(...)
JSX: h1 + card(suma) + wyszukiwarka + formularz + tabela + alert(suma>1000)
„Ile useState tym razem?"
Odp: 5 — wydatki (tablica), nowaNazwa (string), nowaKwota (string), nowaKategoria (string), szukaj (string).
„Znasz z JS metodę, która przerabia całą tablicę w JEDNĄ wartość? Np. sumuje wszystkie kwoty?"
Odp: .reduce() — to nowość. Jeśli nie zna, wytłumacz na następnym kroku.
4. Mini-lekcja: .reduce()
„.reduce() bierze tablicę i 'redukuje' ją do jednej wartości. Np. sumuje."
// Pseudokod:
[10, 20, 30].reduce((suma, el) => suma + el, 0)
// Krok 1: suma=0, el=10 → return 10
// Krok 2: suma=10, el=20 → return 30
// Krok 3: suma=30, el=30 → return 60
// Wynik: 60
„Co robi to '0' na końcu?"
Odp: Wartość startowa akumulatora. Zaczynamy sumę od 0.
5. Częste blokady
| Problem | Podpowiedź |
|---|---|
| Nie zna .reduce() | „Pokaż na prostym przykładzie: [1,2,3].reduce((s,n)=>s+n, 0)" |
| Kwota jest stringiem | „input type='number' zwraca string! parseFloat() zamienia na liczbę." |
| .toFixed() nie działa | „.toFixed(2) działa tylko na number, nie string." |
| Wyszukiwarka case-sensitive | „.toLowerCase() na obu stronach — szukanym i porównywanym." |
| Alert zawsze widoczny | „Warunkowe renderowanie: {suma > 1000 && <div...>}" |
| NaN przy dodawaniu | „parseFloat('') daje NaN. Dlatego walidacja: if (!nowaKwota || ...)" |
| Dzielenie przez 0 | „Gdy tablica pusta, wydatki.length === 0. Sprawdź przed dzieleniem!" |
6. Po zbudowaniu
„Porównaj trzy zadania: galeria, todo, tracker. Co mają wspólnego, a co nowego?"
Odp: Wspólne: tablica + .map() + .filter(). Galeria: zmiana pola (switch+downloads). Todo: dodawanie/usuwanie/toggle. Tracker: reduce (obliczenia), wyszukiwarka, number input, warunkowy alert.
„Gdybyś chciał dodać sortowanie — np. od najdroższego — jak?"
Odp: [...widoczne].sort((a,b) => b.kwota - a.kwota) — sortuj kopię, nie oryginał.
#03 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Jeden komponent | App.jsx | ✅ |
| Nagłówek h1 | „Tracker wydatków" | ✅ |
| Dane początkowe | 5 obiektów {id, nazwa, kwota, kategoria} | ✅ |
| Formularz dodawania | input text + input number + select + button | ✅ |
| Controlled input (text) | value={nowaNazwa} + onChange | ✅ |
| Controlled input (number) | value={nowaKwota} + onChange + parseFloat() | ✅ |
| Controlled select | value={nowaKategoria} + onChange | ✅ |
| Walidacja | Nie dodaje pustej nazwy ani kwoty ≤ 0 | ✅ |
| Wyszukiwarka | .toLowerCase().includes() — case-insensitive | ✅ |
| Tabela Bootstrap | table table-hover, 4 kolumny | ✅ |
| Badge kategoria | 4 kolory — jedzenie/transport/rozrywka/edukacja | ✅ |
| Usuwanie | .filter(w => w.id !== id) | ✅ |
| Karta podsumowania | Bootstrap card — suma, ilość, średnia | ✅ |
| .reduce() suma | wydatki.reduce((s, w) => s + w.kwota, 0) | ✅ |
| .toFixed(2) | Kwoty z 2 miejscami po przecinku + „zł" | ✅ |
| Alert warunkowy | suma > 1000 → alert-warning | ✅ |
| Znaczące nazwy | wydatki, nowaNazwa, dodaj, usun, szukaj, suma | ✅ |
#03 — Budowa krok po kroku
„Możemy użyć tego samego projektu — wyczyść App.jsx i App.css."
# Jeśli nowy:
npm create vite@latest tracker -- --template react
cd tracker
npm install
npm install bootstrap
npm run dev
„Ten sam schemat co galeria i todo — importy, dane poza komponentem, useState."
import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
import './App.css'
const poczatkowe = [
{ id: 1, nazwa: 'Zakupy spożywcze', kwota: 127.50, kategoria: 'jedzenie' },
{ id: 2, nazwa: 'Bilet miesięczny', kwota: 99.00, kategoria: 'transport' },
{ id: 3, nazwa: 'Netflix', kwota: 43.00, kategoria: 'rozrywka' },
{ id: 4, nazwa: 'Obiad w restauracji', kwota: 65.00, kategoria: 'jedzenie' },
{ id: 5, nazwa: 'Książka React', kwota: 89.00, kategoria: 'edukacja' },
]
function App() {
const [wydatki, setWydatki] = useState(poczatkowe)
return (
<div className="container mt-3">
<h1>Tracker wydatków</h1>
</div>
)
}
export default App
„Widzisz wzorzec? Trzeci raz z rzędu — dane na zewnątrz, useState w środku. Dlaczego zawsze tak robimy?"
Odp: Dane początkowe się nie zmieniają — nie muszą być w komponencie. useState kopiuje je jako punkt startowy.
„Formularz jak w Todo, ale teraz mamy trzy pola — text, number i select. I nowość: input type='number'."
Stan:
const [nowaNazwa, setNowaNazwa] = useState('')
const [nowaKwota, setNowaKwota] = useState('')
const [nowaKategoria, setNowaKategoria] = useState('jedzenie')
JSX:
<div className="d-flex gap-2 mb-4">
<input className="form-control" type="text" placeholder="Nazwa wydatku..."
value={nowaNazwa} onChange={(e) => setNowaNazwa(e.target.value)} />
<input className="form-control" type="number" step="0.01" placeholder="Kwota"
style={{ width: '150px' }}
value={nowaKwota} onChange={(e) => setNowaKwota(e.target.value)} />
<select className="form-select" style={{ width: '180px' }}
value={nowaKategoria} onChange={(e) => setNowaKategoria(e.target.value)}>
<option value="jedzenie">Jedzenie</option>
<option value="transport">Transport</option>
<option value="rozrywka">Rozrywka</option>
<option value="edukacja">Edukacja</option>
</select>
<button className="btn btn-primary">Dodaj</button>
</div>
„Dlaczego nowaKwota to string ('') a nie number (0)?"
Odp: Bo controlled input z type='number' nadal operuje na stringach w value. Konwertujemy na number przy dodawaniu (parseFloat).
„Co robi step='0.01' w input number?"
Odp: Pozwala wpisywać grosze — 127.50 zamiast tylko 127.
„Wzorzec znany z Todo. Ale tu mamy dwie walidacje i konwersję stringa na liczbę."
function dodaj() {
if (nowaNazwa.trim() === '') return
if (!nowaKwota || parseFloat(nowaKwota) <= 0) return
const nowy = {
id: Date.now(),
nazwa: nowaNazwa,
kwota: parseFloat(nowaKwota),
kategoria: nowaKategoria,
}
setWydatki([...wydatki, nowy])
setNowaNazwa('')
setNowaKwota('')
}
Podepnij do przycisku:
<button className="btn btn-primary" onClick={dodaj}>Dodaj</button>
„Dlaczego parseFloat() a nie Number()?"
Odp: Oba działają. parseFloat jest bardziej tolerancyjny — ignoruje znaki na końcu. Ale Number() też ok.
Jeśli zapomni konwersji:
„Dodaj wydatek 50 + 30. Ile wyszło? '5030'? To nie suma — to konkatenacja stringów. parseFloat()!"
„Tabela jak w Todo — .map() na tablicy, badge jak z priorytetami."
Funkcja usun (znana z Todo):
function usun(id) {
setWydatki(wydatki.filter((w) => w.id !== id))
}
Helper dla kolorów badge:
function kolorBadge(kat) {
if (kat === 'jedzenie') return 'bg-success'
if (kat === 'transport') return 'bg-primary'
if (kat === 'rozrywka') return 'bg-purple'
if (kat === 'edukacja') return 'bg-warning text-dark'
return 'bg-secondary'
}
JSX tabeli:
<table className="table table-hover">
<thead>
<tr><th>Nazwa</th><th>Kwota</th><th>Kategoria</th><th>Akcje</th></tr>
</thead>
<tbody>
{wydatki.map((w) => (
<tr key={w.id}>
<td>{w.nazwa}</td>
<td>{w.kwota.toFixed(2)} zł</td>
<td><span className={`badge ${kolorBadge(w.kategoria)}`}>{w.kategoria}</span></td>
<td><button className="btn btn-danger btn-sm" onClick={() => usun(w.id)}>Usuń</button></td>
</tr>
))}
</tbody>
</table>
„Co robi .toFixed(2)?"
Odp: Formatuje liczbę do 2 miejsc po przecinku. 99 → „99.00", 127.5 → „127.50".
„Template literal w className: \`badge \${kolorBadge(w.kategoria)}\`. Dlaczego backticki?"
Odp: Łączymy stały tekst „badge " ze zmienną wartością. Template literal = backticki + ${wyrażenie}.
„Nowość: filtrowanie po tekście. W galerii filtrowaliśmy po kategorii (number). Tu filtrujemy po nazwie — string."
Stan:
const [szukaj, setSzukaj] = useState('')
Filtr:
const widoczne = wydatki.filter((w) =>
w.nazwa.toLowerCase().includes(szukaj.toLowerCase())
)
JSX (nad formularzem):
<input className="form-control mb-3" type="text" placeholder="🔍 Szukaj wydatku..."
value={szukaj} onChange={(e) => setSzukaj(e.target.value)} />
Zmień .map() w tabeli: zamień wydatki.map na widoczne.map
„Dlaczego .toLowerCase() na OBU stronach?"
Odp: „Netflix" vs „netflix". Bez toLowerCase() szukanie „net" nie znajdzie „Netflix". Porównujemy małe z małymi.
„Co robi .includes()?"
Odp: Sprawdza czy string zawiera podciąg. 'netflix'.includes('net') → true. 'netflix'.includes('xyz') → false.
Jeśli szukanie nie działa:
„Wypisz w konsoli co porównujesz: console.log(w.nazwa.toLowerCase(), szukaj.toLowerCase())"
„Najtrudniejszy krok — .reduce(). Bierze tablicę i zwraca JEDNĄ wartość. Np. sumę."
„Analogia: masz stos paragonów. Bierzesz kalkulator (zaczynasz od 0), dodajesz kwotę z każdego paragonu, na końcu masz sumę."
const suma = wydatki.reduce((s, w) => s + w.kwota, 0)
const srednia = wydatki.length > 0 ? suma / wydatki.length : 0
JSX (pod h1, nad wyszukiwarką):
<div className="card mb-4">
<div className="card-body d-flex justify-content-around text-center">
<div>
<h5 className="card-title">Suma</h5>
<p className="card-text fs-4">{suma.toFixed(2)} zł</p>
</div>
<div>
<h5 className="card-title">Pozycji</h5>
<p className="card-text fs-4">{wydatki.length}</p>
</div>
<div>
<h5 className="card-title">Średnia</h5>
<p className="card-text fs-4">{srednia.toFixed(2)} zł</p>
</div>
</div>
</div>
„Wytłumacz mi co robi: wydatki.reduce((s, w) => s + w.kwota, 0)"
| Krok | s (akumulator) | w.kwota | wynik |
|---|---|---|---|
| start | 0 | — | — |
| 1 | 0 | 127.50 | 127.50 |
| 2 | 127.50 | 99.00 | 226.50 |
| 3 | 226.50 | 43.00 | 269.50 |
| 4 | 269.50 | 65.00 | 334.50 |
| 5 | 334.50 | 89.00 | 423.50 |
„Dlaczego wydatki.length > 0 przy średniej?"
Odp: Dzielenie przez 0! Jeśli tablica pusta → NaN. Zabezpieczamy.
„Warunkowe renderowanie — alert pojawia się TYLKO gdy suma > 1000."
{suma > 1000 && (
<div className="alert alert-warning">
⚠️ Uwaga! Przekroczono budżet 1000 zł — suma: {suma.toFixed(2)} zł
</div>
)}
App.css:
.bg-purple {
background-color: #6f42c1;
color: white;
}
„Jak działa {suma > 1000 && <div>...</div>}?"
Odp: Short-circuit: jeśli lewe jest false → nic się nie renderuje. Jeśli true → renderuje prawe. Ten sam wzorzec co badge w Todo.
„Przetestujmy! Dodaj parę drogich wydatków — jak suma przekroczy 1000 → alert się pojawi."
#03 — Gotowy kod
App.jsx
import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
import './App.css'
const poczatkowe = [
{ id: 1, nazwa: 'Zakupy spożywcze', kwota: 127.50, kategoria: 'jedzenie' },
{ id: 2, nazwa: 'Bilet miesięczny', kwota: 99.00, kategoria: 'transport' },
{ id: 3, nazwa: 'Netflix', kwota: 43.00, kategoria: 'rozrywka' },
{ id: 4, nazwa: 'Obiad w restauracji', kwota: 65.00, kategoria: 'jedzenie' },
{ id: 5, nazwa: 'Książka React', kwota: 89.00, kategoria: 'edukacja' },
]
function App() {
const [wydatki, setWydatki] = useState(poczatkowe)
const [nowaNazwa, setNowaNazwa] = useState('')
const [nowaKwota, setNowaKwota] = useState('')
const [nowaKategoria, setNowaKategoria] = useState('jedzenie')
const [szukaj, setSzukaj] = useState('')
const widoczne = wydatki.filter((w) =>
w.nazwa.toLowerCase().includes(szukaj.toLowerCase())
)
const suma = wydatki.reduce((s, w) => s + w.kwota, 0)
const srednia = wydatki.length > 0 ? suma / wydatki.length : 0
function dodaj() {
if (nowaNazwa.trim() === '') return
if (!nowaKwota || parseFloat(nowaKwota) <= 0) return
const nowy = {
id: Date.now(),
nazwa: nowaNazwa,
kwota: parseFloat(nowaKwota),
kategoria: nowaKategoria,
}
setWydatki([...wydatki, nowy])
setNowaNazwa('')
setNowaKwota('')
}
function usun(id) {
setWydatki(wydatki.filter((w) => w.id !== id))
}
function kolorBadge(kat) {
if (kat === 'jedzenie') return 'bg-success'
if (kat === 'transport') return 'bg-primary'
if (kat === 'rozrywka') return 'bg-purple'
if (kat === 'edukacja') return 'bg-warning text-dark'
return 'bg-secondary'
}
return (
<div className="container mt-3">
<h1>Tracker wydatków</h1>
<div className="card mb-4">
<div className="card-body d-flex justify-content-around text-center">
<div>
<h5 className="card-title">Suma</h5>
<p className="card-text fs-4">{suma.toFixed(2)} zł</p>
</div>
<div>
<h5 className="card-title">Pozycji</h5>
<p className="card-text fs-4">{wydatki.length}</p>
</div>
<div>
<h5 className="card-title">Średnia</h5>
<p className="card-text fs-4">{srednia.toFixed(2)} zł</p>
</div>
</div>
</div>
{suma > 1000 && (
<div className="alert alert-warning">
⚠️ Uwaga! Przekroczono budżet 1000 zł — suma: {suma.toFixed(2)} zł
</div>
)}
<input className="form-control mb-3" type="text" placeholder="🔍 Szukaj wydatku..."
value={szukaj} onChange={(e) => setSzukaj(e.target.value)} />
<div className="d-flex gap-2 mb-4">
<input className="form-control" type="text" placeholder="Nazwa wydatku..."
value={nowaNazwa} onChange={(e) => setNowaNazwa(e.target.value)} />
<input className="form-control" type="number" step="0.01" placeholder="Kwota"
style={{ width: '150px' }}
value={nowaKwota} onChange={(e) => setNowaKwota(e.target.value)} />
<select className="form-select" style={{ width: '180px' }}
value={nowaKategoria} onChange={(e) => setNowaKategoria(e.target.value)}>
<option value="jedzenie">Jedzenie</option>
<option value="transport">Transport</option>
<option value="rozrywka">Rozrywka</option>
<option value="edukacja">Edukacja</option>
</select>
<button className="btn btn-primary" onClick={dodaj}>Dodaj</button>
</div>
<table className="table table-hover">
<thead>
<tr><th>Nazwa</th><th>Kwota</th><th>Kategoria</th><th>Akcje</th></tr>
</thead>
<tbody>
{widoczne.map((w) => (
<tr key={w.id}>
<td>{w.nazwa}</td>
<td>{w.kwota.toFixed(2)} zł</td>
<td><span className={`badge ${kolorBadge(w.kategoria)}`}>{w.kategoria}</span></td>
<td><button className="btn btn-danger btn-sm" onClick={() => usun(w.id)}>Usuń</button></td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default App
App.css
.bg-purple {
background-color: #6f42c1;
color: white;
}
📝 Sprawdzian — Tracker wydatków
5 pytań · 5 minut · minimum 60% żeby zdać
[] jako drugi argument useEffect?localStorage?.reduce((suma, el) => suma + el.kwota, 0) na tablicy wydatków?useEffect z zależnością [wydatki]?🔥 Rozgrzewka — Dziennik ocen
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
.find() na tablicy?.find() zwraca pierwszy element tablicy spełniający podany warunek — lub undefined, jeśli takiego nie ma. Przykład: uczniowie.find(u => u.id === szukaneId). W odróżnieniu od .filter(), zwraca pojedynczy obiekt, a nie tablicę.Set?new Set(tablica) tworzy zbiór unikalnych wartości. Aby uzyskać z powrotem tablicę: [...new Set(przedmioty)]. Przydaje się np. do wyciągnięcia listy unikalnych przedmiotów z tablicy ocen: [...new Set(oceny.map(o => o.przedmiot))]..sort() z funkcją porównującą: uczniowie.sort((a, b) => a.nazwisko.localeCompare(b.nazwisko)). Uwaga: .sort() modyfikuje oryginalną tablicę, więc w React najpierw tworzymy kopię: [...uczniowie].sort(...)..map() — tworzymy nową tablicę, w której zamieniamy tylko wybrany element: setOceny(oceny.map(o => o.id === edytowanaId ? { ...o, wartosc: nowaWartosc } : o)). Spread { ...o } kopiuje obiekt, a potem nadpisujemy zmienione pole..map() od .filter()?.map() przekształca każdy element tablicy i zwraca tablicę tej samej długości z nowymi wartościami. .filter() wybiera tylko elementy spełniające warunek — zwraca tablicę o tej samej lub mniejszej długości, bez zmieniania samych elementów. Często łączymy je w łańcuch: tablica.filter(...).map(...).#04 — Dziennik ocen
📄 Instrukcja egzaminacyjna
Treść zadania
Napisz aplikację Dziennik ocen w React + Bootstrap. Aplikacja wyświetla listę ocen w tabeli, pozwala dodawać nowe, edytować istniejące, usuwać, sortować po kolumnach i filtrować po przedmiocie.
Dane początkowe
const poczatkowe = [
{ id: 1, uczen: 'Anna Kowalska', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-15' },
{ id: 2, uczen: 'Jan Nowak', przedmiot: 'Fizyka', ocena: 2, data: '2025-01-16' },
{ id: 3, uczen: 'Anna Kowalska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-17' },
{ id: 4, uczen: 'Marta Wiśniewska', przedmiot: 'Matematyka', ocena: 3, data: '2025-01-18' },
{ id: 5, uczen: 'Jan Nowak', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-20' },
{ id: 6, uczen: 'Marta Wiśniewska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-22' },
]
Wymagania funkcjonalne
- Wyświetlanie ocen w tabeli z kolumnami: Uczeń, Przedmiot, Ocena (badge kolorowy), Data, Akcje
- Dodawanie nowej oceny — formularz: imię ucznia (text), przedmiot (text), ocena (select 1-6)
- Edycja istniejącej oceny — kliknięcie ✎ wypełnia formularz, przycisk zmienia tekst na „Zapisz"
- Usuwanie oceny przyciskiem ✕
- Sortowanie — kliknięcie w nagłówek kolumny sortuje rosnąco/malejąco (▲/▼)
- Filtrowanie po przedmiocie — select z dynamicznie generowanymi opcjami
- Karta podsumowania — liczba ocen, średnia (.toFixed(2)), liczba przedmiotów
- Alert ostrzegawczy gdy średnia < 3.0
Przykłady Bootstrap do użycia
| Element | Klasy Bootstrap |
|---|---|
| Tabela | table table-striped |
| Badge oceny | badge bg-danger / bg-warning / bg-success / bg-primary |
| Karta podsumowania | card, card-body, card-title |
| Alert | alert alert-danger |
| Przycisk dodaj | btn btn-primary |
| Przycisk zapisz (edycja) | btn btn-warning |
| Przycisk anuluj | btn btn-secondary |
#04 — Wprowadzenie do lekcji
Diagnoza — co już umiesz z #01–#03
| Umiejętność | Skąd znasz |
|---|---|
| useState + tablica obiektów | #01, #02, #03 |
| .map() → wyświetlanie listy | #01, #02, #03 |
| .filter() → usuwanie / filtrowanie | #02, #03 |
| spread [...arr, nowy] | #02, #03 |
| .reduce() → obliczanie sumy | #03 |
| Bootstrap tabela + badge | #02, #03 |
| Date.now() jako ID | #02, #03 |
Co nowego w #04
.find() i trybu edycji.| Nowa umiejętność | Do czego |
|---|---|
.find() | Znajduje obiekt w tablicy po id — żeby wypełnić formularz edycji |
edytowanyId state | Przechowuje id edytowanego elementu (null = tryb dodawania) |
.sort() | Sortowanie tablicy — klikalne nagłówki kolumn |
[...new Set()] | Wyciąga unikalne wartości (np. listę przedmiotów) |
new Date().toISOString().slice(0,10) | Generuje dzisiejszą datę w formacie YYYY-MM-DD |
| Warunkowy tekst przycisku | Ten sam przycisk mówi „Dodaj" lub „Zapisz" zależnie od trybu |
Mini-lekcja: .find()
const oceny = [
{ id: 1, uczen: 'Anna', ocena: 4 },
{ id: 2, uczen: 'Jan', ocena: 5 },
]
const znaleziony = oceny.find(o => o.id === 2)
// { id: 2, uczen: 'Jan', ocena: 5 }
.find() zwraca pierwszy element spełniający warunek. Jak nie znajdzie — zwraca undefined.Mini-lekcja: .sort()
const liczby = [3, 1, 4, 1, 5]
// ⚠️ .sort() MUTUJE oryginalną tablicę!
// Dlatego zawsze robimy kopię: [...tablica].sort()
const rosnaco = [...liczby].sort((a, b) => a - b) // [1, 1, 3, 4, 5]
const malejaco = [...liczby].sort((a, b) => b - a) // [5, 4, 3, 1, 1]
// Sortowanie stringów:
const imiona = ['Jan', 'Anna', 'Marta']
const posortowane = [...imiona].sort((a, b) => {
if (a < b) return -1
if (a > b) return 1
return 0
})
[...arr].sort() — kopia. React potrzebuje nowej referencji żeby przerendrować.Mini-lekcja: [...new Set()]
const przedmioty = ['Matma', 'Fizyka', 'Matma', 'Informatyka', 'Fizyka']
const unikalne = [...new Set(przedmioty)]
// ['Matma', 'Fizyka', 'Informatyka']
new Set(tablica) tworzy zbiór (bez duplikatów). [...set] zamienia go z powrotem na tablicę.Architektura aplikacji
App
├── state: oceny[], nowyUczen, nowyPrzedmiot, nowaOcena, edytowanyId
├── state: sortKolumna, sortRosnaco, filtrPrzedmiot
├── computed: przedmioty = [...new Set(oceny.map(...))]
├── computed: przefiltrowane = oceny.filter(...)
├── computed: posortowane = [...przefiltrowane].sort(...)
├── computed: srednia = oceny.reduce(...) / oceny.length
├── dodaj() → add NEW or SAVE EDIT (based on edytowanyId)
├── edytuj(id) → .find() → fill form → set edytowanyId
├── usun(id) → .filter() → clear edit if needed
├── zmienSort() → toggle direction or change column
└── kolorOceny()→ badge class by grade
1. Czym się różni
.find() od .filter()?2. Dlaczego
.sort() wymaga kopii tablicy [...arr]?3. Co robi
new Set()?4. Jak byś zrobił, żeby ten sam formularz służył do dodawania I edycji?
#04 — Lista wymagań
| # | Wymaganie | Krok | ✓ |
|---|---|---|---|
| 1 | Projekt Vite + Bootstrap działa | 0 | ☐ |
| 2 | Dane początkowe (6 obiektów) zdefiniowane poza komponentem | 1 | ☐ |
| 3 | useState dla oceny[], nowyUczen, nowyPrzedmiot, nowaOcena | 1 | ☐ |
| 4 | Formularz — 2× input text + select 1-6 + przycisk | 2 | ☐ |
| 5 | Controlled inputs — value + onChange | 2 | ☐ |
| 6 | dodaj() — walidacja pustych pól | 3 | ☐ |
| 7 | dodaj() — nowy obiekt z Date.now() id + toISOString date | 3 | ☐ |
| 8 | dodaj() — spread [...oceny, nowy] + czyszczenie formularza | 3 | ☐ |
| 9 | Tabela z .map() — uczeń, przedmiot, ocena (badge), data, akcje | 4 | ☐ |
| 10 | kolorOceny() — badge czerwony/żółty/zielony/niebieski | 4 | ☐ |
| 11 | usun(id) — .filter() + clear edit state | 4 | ☐ |
| 12 | edytuj(id) — .find() wypełnia formularz | 5 | ☐ |
| 13 | edytowanyId state — null = dodawanie, id = edycja | 5 | ☐ |
| 14 | dodaj() obsługuje oba tryby (add / save edit) | 5 | ☐ |
| 15 | Przycisk „Dodaj" / „Zapisz" + „Anuluj" w trybie edycji | 5 | ☐ |
| 16 | zmienSort() — sortKolumna + sortRosnaco state | 6 | ☐ |
| 17 | Klikalne nagłówki tabeli z ▲/▼ | 6 | ☐ |
| 18 | [...przefiltrowane].sort() — kopia! | 6 | ☐ |
| 19 | Filtr po przedmiocie — dynamiczny select z [...new Set()] | 7 | ☐ |
| 20 | Karta podsumowania — ilość, średnia toFixed(2), ile przedmiotów | 7 | ☐ |
| 21 | Alert ostrzegawczy gdy średnia < 3.0 | 7 | ☐ |
#04 — Budowa krok po kroku
npm create vite@latest dziennik -- --template react
cd dziennik
npm install
npm install bootstrap
src/App.jsx, kasujesz domyślną zawartość i importujesz Bootstrap:import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
function App() {
return (
<div className="container mt-3">
<h1>Dziennik ocen</h1>
<p>Zaraz tu będzie apka!</p>
</div>
)
}
export default App
npm run dev?const poczatkowe = [
{ id: 1, uczen: 'Anna Kowalska', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-15' },
{ id: 2, uczen: 'Jan Nowak', przedmiot: 'Fizyka', ocena: 2, data: '2025-01-16' },
{ id: 3, uczen: 'Anna Kowalska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-17' },
{ id: 4, uczen: 'Marta Wiśniewska', przedmiot: 'Matematyka', ocena: 3, data: '2025-01-18' },
{ id: 5, uczen: 'Jan Nowak', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-20' },
{ id: 6, uczen: 'Marta Wiśniewska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-22' },
]
App(). Zauważ — tym razem mamy DWA rodzaje state: dane (oceny, formularz) i UI (sortowanie, filtr, edycja).function App() {
// Dane
const [oceny, setOceny] = useState(poczatkowe)
const [nowyUczen, setNowyUczen] = useState('')
const [nowyPrzedmiot, setNowyPrzedmiot] = useState('')
const [nowaOcena, setNowaOcena] = useState(3)
// Na razie return ze szkieletem
return (
<div className="container mt-3">
<h1>Dziennik ocen</h1>
<p>Ocen: {oceny.length}</p>
</div>
)
}
nowaOcena inicjalizujemy na 3, a nie na ''?d-flex gap-2.<div className="d-flex gap-2 mb-3">
<input className="form-control" type="text" placeholder="Uczeń..."
value={nowyUczen} onChange={(e) => setNowyUczen(e.target.value)} />
<input className="form-control" type="text" placeholder="Przedmiot..."
value={nowyPrzedmiot} onChange={(e) => setNowyPrzedmiot(e.target.value)} />
<select className="form-select" style={{ width: '120px' }}
value={nowaOcena} onChange={(e) => setNowaOcena(Number(e.target.value))}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
<option value={5}>5</option>
<option value={6}>6</option>
</select>
<button className="btn btn-primary" onClick={dodaj}>Dodaj</button>
</div>
Number(e.target.value) w onChange selecta! Bez Number() dostalibyśmy string „3" zamiast number 3.dodaj() na razie tylko dodaje — edycję dorobimy w kroku 5. Ale przygotuj się: ta sama funkcja będzie potem obsługiwać OBA tryby!function dodaj() {
if (nowyUczen.trim() === '' || nowyPrzedmiot.trim() === '') return
const nowy = {
id: Date.now(),
uczen: nowyUczen,
przedmiot: nowyPrzedmiot,
ocena: nowaOcena,
data: new Date().toISOString().slice(0, 10),
}
setOceny([...oceny, nowy])
setNowyUczen('')
setNowyPrzedmiot('')
setNowaOcena(3)
}
new Date().toISOString().slice(0, 10)?new Date() → obiekt daty. .toISOString() → "2025-01-22T14:30:00.000Z". .slice(0, 10) → "2025-01-22". Elegancki sposób na dzisiejszą datę!.trim() === '' sprawdza, czy pole nie jest puste (ani same spacje). Nie walidujemy oceny — select wymusza wartość 1-6.oceny.length powinno zwiększyć się o 1.Najpierw funkcja kolorująca badge — dodaj ją wewnątrz App():
function kolorOceny(ocena) {
if (ocena <= 2) return 'bg-danger'
if (ocena === 3) return 'bg-warning text-dark'
if (ocena <= 5) return 'bg-success'
return 'bg-primary'
}
function usun(id) {
setOceny(oceny.filter(o => o.id !== id))
}
I sama tabela w JSX:
<table className="table table-striped">
<thead>
<tr>
<th>Uczeń</th>
<th>Przedmiot</th>
<th>Ocena</th>
<th>Data</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{oceny.map(o => (
<tr key={o.id}>
<td>{o.uczen}</td>
<td>{o.przedmiot}</td>
<td><span className={`badge ${kolorOceny(o.ocena)}`}>{o.ocena}</span></td>
<td>{o.data}</td>
<td>
<button className="btn btn-danger btn-sm" onClick={() => usun(o.id)}>✕</button>
</td>
</tr>
))}
</tbody>
</table>
className={`badge ${kolorOceny(o.ocena)}`} to template literal — łączysz stałą klasę „badge" ze zmienną klasą koloru.5a. Nowy state
const [edytowanyId, setEdytowanyId] = useState(null)
null = tryb dodawania. Jak klikniemy edit → ustawiamy id edytowanego elementu. To prosty wzorzec „toggle" między dwoma trybami.5b. Funkcja edytuj(id)
function edytuj(id) {
const o = oceny.find(o => o.id === id)
setNowyUczen(o.uczen)
setNowyPrzedmiot(o.przedmiot)
setNowaOcena(o.ocena)
setEdytowanyId(id)
}
.find() tutaj?oceny obiektu, którego id pasuje. Zwraca cały obiekt — wtedy wypełniamy formularz jego danymi. Użytkownik widzi stare wartości i może je zmienić!5c. Zmień dodaj() — obsługa edycji
function dodaj() {
if (nowyUczen.trim() === '' || nowyPrzedmiot.trim() === '') return
if (edytowanyId !== null) {
// TRYB EDYCJI — aktualizuj istniejący obiekt
setOceny(oceny.map(o =>
o.id === edytowanyId
? { ...o, uczen: nowyUczen, przedmiot: nowyPrzedmiot, ocena: nowaOcena }
: o
))
setEdytowanyId(null)
} else {
// TRYB DODAWANIA — nowy obiekt
const nowy = {
id: Date.now(),
uczen: nowyUczen,
przedmiot: nowyPrzedmiot,
ocena: nowaOcena,
data: new Date().toISOString().slice(0, 10),
}
setOceny([...oceny, nowy])
}
setNowyUczen('')
setNowyPrzedmiot('')
setNowaOcena(3)
}
oceny.map(o => o.id === edytowanyId ? { ...o, ... } : o). Przelatuje całą tablicę — jak trafia na edytowany element, podmienia dane. Resztę zostawia.5d. Zmień usun() — wyczyść edycję jeśli kasujesz edytowany
function usun(id) {
setOceny(oceny.filter(o => o.id !== id))
if (edytowanyId === id) setEdytowanyId(null)
}
5e. Zmień formularz — warunkowy przycisk
<button className={`btn ${edytowanyId ? 'btn-warning' : 'btn-primary'}`}
onClick={dodaj}>
{edytowanyId ? 'Zapisz' : 'Dodaj'}
</button>
{edytowanyId && (
<button className="btn btn-secondary" onClick={() => {
setEdytowanyId(null); setNowyUczen(''); setNowyPrzedmiot(''); setNowaOcena(3);
}}>Anuluj</button>
)}
5f. Dodaj przycisk edycji w tabeli
<td>
<button className="btn btn-warning btn-sm me-1" onClick={() => edytuj(o.id)}>✎</button>
<button className="btn btn-danger btn-sm" onClick={() => usun(o.id)}>✕</button>
</td>
6a. Nowy state
const [sortKolumna, setSortKolumna] = useState('data')
const [sortRosnaco, setSortRosnaco] = useState(true)
6b. Funkcja zmienSort()
function zmienSort(kolumna) {
if (sortKolumna === kolumna) {
setSortRosnaco(!sortRosnaco)
} else {
setSortKolumna(kolumna)
setSortRosnaco(true)
}
}
6c. Obliczona posortowana lista
const posortowane = [...oceny].sort((a, b) => {
if (a[sortKolumna] < b[sortKolumna]) return sortRosnaco ? -1 : 1
if (a[sortKolumna] > b[sortKolumna]) return sortRosnaco ? 1 : -1
return 0
})
a[sortKolumna]?sortKolumna = 'uczen', to a['uczen'] = a.uczen. Dzięki temu jeden sort obsługuje WSZYSTKIE kolumny![...oceny].sort() — robimy kopię! .sort() mutuje oryginalną tablicę, a React tego nie lubi.6d. Zamień .map w tbody na posortowane
{posortowane.map(o => (...))}
6e. Klikalne nagłówki z ikonkami
<thead>
<tr>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('uczen')}>
Uczeń {sortKolumna === 'uczen' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('przedmiot')}>
Przedmiot {sortKolumna === 'przedmiot' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('ocena')}>
Ocena {sortKolumna === 'ocena' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('data')}>
Data {sortKolumna === 'data' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th>Akcje</th>
</tr>
</thead>
sortKolumna === 'uczen' ? (sortRosnaco ? '▲' : '▼') : '' → jeśli ta kolumna jest aktywna → pokaż strzałkę (góra/dół), jeśli nie → nic.7a. State filtra
const [filtrPrzedmiot, setFiltrPrzedmiot] = useState('')
7b. Computed values — unikalne przedmioty + filtrowanie + sortowanie + średnia
const przedmioty = [...new Set(oceny.map(o => o.przedmiot))]
const przefiltrowane = oceny.filter(o =>
filtrPrzedmiot === '' || o.przedmiot === filtrPrzedmiot
)
const posortowane = [...przefiltrowane].sort((a, b) => {
if (a[sortKolumna] < b[sortKolumna]) return sortRosnaco ? -1 : 1
if (a[sortKolumna] > b[sortKolumna]) return sortRosnaco ? 1 : -1
return 0
})
const srednia = oceny.length > 0
? oceny.reduce((s, o) => s + o.ocena, 0) / oceny.length
: 0
oceny → przefiltrowane → posortowane. Każdy krok to nowa kopia. Oryginalna tablica oceny nigdy się nie zmienia![...new Set(oceny.map(o => o.przedmiot))] → wyciąga tablicę przedmiotów → Set usuwa duplikaty → spread zamienia na tablicę. Wynik: ['Matematyka', 'Fizyka', 'Informatyka'].7c. Karta podsumowania — przed formularzem
<div className="card mb-4">
<div className="card-body d-flex justify-content-around text-center">
<div>
<h5 className="card-title">Ocen</h5>
<p className="card-text fs-4">{oceny.length}</p>
</div>
<div>
<h5 className="card-title">Średnia</h5>
<p className="card-text fs-4">{srednia.toFixed(2)}</p>
</div>
<div>
<h5 className="card-title">Przedmiotów</h5>
<p className="card-text fs-4">{przedmioty.length}</p>
</div>
</div>
</div>
7d. Select filtra — po formularzu, przed tabelą
<select className="form-select mb-3" style={{ width: '220px' }}
value={filtrPrzedmiot} onChange={(e) => setFiltrPrzedmiot(e.target.value)}>
<option value="">Wszystkie przedmioty</option>
{przedmioty.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
przedmioty — dodasz nowy przedmiot → pojawi się w filtrze. Nie musisz nic hardkodować.7e. Alert ostrzegawczy — na koniec return
{oceny.length > 0 && srednia < 3 && (
<div className="alert alert-danger">
⚠️ Uwaga! Średnia ocen poniżej 3.0 — potrzebna poprawa!
</div>
)}
oceny.length > 0 && srednia < 3 — sprawdzamy oba warunki. Bez pierwszego, pusta tablica dałaby średnia = 0, i alert by się pokazał mimo braku danych.1. Karta pokazuje poprawną liczbę ocen i średnią?
2. Filtr po przedmiocie działa?
3. Sortowanie nadal działa po filtrowaniu?
4. Edycja nadal działa (pełny cykl)?
5. Dodaj kilka jedynek — alert się pojawi gdy średnia < 3?
#04 — Gotowy kod
App.jsx
import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
const poczatkowe = [
{ id: 1, uczen: 'Anna Kowalska', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-15' },
{ id: 2, uczen: 'Jan Nowak', przedmiot: 'Fizyka', ocena: 2, data: '2025-01-16' },
{ id: 3, uczen: 'Anna Kowalska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-17' },
{ id: 4, uczen: 'Marta Wiśniewska', przedmiot: 'Matematyka', ocena: 3, data: '2025-01-18' },
{ id: 5, uczen: 'Jan Nowak', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-20' },
{ id: 6, uczen: 'Marta Wiśniewska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-22' },
]
function App() {
const [oceny, setOceny] = useState(poczatkowe)
const [nowyUczen, setNowyUczen] = useState('')
const [nowyPrzedmiot, setNowyPrzedmiot] = useState('')
const [nowaOcena, setNowaOcena] = useState(3)
const [edytowanyId, setEdytowanyId] = useState(null)
const [sortKolumna, setSortKolumna] = useState('data')
const [sortRosnaco, setSortRosnaco] = useState(true)
const [filtrPrzedmiot, setFiltrPrzedmiot] = useState('')
const przedmioty = [...new Set(oceny.map(o => o.przedmiot))]
const przefiltrowane = oceny.filter(o =>
filtrPrzedmiot === '' || o.przedmiot === filtrPrzedmiot
)
const posortowane = [...przefiltrowane].sort((a, b) => {
if (a[sortKolumna] < b[sortKolumna]) return sortRosnaco ? -1 : 1
if (a[sortKolumna] > b[sortKolumna]) return sortRosnaco ? 1 : -1
return 0
})
const srednia = oceny.length > 0
? oceny.reduce((s, o) => s + o.ocena, 0) / oceny.length
: 0
function dodaj() {
if (nowyUczen.trim() === '' || nowyPrzedmiot.trim() === '') return
if (edytowanyId !== null) {
setOceny(oceny.map(o =>
o.id === edytowanyId
? { ...o, uczen: nowyUczen, przedmiot: nowyPrzedmiot, ocena: nowaOcena }
: o
))
setEdytowanyId(null)
} else {
const nowy = {
id: Date.now(),
uczen: nowyUczen,
przedmiot: nowyPrzedmiot,
ocena: nowaOcena,
data: new Date().toISOString().slice(0, 10),
}
setOceny([...oceny, nowy])
}
setNowyUczen('')
setNowyPrzedmiot('')
setNowaOcena(3)
}
function edytuj(id) {
const o = oceny.find(o => o.id === id)
setNowyUczen(o.uczen)
setNowyPrzedmiot(o.przedmiot)
setNowaOcena(o.ocena)
setEdytowanyId(id)
}
function usun(id) {
setOceny(oceny.filter(o => o.id !== id))
if (edytowanyId === id) setEdytowanyId(null)
}
function zmienSort(kolumna) {
if (sortKolumna === kolumna) {
setSortRosnaco(!sortRosnaco)
} else {
setSortKolumna(kolumna)
setSortRosnaco(true)
}
}
function kolorOceny(ocena) {
if (ocena <= 2) return 'bg-danger'
if (ocena === 3) return 'bg-warning text-dark'
if (ocena <= 5) return 'bg-success'
return 'bg-primary'
}
return (
<div className="container mt-3">
<h1>Dziennik ocen</h1>
<div className="card mb-4">
<div className="card-body d-flex justify-content-around text-center">
<div>
<h5 className="card-title">Ocen</h5>
<p className="card-text fs-4">{oceny.length}</p>
</div>
<div>
<h5 className="card-title">Średnia</h5>
<p className="card-text fs-4">{srednia.toFixed(2)}</p>
</div>
<div>
<h5 className="card-title">Przedmiotów</h5>
<p className="card-text fs-4">{przedmioty.length}</p>
</div>
</div>
</div>
<div className="d-flex gap-2 mb-3">
<input className="form-control" type="text" placeholder="Uczeń..."
value={nowyUczen} onChange={(e) => setNowyUczen(e.target.value)} />
<input className="form-control" type="text" placeholder="Przedmiot..."
value={nowyPrzedmiot} onChange={(e) => setNowyPrzedmiot(e.target.value)} />
<select className="form-select" style={{ width: '120px' }}
value={nowaOcena} onChange={(e) => setNowaOcena(Number(e.target.value))}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
<option value={5}>5</option>
<option value={6}>6</option>
</select>
<button className={`btn ${edytowanyId ? 'btn-warning' : 'btn-primary'}`}
onClick={dodaj}>
{edytowanyId ? 'Zapisz' : 'Dodaj'}
</button>
{edytowanyId && (
<button className="btn btn-secondary" onClick={() => {
setEdytowanyId(null); setNowyUczen(''); setNowyPrzedmiot(''); setNowaOcena(3);
}}>Anuluj</button>
)}
</div>
<select className="form-select mb-3" style={{ width: '220px' }}
value={filtrPrzedmiot} onChange={(e) => setFiltrPrzedmiot(e.target.value)}>
<option value="">Wszystkie przedmioty</option>
{przedmioty.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
<table className="table table-striped">
<thead>
<tr>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('uczen')}>
Uczeń {sortKolumna === 'uczen' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('przedmiot')}>
Przedmiot {sortKolumna === 'przedmiot' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('ocena')}>
Ocena {sortKolumna === 'ocena' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('data')}>
Data {sortKolumna === 'data' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{posortowane.map(o => (
<tr key={o.id}>
<td>{o.uczen}</td>
<td>{o.przedmiot}</td>
<td><span className={`badge ${kolorOceny(o.ocena)}`}>{o.ocena}</span></td>
<td>{o.data}</td>
<td>
<button className="btn btn-warning btn-sm me-1" onClick={() => edytuj(o.id)}>✎</button>
<button className="btn btn-danger btn-sm" onClick={() => usun(o.id)}>✕</button>
</td>
</tr>
))}
</tbody>
</table>
{oceny.length > 0 && srednia < 3 && (
<div className="alert alert-danger">
⚠️ Uwaga! Średnia ocen poniżej 3.0 — potrzebna poprawa!
</div>
)}
</div>
)
}
export default App
.find(), .sort(), [...new Set()] i wzorzec edycji z edytowanyId. Brawo! 🎉📝 Sprawdzian — Dziennik ocen
5 pytań · 5 minut · minimum 60% żeby zdać
.find() od .filter()?.sort() stosujemy operator spread [...tablica]?.map()?📝 Ściąga React
Nowy projekt
npm create vite@latest app -- --template react
cd app; npm installImport useState
import { useState } from 'react'Stan (prymityw)
const [x, setX] = useState(0)
setX(x + 1)Stan (tablica)
const [l, setL] = useState([])
setL([...l, nowy])Zmiana obiektu w tablicy
setL(l.map(el =>
el.id === id
? { ...el, pole: nowa }
: el
))Usuwanie z tablicy
setL(l.filter(el =>
el.id !== id
)).filter()
const w = arr.filter(el =>
el.category === 1
).map() w JSX
{lista.map(el => (
<div key={el.id}>{el.x}</div>
))}Controlled input
<input value={x}
onChange={e => setX(e.target.value)} />Controlled checkbox
<input type="checkbox"
checked={on}
onChange={e => setOn(e.target.checked)} />Controlled select
<select value={x}
onChange={e => setX(Number(e.target.value))}>
<option value={1}>A</option>
</select>Ternary w JSX
{warunek ? <A /> : <B />}Short-circuit
{warunek && <A />}Spread operator
const n = { ...stary, pole: 'x' }
const a2 = [...a1, nowy]Warunkowe klasy
className={ok ? 'done' : ''}
className={`base ${ok ? 'done' : ''}`}Bootstrap switch (JSX)
<div className="form-check form-switch">
<input className="form-check-input"
type="checkbox" checked={x}
onChange={e => setX(e.target.checked)} />
<label className="form-check-label">
Label</label>
</div>🐛 Typowe błędy
// ❌ <div class="box"> ✅ <div className="box">// ❌ <label for="x"> ✅ <label htmlFor="x">// ❌ onClick={pobierz(id)} → od razu
// ✅ onClick={() => pobierz(id)} → po kliknięciu// ❌ z.downloads++ ✅ { ...z, downloads: z.downloads + 1 }
// ❌ arr.push(x) ✅ [...arr, x]
// ❌ arr.splice(i, 1) ✅ arr.filter(el => el.id !== id)// ❌ {l.map(el => <div>...</div>)}
// ✅ {l.map(el => <div key={el.id}>...</div>)}Muszą być w public/assets/. Ścieżka: /assets/obraz1.jpg.
import 'bootstrap/dist/css/bootstrap.css'// ❌ <img src="x"> ✅ <img src="x" />// ❌ <input checked={x} />
// ✅ <input checked={x} onChange={e => setX(e.target.checked)} />// select/number input zwraca STRING
// ❌ setX(e.target.value) → "2" (string)
// ✅ setX(Number(e.target.value)) → 2 (number)1. Co to Angular?
Angular to framework od Google do budowy aplikacji SPA (Single Page Application). Używa TypeScript zamiast czystego JavaScript.
Angular vs React
| Cecha | Angular | React |
|---|---|---|
| Typ | Pełny framework | Biblioteka UI |
| Język | TypeScript (wymagany) | JS/TS (opcjonalny) |
| Szablony | HTML + dyrektywy | JSX |
| Two-way binding | Wbudowany [(ngModel)] | Ręczny (value + onChange) |
| Routing | Wbudowany | Zewnętrzny (react-router) |
| DI (Dependency Injection) | Wbudowany | Brak (Context API) |
Kluczowe koncepcje
- Komponenty — bloki UI z własnym HTML, CSS, TS
- Moduły — grupują komponenty, serwisy, routing
- Serwisy — logika biznesowa, pobieranie danych
- Dyrektywy — *ngIf, *ngFor, [ngClass]
- Dependency Injection — automatyczne wstrzykiwanie zależności
📌 Zadanie: Porównanie frameworków
Odpowiedz na pytania:
1. Który framework wymaga TypeScript?
2. Gdzie two-way binding jest wbudowany?
3. Co to SPA?
1. Angular wymaga TypeScript
2. Angular ma wbudowany two-way binding [(ngModel)]
3. SPA = Single Page Application — aplikacja ładowana raz, bez przeładowań strony
2. Środowisko (Angular CLI)
Angular CLI to narzędzie do tworzenia i zarządzania projektem Angular.
Wymagania
- Node.js — wersja 18+ (LTS)
- npm — menedżer pakietów
- Angular CLI —
npm install -g @angular/cli
Tworzenie projektu
# Instalacja Angular CLI globalnie
npm install -g @angular/cli
# Tworzenie nowego projektu
ng new moj-projekt
# → Wybierz: CSS/SCSS, routing Yes/No
# Uruchomienie serwera dev
cd moj-projekt
ng serve
# → http://localhost:4200
Komendy CLI
| Komenda | Opis |
|---|---|
ng new nazwa | Nowy projekt |
ng serve | Serwer deweloperski |
ng generate component nazwa | Generuj komponent |
ng generate service nazwa | Generuj serwis |
ng build | Build produkcyjny |
ng test | Uruchom testy |
Skróty CLI
ng g c header # generate component
ng g s data # generate service
ng g m admin # generate module
📌 Zadanie: Stwórz projekt
Wykonaj kroki:
1. Zainstaluj Angular CLI
2. Stwórz projekt "galeria-angular"
3. Uruchom serwer i otwórz w przeglądarce
npm install -g @angular/cli
ng new galeria-angular
cd galeria-angular
ng serve
# Otwórz http://localhost:4200
3. Komponenty
Komponent w Angular to klasa TypeScript z dekoratorem @Component.
Struktura komponentu
// header.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-header', // tag HTML
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent {
tytul = 'Moja Aplikacja'; // właściwość
kliknij() { // metoda
console.log('Kliknięto!');
}
}
Szablon HTML
<!-- header.component.html -->
<header>
<h1>{{ tytul }}</h1>
<button (click)="kliknij()">Klik</button>
</header>
Generowanie komponentu
ng generate component header
# lub skrót:
ng g c header
# Tworzy:
# src/app/header/
# ├── header.component.ts
# ├── header.component.html
# ├── header.component.css
# └── header.component.spec.ts
Użycie komponentu
<!-- app.component.html -->
<app-header></app-header>
<main>Treść aplikacji</main>
📌 Zadanie: Komponent Karta
Wygeneruj komponent karta i dodaj:
— właściwość nazwa = 'Produkt'
— właściwość cena = 99
— metodę dodajDoKoszyka() wyświetlającą alert
— szablon HTML z tymi danymi i przyciskiem
// karta.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-karta',
templateUrl: './karta.component.html'
})
export class KartaComponent {
nazwa = 'Produkt';
cena = 99;
dodajDoKoszyka() {
alert(`Dodano ${this.nazwa} do koszyka!`);
}
}
<!-- karta.component.html -->
<div class="karta">
<h3>{{ nazwa }}</h3>
<p>Cena: {{ cena }} zł</p>
<button (click)="dodajDoKoszyka()">Dodaj do koszyka</button>
</div>
4. Szablony i interpolacja
Angular używa {{ }} do wyświetlania danych w HTML (interpolacja).
Interpolacja {{ }}
// komponent
export class AppComponent {
imie = 'Maks';
wiek = 18;
getPowitanie() {
return `Cześć ${this.imie}!`;
}
}
<!-- szablon -->
<p>Imię: {{ imie }}</p>
<p>Wiek: {{ wiek }}</p>
<p>Wynik: {{ 2 + 2 }}</p>
<p>{{ getPowitanie() }}</p>
Property binding [atrybut]
<!-- Dynamiczny atrybut -->
<img [src]="urlObrazu" [alt]="opis">
<button [disabled]="czyWylaczony">Wyślij</button>
<div [class.active]="jestAktywny"></div>
<p [style.color]="kolor">Tekst</p>
Event binding (event)
<button (click)="zapisz()">Zapisz</button>
<input (input)="onInput($event)">
<input (keyup.enter)="wyslij()">
<div (mouseenter)="pokaz()" (mouseleave)="ukryj()"></div>
Porównanie React vs Angular
| React (JSX) | Angular (Template) |
|---|---|
{wartosc} | {{ wartosc }} |
src={url} | [src]="url" |
onClick={fn} | (click)="fn()" |
className="x" | class="x" |
📌 Zadanie: Binding danych
Stwórz komponent z:
— właściwością urlLogo = 'logo.png'
— właściwością altText = 'Logo firmy'
— metodą kliknijLogo()
W szablonie użyj property binding dla src i alt, oraz event binding dla click.
<img
[src]="urlLogo"
[alt]="altText"
(click)="kliknijLogo()">
5. Dyrektywy (*ngIf, *ngFor)
Dyrektywy strukturalne zmieniają strukturę DOM — dodają/usuwają elementy.
*ngIf — warunkowe renderowanie
<!-- Pokaż jeśli warunek true -->
<p *ngIf="zalogowany">Witaj z powrotem!</p>
<!-- if-else -->
<p *ngIf="zalogowany; else niezalogowany">
Witaj {{ imie }}!
</p>
<ng-template #niezalogowany>
<p>Proszę się zalogować</p>
</ng-template>
*ngFor — pętla po tablicy
// komponent
produkty = [
{ id: 1, nazwa: 'Laptop', cena: 3500 },
{ id: 2, nazwa: 'Telefon', cena: 1200 },
{ id: 3, nazwa: 'Słuchawki', cena: 150 }
];
<!-- szablon -->
<ul>
<li *ngFor="let p of produkty">
{{ p.nazwa }} — {{ p.cena }} zł
</li>
</ul>
<!-- z indeksem -->
<div *ngFor="let p of produkty; let i = index">
{{ i + 1 }}. {{ p.nazwa }}
</div>
<!-- trackBy dla wydajności -->
<div *ngFor="let p of produkty; trackBy: trackById">
trackById(index: number, item: any) {
return item.id;
}
[ngClass] — klasy warunkowe
<div [ngClass]="{'active': jestAktywny, 'disabled': jestWylaczony}">
</div>
[ngStyle] — style warunkowe
<p [ngStyle]="{'color': blad ? 'red' : 'green'}">
{{ wiadomosc }}
</p>
📌 Zadanie: Lista z warunkiem
Mając tablicę użytkowników, wyświetl:
— listę imion używając *ngFor
— komunikat "Brak użytkowników" gdy lista pusta (*ngIf)
<div *ngIf="uzytkownicy.length === 0">
Brak użytkowników
</div>
<ul *ngIf="uzytkownicy.length > 0">
<li *ngFor="let u of uzytkownicy">
{{ u.imie }}
</li>
</ul>
6. Data Binding
Angular ma 4 typy data binding:
1. Interpolacja (komponent → DOM)
{{ wartosc }}
2. Property binding (komponent → DOM)
[src]="urlObrazu"
[disabled]="jestWylaczony"
3. Event binding (DOM → komponent)
(click)="zapisz()"
(input)="onInput($event)"
4. Two-way binding (obustronny)
<!-- Wymaga FormsModule! -->
<input [(ngModel)]="imie">
<p>Wpisano: {{ imie }}</p>
// app.module.ts
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [FormsModule]
})
[(ngModel)] = syntax banana-in-box
<!-- To samo co: -->
<input [ngModel]="imie" (ngModelChange)="imie = $event">
Porównanie z React
| React | Angular |
|---|---|
value={x} onChange={e => setX(e.target.value)} | [(ngModel)]="x" |
📌 Zadanie: Formularz z two-way binding
Stwórz formularz z polami imię i email używając [(ngModel)]. Wyświetl podgląd danych pod formularzem.
// komponent
imie = '';
email = '';
<input [(ngModel)]="imie" placeholder="Imię">
<input [(ngModel)]="email" placeholder="Email">
<p>Imię: {{ imie }}</p>
<p>Email: {{ email }}</p>
7. @Input i @Output
Komunikacja między komponentami rodzic ↔ dziecko.
@Input — dane od rodzica
// dziecko: karta.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-karta',
template: `<div>{{ nazwa }} — {{ cena }} zł</div>`
})
export class KartaComponent {
@Input() nazwa = '';
@Input() cena = 0;
}
<!-- rodzic -->
<app-karta [nazwa]="'Laptop'" [cena]="3500"></app-karta>
<app-karta [nazwa]="produkt.nazwa" [cena]="produkt.cena"></app-karta>
@Output — eventy do rodzica
// dziecko: karta.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-karta',
template: `
<div>{{ nazwa }}</div>
<button (click)="onDodaj()">Dodaj</button>
`
})
export class KartaComponent {
@Input() nazwa = '';
@Output() dodano = new EventEmitter<string>();
onDodaj() {
this.dodano.emit(this.nazwa);
}
}
<!-- rodzic -->
<app-karta
[nazwa]="'Laptop'"
(dodano)="handleDodano($event)">
</app-karta>
// rodzic
handleDodano(nazwa: string) {
console.log('Dodano:', nazwa);
}
Porównanie z React
| React | Angular |
|---|---|
<Karta nazwa="X" /> | <app-karta [nazwa]="'X'"> |
<Karta onDodaj={fn} /> | <app-karta (dodano)="fn($event)"> |
📌 Zadanie: Komponent z Input/Output
Stwórz komponent Przycisk:
— @Input() tekst: string
— @Output() kliknieto: EventEmitter
Użyj go w rodzicu i obsłuż kliknięcie.
// przycisk.component.ts
@Component({
selector: 'app-przycisk',
template: `<button (click)="onClick()">{{ tekst }}</button>`
})
export class PrzyciskComponent {
@Input() tekst = 'Kliknij';
@Output() kliknieto = new EventEmitter<void>();
onClick() {
this.kliknieto.emit();
}
}
// rodzic
<app-przycisk
[tekst]="'Zapisz'"
(kliknieto)="zapisz()">
</app-przycisk>
8. Serwisy i Dependency Injection
Serwis = klasa z logiką biznesową (API, obliczenia). DI = Angular automatycznie tworzy i wstrzykuje instancje.
Tworzenie serwisu
ng generate service data
# lub: ng g s data
// data.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // dostępny w całej aplikacji
})
export class DataService {
private produkty = [
{ id: 1, nazwa: 'Laptop', cena: 3500 },
{ id: 2, nazwa: 'Telefon', cena: 1200 }
];
getProdukty() {
return this.produkty;
}
dodajProdukt(nazwa: string, cena: number) {
const id = this.produkty.length + 1;
this.produkty.push({ id, nazwa, cena });
}
}
Wstrzykiwanie do komponentu
// lista.component.ts
import { Component } from '@angular/core';
import { DataService } from '../data.service';
@Component({
selector: 'app-lista',
template: `
<ul>
<li *ngFor="let p of produkty">{{ p.nazwa }}</li>
</ul>
`
})
export class ListaComponent {
produkty: any[] = [];
constructor(private dataService: DataService) {
this.produkty = this.dataService.getProdukty();
}
}
Dlaczego serwisy?
- Współdzielenie danych między komponentami
- Separacja logiki od UI
- Łatwiejsze testowanie
- Singleton — jedna instancja w aplikacji
📌 Zadanie: Serwis koszyka
Stwórz KoszykService z metodami:
— dodaj(produkt)
— usun(id)
— getKoszyk()
— getSuma()
@Injectable({ providedIn: 'root' })
export class KoszykService {
private koszyk: any[] = [];
dodaj(produkt: any) {
this.koszyk.push(produkt);
}
usun(id: number) {
this.koszyk = this.koszyk.filter(p => p.id !== id);
}
getKoszyk() {
return this.koszyk;
}
getSuma() {
return this.koszyk.reduce((sum, p) => sum + p.cena, 0);
}
}
9. Routing
Angular ma wbudowany router do nawigacji między widokami.
Konfiguracja routingu
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProduktyComponent } from './produkty/produkty.component';
import { KontaktComponent } from './kontakt/kontakt.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'produkty', component: ProduktyComponent },
{ path: 'kontakt', component: KontaktComponent },
{ path: '**', redirectTo: '' } // 404 → home
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
router-outlet
<!-- app.component.html -->
<nav>
<a routerLink="/">Home</a>
<a routerLink="/produkty">Produkty</a>
<a routerLink="/kontakt">Kontakt</a>
</nav>
<router-outlet></router-outlet>
routerLinkActive — aktywny link
<a routerLink="/produkty" routerLinkActive="active">
Produkty
</a>
Parametry trasy
// routing
{ path: 'produkt/:id', component: ProduktDetailComponent }
// komponent
import { ActivatedRoute } from '@angular/router';
constructor(private route: ActivatedRoute) {
this.route.params.subscribe(params => {
this.produktId = params['id'];
});
}
Nawigacja programowa
import { Router } from '@angular/router';
constructor(private router: Router) {}
goToProdukt(id: number) {
this.router.navigate(['/produkt', id]);
}
📌 Zadanie: Routing podstawowy
Skonfiguruj routing z 3 stronami: Home, O nas, Kontakt. Dodaj nawigację i router-outlet.
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'o-nas', component: ONasComponent },
{ path: 'kontakt', component: KontaktComponent }
];
<nav>
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
<a routerLink="/o-nas" routerLinkActive="active">O nas</a>
<a routerLink="/kontakt" routerLinkActive="active">Kontakt</a>
</nav>
<router-outlet></router-outlet>
10. Formularze
Angular ma 2 podejścia: Template-driven (proste) i Reactive (zaawansowane).
Template-driven (FormsModule)
// app.module.ts
import { FormsModule } from '@angular/forms';
@NgModule({ imports: [FormsModule] })
<form #f="ngForm" (ngSubmit)="onSubmit(f)">
<input name="imie" [(ngModel)]="imie" required>
<input name="email" [(ngModel)]="email" required email>
<button [disabled]="!f.valid">Wyślij</button>
</form>
Reactive Forms (ReactiveFormsModule)
// app.module.ts
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({ imports: [ReactiveFormsModule] })
// komponent
import { FormGroup, FormControl, Validators } from '@angular/forms';
export class FormComponent {
form = new FormGroup({
imie: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email]),
wiek: new FormControl(18, [Validators.min(1), Validators.max(120)])
});
onSubmit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="imie">
<div *ngIf="form.get('imie')?.invalid && form.get('imie')?.touched">
Imię jest wymagane
</div>
<input formControlName="email">
<input type="number" formControlName="wiek">
<button [disabled]="form.invalid">Wyślij</button>
</form>
Walidatory
| Walidator | Użycie |
|---|---|
required | Pole wymagane |
email | Format email |
minLength(n) | Min. długość |
maxLength(n) | Max. długość |
min(n) | Min. wartość |
max(n) | Max. wartość |
pattern(regex) | Wyrażenie regularne |
📌 Zadanie: Formularz rejestracji
Stwórz reactive form z polami: imie (required), email (required, email), haslo (required, minLength 6). Pokaż błędy walidacji.
form = new FormGroup({
imie: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email]),
haslo: new FormControl('', [Validators.required, Validators.minLength(6)])
});
11. HttpClient
Angular ma wbudowany HttpClient do komunikacji z API.
Konfiguracja
// app.module.ts
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [HttpClientModule]
})
Serwis z HTTP
// api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ApiService {
private apiUrl = 'https://jsonplaceholder.typicode.com';
constructor(private http: HttpClient) {}
// GET
getPosts(): Observable<any[]> {
return this.http.get<any[]>(`${this.apiUrl}/posts`);
}
// POST
addPost(post: any): Observable<any> {
return this.http.post(`${this.apiUrl}/posts`, post);
}
// DELETE
deletePost(id: number): Observable<any> {
return this.http.delete(`${this.apiUrl}/posts/${id}`);
}
}
Użycie w komponencie
export class PostyComponent implements OnInit {
posty: any[] = [];
loading = true;
constructor(private api: ApiService) {}
ngOnInit() {
this.api.getPosts().subscribe({
next: (data) => {
this.posty = data;
this.loading = false;
},
error: (err) => console.error(err)
});
}
}
Porównanie z React
| React | Angular |
|---|---|
useEffect + fetch | ngOnInit + HttpClient.subscribe |
| Promise | Observable (RxJS) |
📌 Zadanie: Pobierz użytkowników
Stwórz serwis pobierający użytkowników z https://jsonplaceholder.typicode.com/users. Wyświetl ich imiona w komponencie.
// user.service.ts
getUsers(): Observable<any[]> {
return this.http.get<any[]>('https://jsonplaceholder.typicode.com/users');
}
// komponent
ngOnInit() {
this.userService.getUsers().subscribe(data => {
this.users = data;
});
}
12. Struktura projektu Angular
Domyślna struktura
moj-projekt/
├── src/
│ ├── app/
│ │ ├── app.component.ts # Główny komponent
│ │ ├── app.component.html
│ │ ├── app.component.css
│ │ ├── app.module.ts # Główny moduł
│ │ └── app-routing.module.ts # Routing
│ ├── assets/ # Statyczne pliki
│ ├── index.html # Główny HTML
│ ├── main.ts # Entry point
│ └── styles.css # Globalne style
├── angular.json # Konfiguracja CLI
├── package.json # Zależności
└── tsconfig.json # Konfiguracja TS
Zalecana struktura dla większych projektów
src/app/
├── core/ # Singletons, guardy, interceptory
│ ├── services/
│ │ └── auth.service.ts
│ └── guards/
│ └── auth.guard.ts
├── shared/ # Współdzielone komponenty, pipes
│ ├── components/
│ │ └── button/
│ └── pipes/
├── features/ # Moduły funkcjonalne
│ ├── home/
│ │ ├── home.component.ts
│ │ └── home.module.ts
│ ├── produkty/
│ │ ├── produkty.component.ts
│ │ ├── produkt-detail/
│ │ └── produkty.module.ts
│ └── koszyk/
├── app.component.ts
├── app.module.ts
└── app-routing.module.ts
Moduły funkcjonalne (Feature Modules)
// produkty/produkty.module.ts
@NgModule({
declarations: [
ProduktyComponent,
ProduktDetailComponent
],
imports: [
CommonModule,
ProduktyRoutingModule
]
})
export class ProduktyModule {}
Lazy Loading
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'produkty',
loadChildren: () => import('./features/produkty/produkty.module')
.then(m => m.ProduktyModule)
}
];
📌 Zadanie: Organizacja projektu
Narysuj strukturę folderów dla aplikacji e-commerce z modułami: produkty, koszyk, użytkownik.
src/app/
├── core/
│ └── services/
│ ├── auth.service.ts
│ └── api.service.ts
├── shared/
│ └── components/
│ └── navbar/
├── features/
│ ├── produkty/
│ │ ├── lista-produktow/
│ │ ├── produkt-detail/
│ │ └── produkty.module.ts
│ ├── koszyk/
│ │ ├── koszyk.component.ts
│ │ └── koszyk.module.ts
│ └── user/
│ ├── login/
│ ├── profil/
│ └── user.module.ts
├── app.module.ts
└── app-routing.module.ts
🔥 Rozgrzewka — Galeria zdjęć (Angular)
Odpowiedz na pytania przed rozpoczęciem ćwiczenia:
1. Jak wyświetlić tablicę obrazów w Angular?
2. Jak dynamicznie ustawić atrybut src obrazka?
3. Jak obsłużyć kliknięcie w Angular?
4. Jak warunkowo pokazać element?
5. Jaka komenda CLI generuje komponent?
📋 Instrukcja — Galeria zdjęć (Angular)
Zbuduj galerię zdjęć w Angular z filtrowaniem kategorii i modalne preview.
Funkcjonalności
- Wyświetlanie siatki obrazów z *ngFor
- Filtrowanie po kategorii
- Powiększanie obrazu w modalu
- Licznik pobrań dla każdego obrazu
Technologie
- Angular 17+
- *ngFor, *ngIf, [ngClass]
- @Input, @Output
- Bootstrap 5 (opcjonalnie)
🔨 Budowa — Galeria zdjęć (Angular)
Krok 1: Nowy projekt
ng new galeria-angular
cd galeria-angular
ng serve
Krok 2: Model danych
// app.component.ts
zdjecia = [
{ id: 1, url: 'assets/img1.jpg', tytul: 'Góry', kategoria: 'natura', downloads: 0 },
{ id: 2, url: 'assets/img2.jpg', tytul: 'Miasto', kategoria: 'urban', downloads: 0 },
{ id: 3, url: 'assets/img3.jpg', tytul: 'Las', kategoria: 'natura', downloads: 0 },
{ id: 4, url: 'assets/img4.jpg', tytul: 'Plaża', kategoria: 'natura', downloads: 0 }
];
aktywnaKategoria = 'wszystkie';
wybraneZdjecie: any = null;
Krok 3: Filtrowanie
get przefiltrowane() {
if (this.aktywnaKategoria === 'wszystkie') {
return this.zdjecia;
}
return this.zdjecia.filter(z => z.kategoria === this.aktywnaKategoria);
}
get kategorie() {
return ['wszystkie', ...new Set(this.zdjecia.map(z => z.kategoria))];
}
setKategoria(kat: string) {
this.aktywnaKategoria = kat;
}
Krok 4: Szablon HTML
<div class="container">
<h1>Galeria</h1>
<!-- Filtry -->
<div class="filtry">
<button *ngFor="let kat of kategorie"
[ngClass]="{'active': kat === aktywnaKategoria}"
(click)="setKategoria(kat)">
{{ kat }}
</button>
</div>
<!-- Siatka -->
<div class="galeria">
<div *ngFor="let z of przefiltrowane" class="karta">
<img [src]="z.url" [alt]="z.tytul" (click)="wybraneZdjecie = z">
<h3>{{ z.tytul }}</h3>
<p>Pobrano: {{ z.downloads }}</p>
<button (click)="download(z)">Pobierz</button>
</div>
</div>
</div>
Krok 5: Modal
<!-- Modal -->
<div *ngIf="wybraneZdjecie" class="modal-overlay" (click)="wybraneZdjecie = null">
<div class="modal-content" (click)="$event.stopPropagation()">
<button class="close-btn" (click)="wybraneZdjecie = null">×</button>
<img [src]="wybraneZdjecie.url" [alt]="wybraneZdjecie.tytul">
<h2>{{ wybraneZdjecie.tytul }}</h2>
</div>
</div>
Krok 6: Metoda download
download(zdjecie: any) {
zdjecie.downloads++;
}
Krok 7: Style CSS
.galeria {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.karta {
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
}
.karta img {
width: 100%;
cursor: pointer;
}
.filtry button.active {
background: #007bff;
color: white;
}
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.8);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
max-width: 90%;
}
Krok 8: Obrazy w assets
Dodaj pliki graficzne do src/assets/: img1.jpg, img2.jpg, img3.jpg, img4.jpg
💻 Gotowy kod — Galeria (Angular)
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
zdjecia = [
{ id: 1, url: 'assets/img1.jpg', tytul: 'Góry', kategoria: 'natura', downloads: 0 },
{ id: 2, url: 'assets/img2.jpg', tytul: 'Miasto', kategoria: 'urban', downloads: 0 },
{ id: 3, url: 'assets/img3.jpg', tytul: 'Las', kategoria: 'natura', downloads: 0 },
{ id: 4, url: 'assets/img4.jpg', tytul: 'Plaża', kategoria: 'natura', downloads: 0 }
];
aktywnaKategoria = 'wszystkie';
wybraneZdjecie: any = null;
get przefiltrowane() {
if (this.aktywnaKategoria === 'wszystkie') return this.zdjecia;
return this.zdjecia.filter(z => z.kategoria === this.aktywnaKategoria);
}
get kategorie() {
return ['wszystkie', ...new Set(this.zdjecia.map(z => z.kategoria))];
}
setKategoria(kat: string) {
this.aktywnaKategoria = kat;
}
download(zdjecie: any) {
zdjecie.downloads++;
}
}
📝 Sprawdzian — Galeria (Angular)
5 pytań · 10 minut · minimum 60% żeby zdać
🔥 Rozgrzewka — Lista zadań (Angular)
1. Two-way binding dla inputa w Angular:
2. Jaki moduł potrzebny do ngModel?
3. Jak dodać element do tablicy (niemutująco)?
4. Metoda usunięcia elementu z tablicy po id:
5. Jak obsłużyć submit formularza?
📋 Instrukcja — Lista zadań (Angular)
Zbuduj aplikację Todo w Angular z dodawaniem, usuwaniem i oznaczaniem zadań.
Funkcjonalności
- Dodawanie nowych zadań
- Oznaczanie jako wykonane (checkbox)
- Usuwanie zadań
- Filtrowanie: wszystkie/aktywne/wykonane
🔨 Budowa — Lista zadań (Angular)
Krok 1: Projekt i FormsModule
// app.module.ts
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, FormsModule]
})
Krok 2: Model danych
interface Zadanie {
id: number;
tytul: string;
wykonane: boolean;
}
zadania: Zadanie[] = [];
noweZadanie = '';
filtr = 'wszystkie';
Krok 3: Metody CRUD
dodaj() {
if (!this.noweZadanie.trim()) return;
this.zadania = [...this.zadania, {
id: Date.now(),
tytul: this.noweZadanie,
wykonane: false
}];
this.noweZadanie = '';
}
usun(id: number) {
this.zadania = this.zadania.filter(z => z.id !== id);
}
toggle(id: number) {
this.zadania = this.zadania.map(z =>
z.id === id ? { ...z, wykonane: !z.wykonane } : z
);
}
Krok 4: Filtrowanie
get przefiltrowane() {
switch (this.filtr) {
case 'aktywne': return this.zadania.filter(z => !z.wykonane);
case 'wykonane': return this.zadania.filter(z => z.wykonane);
default: return this.zadania;
}
}
Krok 5: Szablon HTML
<div class="todo-app">
<h1>Lista zadań</h1>
<form (ngSubmit)="dodaj()">
<input [(ngModel)]="noweZadanie" name="nowe" placeholder="Nowe zadanie...">
<button type="submit">Dodaj</button>
</form>
<div class="filtry">
<button [ngClass]="{'active': filtr === 'wszystkie'}" (click)="filtr = 'wszystkie'">Wszystkie</button>
<button [ngClass]="{'active': filtr === 'aktywne'}" (click)="filtr = 'aktywne'">Aktywne</button>
<button [ngClass]="{'active': filtr === 'wykonane'}" (click)="filtr = 'wykonane'">Wykonane</button>
</div>
<ul>
<li *ngFor="let z of przefiltrowane" [ngClass]="{'done': z.wykonane}">
<input type="checkbox" [checked]="z.wykonane" (change)="toggle(z.id)">
{{ z.tytul }}
<button (click)="usun(z.id)">×</button>
</li>
</ul>
</div>
Krok 6: Style
.done { text-decoration: line-through; opacity: 0.6; }
.filtry button.active { background: #28a745; color: white; }
Krok 7: Testowanie
Przetestuj wszystkie funkcje: dodawanie, usuwanie, toggle, filtrowanie.
💻 Gotowy kod — Lista zadań (Angular)
import { Component } from '@angular/core';
interface Zadanie {
id: number;
tytul: string;
wykonane: boolean;
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
zadania: Zadanie[] = [];
noweZadanie = '';
filtr = 'wszystkie';
get przefiltrowane() {
switch (this.filtr) {
case 'aktywne': return this.zadania.filter(z => !z.wykonane);
case 'wykonane': return this.zadania.filter(z => z.wykonane);
default: return this.zadania;
}
}
dodaj() {
if (!this.noweZadanie.trim()) return;
this.zadania = [...this.zadania, {
id: Date.now(),
tytul: this.noweZadanie,
wykonane: false
}];
this.noweZadanie = '';
}
usun(id: number) {
this.zadania = this.zadania.filter(z => z.id !== id);
}
toggle(id: number) {
this.zadania = this.zadania.map(z =>
z.id === id ? { ...z, wykonane: !z.wykonane } : z
);
}
}
📝 Sprawdzian — Lista zadań (Angular)
5 pytań · 10 minut · minimum 60% żeby zdać
🔥 Rozgrzewka — Tracker wydatków (Angular)
1. Metoda sumowania tablicy w JS:
2. Jak przekazać dane do komponentu dziecka?
3. Jak wysłać event do rodzica?
4. Pipe do formatowania waluty:
5. Typ dla number input (e.target.value):
📋 Instrukcja — Tracker wydatków (Angular)
Zbuduj aplikację do śledzenia wydatków z kategoriami i podsumowaniem.
Funkcjonalności
- Dodawanie wydatków (opis, kwota, kategoria)
- Lista wydatków z możliwością usuwania
- Podsumowanie: suma, średnia, max
- Filtrowanie po kategorii
🔨 Budowa — Tracker wydatków (Angular)
Krok 1: Model danych
interface Wydatek {
id: number;
opis: string;
kwota: number;
kategoria: string;
data: Date;
}
wydatki: Wydatek[] = [];
nowyOpis = '';
nowaKwota = 0;
nowaKategoria = 'jedzenie';
filtrKategoria = 'wszystkie';
kategorie = ['jedzenie', 'transport', 'rozrywka', 'rachunki', 'inne'];
Krok 2: Metody obliczeń
get przefiltrowane() {
if (this.filtrKategoria === 'wszystkie') return this.wydatki;
return this.wydatki.filter(w => w.kategoria === this.filtrKategoria);
}
get suma() {
return this.przefiltrowane.reduce((s, w) => s + w.kwota, 0);
}
get srednia() {
return this.przefiltrowane.length ? this.suma / this.przefiltrowane.length : 0;
}
get max() {
return this.przefiltrowane.length
? Math.max(...this.przefiltrowane.map(w => w.kwota))
: 0;
}
Krok 3: Dodawanie/Usuwanie
dodaj() {
if (!this.nowyOpis.trim() || this.nowaKwota <= 0) return;
this.wydatki = [...this.wydatki, {
id: Date.now(),
opis: this.nowyOpis,
kwota: this.nowaKwota,
kategoria: this.nowaKategoria,
data: new Date()
}];
this.nowyOpis = '';
this.nowaKwota = 0;
}
usun(id: number) {
this.wydatki = this.wydatki.filter(w => w.id !== id);
}
Krok 4: Formularz HTML
<form (ngSubmit)="dodaj()">
<input [(ngModel)]="nowyOpis" name="opis" placeholder="Opis wydatku">
<input type="number" [(ngModel)]="nowaKwota" name="kwota" placeholder="Kwota">
<select [(ngModel)]="nowaKategoria" name="kategoria">
<option *ngFor="let k of kategorie" [value]="k">{{ k }}</option>
</select>
<button type="submit">Dodaj</button>
</form>
Krok 5: Lista i podsumowanie
<div class="podsumowanie">
<p>Suma: {{ suma | currency:'PLN' }}</p>
<p>Średnia: {{ srednia | currency:'PLN' }}</p>
<p>Max: {{ max | currency:'PLN' }}</p>
</div>
<select [(ngModel)]="filtrKategoria">
<option value="wszystkie">Wszystkie</option>
<option *ngFor="let k of kategorie" [value]="k">{{ k }}</option>
</select>
<ul>
<li *ngFor="let w of przefiltrowane">
{{ w.opis }} - {{ w.kwota | currency:'PLN' }} ({{ w.kategoria }})
<button (click)="usun(w.id)">×</button>
</li>
</ul>
Krok 6: Locale dla PLN
// app.module.ts
import { registerLocaleData } from '@angular/common';
import localePl from '@angular/common/locales/pl';
registerLocaleData(localePl);
@NgModule({
providers: [{ provide: LOCALE_ID, useValue: 'pl-PL' }]
})
Krok 7: Style
.podsumowanie { background: #f8f9fa; padding: 15px; border-radius: 8px; }
.podsumowanie p { margin: 5px 0; font-weight: bold; }
Krok 8: Testowanie
Dodaj kilka wydatków, przetestuj filtrowanie i obliczenia.
💻 Gotowy kod — Tracker wydatków (Angular)
import { Component } from '@angular/core';
interface Wydatek {
id: number;
opis: string;
kwota: number;
kategoria: string;
data: Date;
}
@Component({ selector: 'app-root', templateUrl: './app.component.html' })
export class AppComponent {
wydatki: Wydatek[] = [];
nowyOpis = '';
nowaKwota = 0;
nowaKategoria = 'jedzenie';
filtrKategoria = 'wszystkie';
kategorie = ['jedzenie', 'transport', 'rozrywka', 'rachunki', 'inne'];
get przefiltrowane() {
if (this.filtrKategoria === 'wszystkie') return this.wydatki;
return this.wydatki.filter(w => w.kategoria === this.filtrKategoria);
}
get suma() { return this.przefiltrowane.reduce((s, w) => s + w.kwota, 0); }
get srednia() { return this.przefiltrowane.length ? this.suma / this.przefiltrowane.length : 0; }
get max() { return this.przefiltrowane.length ? Math.max(...this.przefiltrowane.map(w => w.kwota)) : 0; }
dodaj() {
if (!this.nowyOpis.trim() || this.nowaKwota <= 0) return;
this.wydatki = [...this.wydatki, {
id: Date.now(), opis: this.nowyOpis, kwota: this.nowaKwota,
kategoria: this.nowaKategoria, data: new Date()
}];
this.nowyOpis = ''; this.nowaKwota = 0;
}
usun(id: number) { this.wydatki = this.wydatki.filter(w => w.id !== id); }
}
📝 Sprawdzian — Tracker wydatków (Angular)
5 pytań · 10 minut · minimum 60% żeby zdać
🔥 Rozgrzewka — Dziennik ocen (Angular)
1. Jak wygenerować serwis Angular CLI?
2. Dekorator dla serwisu singleton:
3. Sortowanie tablicy w JS:
4. Jak wstrzyknąć serwis do komponentu?
5. Znajdowanie elementu po warunku:
📋 Instrukcja — Dziennik ocen (Angular)
Zbuduj dziennik ocen z serwisem do zarządzania danymi.
Funkcjonalności
- Lista uczniów z przedmiotami i ocenami
- Dodawanie ocen do ucznia
- Średnia ocen ucznia
- Sortowanie po nazwisku/średniej
- Serwis do przechowywania danych
🔨 Budowa — Dziennik ocen (Angular)
Krok 1: Serwis
ng generate service dziennik
// dziennik.service.ts
@Injectable({ providedIn: 'root' })
export class DziennikService {
private uczniowie = [
{ id: 1, imie: 'Jan', nazwisko: 'Kowalski', oceny: [4, 5, 3, 4] },
{ id: 2, imie: 'Anna', nazwisko: 'Nowak', oceny: [5, 5, 4, 5] },
{ id: 3, imie: 'Piotr', nazwisko: 'Wiśniewski', oceny: [3, 3, 4, 3] }
];
getUczniowie() { return this.uczniowie; }
dodajOcene(id: number, ocena: number) {
const uczen = this.uczniowie.find(u => u.id === id);
if (uczen) uczen.oceny.push(ocena);
}
getSrednia(oceny: number[]) {
return oceny.length ? oceny.reduce((s, o) => s + o, 0) / oceny.length : 0;
}
}
Krok 2: Komponent
export class AppComponent {
uczniowie: any[] = [];
sortowanie = 'nazwisko';
nowaOcena = 5;
wybranyUczen: number | null = null;
constructor(private dziennik: DziennikService) {
this.uczniowie = this.dziennik.getUczniowie();
}
get posortowani() {
return [...this.uczniowie].sort((a, b) => {
if (this.sortowanie === 'nazwisko') {
return a.nazwisko.localeCompare(b.nazwisko);
}
return this.getSrednia(b.oceny) - this.getSrednia(a.oceny);
});
}
getSrednia(oceny: number[]) {
return this.dziennik.getSrednia(oceny);
}
dodajOcene(id: number) {
this.dziennik.dodajOcene(id, this.nowaOcena);
}
}
Krok 3: Szablon
<h1>Dziennik ocen</h1>
<select [(ngModel)]="sortowanie">
<option value="nazwisko">Sortuj po nazwisku</option>
<option value="srednia">Sortuj po średniej</option>
</select>
<table>
<tr>
<th>Imię</th><th>Nazwisko</th><th>Oceny</th><th>Średnia</th><th>Akcje</th>
</tr>
<tr *ngFor="let u of posortowani">
<td>{{ u.imie }}</td>
<td>{{ u.nazwisko }}</td>
<td>{{ u.oceny.join(', ') }}</td>
<td>{{ getSrednia(u.oceny) | number:'1.2-2' }}</td>
<td>
<select [(ngModel)]="nowaOcena">
<option *ngFor="let o of [1,2,3,4,5,6]" [value]="o">{{ o }}</option>
</select>
<button (click)="dodajOcene(u.id)">Dodaj</button>
</td>
</tr>
</table>
Krok 4: Style tabeli
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; border: 1px solid #ddd; text-align: left; }
th { background: #f8f9fa; }
Krok 5: Kolorowanie średniej
<td [ngStyle]="{'color': getSrednia(u.oceny) >= 4 ? 'green' : getSrednia(u.oceny) < 3 ? 'red' : 'orange'}">
{{ getSrednia(u.oceny) | number:'1.2-2' }}
</td>
Krok 6: Dodawanie ucznia
// Rozszerz serwis
dodajUcznia(imie: string, nazwisko: string) {
this.uczniowie.push({
id: Date.now(),
imie, nazwisko,
oceny: []
});
}
Krok 7: Walidacja
Dodaj walidację: ocena 1-6, niepuste imię/nazwisko.
Krok 8: Testowanie
Przetestuj dodawanie ocen, sortowanie, kolorowanie.
💻 Gotowy kod — Dziennik ocen (Angular)
// dziennik.service.ts
@Injectable({ providedIn: 'root' })
export class DziennikService {
private uczniowie = [
{ id: 1, imie: 'Jan', nazwisko: 'Kowalski', oceny: [4, 5, 3, 4] },
{ id: 2, imie: 'Anna', nazwisko: 'Nowak', oceny: [5, 5, 4, 5] },
{ id: 3, imie: 'Piotr', nazwisko: 'Wiśniewski', oceny: [3, 3, 4, 3] }
];
getUczniowie() { return this.uczniowie; }
dodajOcene(id: number, ocena: number) {
const u = this.uczniowie.find(x => x.id === id);
if (u && ocena >= 1 && ocena <= 6) u.oceny.push(ocena);
}
getSrednia(oceny: number[]) {
return oceny.length ? oceny.reduce((s, o) => s + o, 0) / oceny.length : 0;
}
}
// app.component.ts
export class AppComponent {
uczniowie: any[] = [];
sortowanie = 'nazwisko';
nowaOcena = 5;
constructor(private dziennik: DziennikService) {
this.uczniowie = this.dziennik.getUczniowie();
}
get posortowani() {
return [...this.uczniowie].sort((a, b) =>
this.sortowanie === 'nazwisko'
? a.nazwisko.localeCompare(b.nazwisko)
: this.getSrednia(b.oceny) - this.getSrednia(a.oceny)
);
}
getSrednia(oceny: number[]) { return this.dziennik.getSrednia(oceny); }
dodajOcene(id: number) { this.dziennik.dodajOcene(id, this.nowaOcena); }
}
📝 Sprawdzian — Dziennik ocen (Angular)
5 pytań · 10 minut · minimum 60% żeby zdać
📋 Ściąga Angular
Nowy projekt
ng new nazwa
ng serveGenerowanie
ng g c nazwa # component
ng g s nazwa # service
ng g m nazwa # moduleInterpolacja
{{ wartosc }}
{{ metoda() }}
{{ a + b }}Property binding
[src]="url"
[disabled]="x"
[class.active]="y"Event binding
(click)="fn()"
(input)="onInput($event)"
(keyup.enter)="submit()"Two-way binding
[(ngModel)]="wartosc"
// Wymaga FormsModule!*ngIf
<p *ngIf="warunek">...</p>
*ngIf="x; else other"
<ng-template #other>*ngFor
*ngFor="let x of arr"
*ngFor="let x of arr; let i = index"
trackBy: trackFn[ngClass]
[ngClass]="{'active': x, 'disabled': y}"@Input
@Input() nazwa = '';
// rodzic: [nazwa]="'X'"@Output
@Output() ev = new EventEmitter();
this.ev.emit(data);
// rodzic: (ev)="fn($event)"Serwis
@Injectable({providedIn:'root'})
constructor(private srv: XService)🐛 Typowe błędy Angular
Can't bind to 'ngModel' since it isn't a known property
→ Dodaj FormsModule do imports w module// ❌ *ngFor="x of arr"
// ✅ *ngFor="let x of arr"Component X is not part of any NgModule
→ Dodaj do declarations w @NgModule// ❌ this.arr.push(x)
// ✅ this.arr = [...this.arr, x]No provider for XService
→ Dodaj @Injectable({providedIn:'root'})// ❌ <app-Header> (wielkość liter!)
// ✅ <app-header>// ❌ *ngFor="let x of data$"
// ✅ *ngFor="let x of data$ | async"// ✅ trackById(index: number, item: any) {
// return item.id;
// }🖥️ Co to JavaFX?
„JavaFX to framework do tworzenia aplikacji desktopowych w Javie. Na egzaminie INF.04 część I to właśnie aplikacja okienkowa — najczęściej w JavaFX lub Swing."
Czym jest JavaFX?
JavaFX to nowoczesny framework GUI dla Javy, który zastąpił starszy Swing. Pozwala tworzyć aplikacje okienkowe z przyciskami, polami tekstowymi, tabelami i innymi kontrolkami.
Porównanie z innymi technologiami
| Technologia | Typ | Język | Zastosowanie |
|---|---|---|---|
| JavaFX | Desktop | Java | Aplikacje okienkowe Windows/Mac/Linux |
| Swing | Desktop | Java | Starsze aplikacje desktop (nadal na egzaminie) |
| React | Web | JavaScript | Strony i aplikacje webowe |
| Android | Mobile | Java/Kotlin | Aplikacje na telefony |
Dlaczego JavaFX na egzaminie?
Część I egzaminu to aplikacja desktopowa. Możesz wybrać JavaFX, Swing lub C# WinForms. JavaFX jest najpopularniejszy i najłatwiejszy do opanowania.
Zalety JavaFX
- Scene Builder — wizualne projektowanie interfejsu (drag & drop)
- FXML — oddzielenie wyglądu (XML) od logiki (Java)
- CSS — możesz stylować kontrolki jak w HTML
- Nowoczesny — animacje, efekty, multimedia
Podstawowe pojęcia
| Pojęcie | Opis |
|---|---|
Stage | Okno aplikacji (główne lub dialog) |
Scene | Zawartość okna (kontrolki + layout) |
Node | Dowolny element UI (Button, Label, VBox...) |
Controller | Klasa z logiką obsługi zdarzeń |
FXML | Plik XML definiujący układ kontrolek |
Pierwszy program
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.stage.Stage;
public class HelloFX extends Application {
@Override
public void start(Stage stage) {
Label label = new Label("Witaj w JavaFX!");
Scene scene = new Scene(label, 300, 200);
stage.setTitle("Moja aplikacja");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
„Jakie widzisz podobieństwa między JavaFX a React? Podpowiedź: pomyśl o komponentach i oddzieleniu widoku od logiki."
📝 Zadanie do teorii
3 elementy: Stage (okno), Scene (scena/zawartość), Node (kontrolka/element UI)
Odpowiednik komponentu: W JavaFX to Controller + FXML — razem tworzą zamkniętą całość z widokiem i logiką, podobnie jak komponent React.
🛠️ NetBeans + Scene Builder
„Na egzaminie masz NetBeans z zainstalowanym Scene Builderem. To pozwala projektować interfejs wizualnie — przeciągasz przyciski, pola tekstowe, a NetBeans generuje kod FXML."
NetBeans IDE
NetBeans to darmowe IDE do Javy, które jest standardem na egzaminie INF.04. Ma wbudowane wsparcie dla JavaFX.
Tworzenie projektu JavaFX
- File → New Project
- Wybierz JavaFX → JavaFX FXML Application
- Podaj nazwę projektu (np.
Kalkulator) - NetBeans utworzy 3 pliki:
Main.java,FXMLDocument.fxml,FXMLDocumentController.java
Scene Builder
Scene Builder to wizualny edytor FXML — projektujesz interfejs metodą drag & drop.
Otwieranie Scene Builder
- Kliknij prawym na plik
.fxml - Wybierz Open in Scene Builder
- Przeciągaj kontrolki z lewego panelu na scenę
Główne sekcje Scene Buildera
| Sekcja | Opis |
|---|---|
| Library (lewy panel) | Paleta kontrolek do przeciągania |
| Hierarchy (lewy dolny) | Drzewo elementów (co zawiera co) |
| Content (środek) | Podgląd interfejsu |
| Inspector (prawy panel) | Właściwości zaznaczonego elementu |
💡 W Inspector → Code ustaw fx:id dla kontrolek, które chcesz obsługiwać w kodzie Java. Np. fx:id="btnDodaj"
Łączenie FXML z Controllerem
// W FXMLDocumentController.java:
public class FXMLDocumentController implements Initializable {
@FXML
private Label lblWynik; // Połączenie z fx:id="lblWynik"
@FXML
private TextField txtLiczba;
@FXML
private void handleButtonClick(ActionEvent event) {
// Obsługa kliknięcia przycisku
lblWynik.setText("Kliknięto!");
}
@Override
public void initialize(URL url, ResourceBundle rb) {
// Kod uruchamiany przy starcie
}
}
📝 Zadanie do teorii
- Library — paleta kontrolek (Button, Label, TextField...)
- Hierarchy — drzewo elementów, pokazuje strukturę zagnieżdżenia
- Content — podgląd projektowanego interfejsu
- Inspector — właściwości, layout i kod (fx:id, onAction)
📁 Struktura projektu JavaFX
„Projekt JavaFX w NetBeans ma standardową strukturę. Najważniejsze to wiedzieć gdzie jest FXML, gdzie Controller, i jak są połączone."
Typowa struktura
MojProjekt/
├── src/
│ └── mojprojekt/
│ ├── MojProjekt.java # Klasa główna (main)
│ ├── FXMLDocument.fxml # Układ interfejsu
│ └── FXMLDocumentController.java # Logika
├── build/
└── nbproject/ # Pliki NetBeans
Opis plików
| Plik | Rola |
|---|---|
MojProjekt.java | Punkt wejścia, ładuje FXML i pokazuje okno |
FXMLDocument.fxml | Definicja UI w XML (kontrolki, layout) |
FXMLDocumentController.java | Obsługa zdarzeń, logika biznesowa |
Plik główny (Main)
package mojprojekt;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class MojProjekt extends Application {
@Override
public void start(Stage stage) throws Exception {
// Ładowanie pliku FXML
Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
Scene scene = new Scene(root);
stage.setTitle("Moja Aplikacja");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Plik FXML
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns:fx="http://javafx.com/fxml"
fx:controller="mojprojekt.FXMLDocumentController"
spacing="10" alignment="CENTER">
<Label text="Witaj!" fx:id="lblPowitanie"/>
<Button text="Kliknij" onAction="#handleKlik"/>
</VBox>
fx:controller musi wskazywać pełną nazwę klasy controllera (z pakietem).
📝 Zadanie do teorii
fx:controller w pliku FXML? Co się stanie, jeśli go pominiesz?
fx:controller wskazuje klasę Java, która obsługuje zdarzenia i ma dostęp do kontrolek (przez @FXML).
Bez tego atrybutu: kontrolki będą się wyświetlać, ale kliknięcia przycisków nie będą działać (brak połączenia z metodami handlera).
📐 Layouty (VBox, HBox, GridPane)
„Layouty to kontenery, które automatycznie rozmieszczają kontrolki. Podobnie jak flexbox w CSS — nie musisz podawać dokładnych pozycji."
Najważniejsze layouty
| Layout | Rozmieszczenie | Zastosowanie |
|---|---|---|
VBox | Pionowo (w kolumnie) | Formularz, lista opcji |
HBox | Poziomo (w wierszu) | Pasek narzędzi, przyciski obok siebie |
GridPane | Siatka (wiersze × kolumny) | Kalkulator, formularze |
BorderPane | 5 stref (top, left, center, right, bottom) | Główny layout aplikacji |
AnchorPane | Pozycjonowanie względem krawędzi | Dokładne pozycjonowanie |
StackPane | Warstwy (jeden na drugim) | Overlay, nakładki |
VBox — układ pionowy
<VBox spacing="10" alignment="CENTER" padding="20">
<Label text="Formularz"/>
<TextField promptText="Imię"/>
<TextField promptText="Nazwisko"/>
<Button text="Zapisz"/>
</VBox>
HBox — układ poziomy
<HBox spacing="10" alignment="CENTER">
<Button text="OK"/>
<Button text="Anuluj"/>
<Button text="Pomoc"/>
</HBox>
GridPane — siatka
<GridPane hgap="10" vgap="10">
<Label text="Login:" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<TextField GridPane.rowIndex="0" GridPane.columnIndex="1"/>
<Label text="Hasło:" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
<PasswordField GridPane.rowIndex="1" GridPane.columnIndex="1"/>
<Button text="Zaloguj" GridPane.rowIndex="2" GridPane.columnIndex="1"/>
</GridPane>
BorderPane
<BorderPane>
<top><MenuBar>...</MenuBar></top>
<left><VBox>Menu boczne</VBox></left>
<center><TextArea/></center>
<bottom><Label text="Status"/></bottom>
</BorderPane>
💡 Na egzaminie najczęściej używasz VBox lub GridPane dla prostych formularzy. BorderPane dla aplikacji z menu.
📝 Zadanie do teorii
GridPane — naturalnie mapuje przyciski kalkulatora na siatkę 4×4. Każdy przycisk ma swój wiersz i kolumnę.
Alternatywnie: VBox z zagnieżdżonymi HBox (każdy wiersz to HBox z 4 przyciskami).
🔘 Button i Label
„Button i Label to podstawowe kontrolki. Label wyświetla tekst, Button reaguje na kliknięcia. To są Twoje chleb i masło w JavaFX."
Label — etykieta tekstowa
<!-- W FXML -->
<Label text="Witaj!" fx:id="lblWynik"/>
<Label text="Pogrubiony" style="-fx-font-weight: bold; -fx-font-size: 18px;"/>
// W Controller
@FXML private Label lblWynik;
// Zmiana tekstu
lblWynik.setText("Nowy tekst");
// Pobranie tekstu
String tekst = lblWynik.getText();
Button — przycisk
<!-- W FXML -->
<Button text="Kliknij mnie" fx:id="btnAkcja" onAction="#handleKlik"/>
// W Controller
@FXML private Button btnAkcja;
@FXML
private void handleKlik(ActionEvent event) {
System.out.println("Kliknięto przycisk!");
lblWynik.setText("Kliknięto!");
// Wyłączenie przycisku
btnAkcja.setDisable(true);
}
Właściwości Button
| Właściwość | Metoda | Opis |
|---|---|---|
| Tekst | setText() / getText() | Napis na przycisku |
| Wyłączony | setDisable(true/false) | Szary, nieklikany |
| Widoczny | setVisible(true/false) | Ukryj/pokaż |
| Styl | setStyle("-fx-...") | CSS inline |
Stylowanie CSS
<Button text="Zielony"
style="-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-size: 14px;"/>
<Label style="-fx-text-fill: red; -fx-font-weight: bold;"/>
W React: <button onClick={handleClick}>
W JavaFX: <Button onAction="#handleClick">
Różnica: React używa camelCase, JavaFX używa # przed nazwą metody.
📝 Zadanie do teorii
@FXML private TextField txtImie;
@FXML private Label lblWynik;
@FXML
private void handlePowitaj(ActionEvent event) {
String imie = txtImie.getText();
lblWynik.setText("Witaj " + imie + "!");
}
✏️ TextField i TextArea
„TextField to pole jednoliniowe — login, email, liczba. TextArea to pole wieloliniowe — opis, komentarz, notatka."
TextField — pole jednoliniowe
<TextField fx:id="txtImie" promptText="Wpisz imię..."/>
<TextField fx:id="txtLiczba" prefWidth="100"/>
<PasswordField fx:id="txtHaslo" promptText="Hasło"/>
// Pobranie tekstu
String imie = txtImie.getText();
// Ustawienie tekstu
txtImie.setText("Jan");
// Wyczyszczenie
txtImie.clear();
// Konwersja na liczbę (UWAGA: może rzucić wyjątek!)
int liczba = Integer.parseInt(txtLiczba.getText());
double wartosc = Double.parseDouble(txtLiczba.getText());
TextArea — pole wieloliniowe
<TextArea fx:id="txtOpis"
promptText="Wpisz opis..."
prefRowCount="5"
prefColumnCount="30"
wrapText="true"/>
// Pobranie całego tekstu
String opis = txtOpis.getText();
// Dodanie tekstu na końcu
txtOpis.appendText("\nNowa linia");
// Zaznaczenie całego tekstu
txtOpis.selectAll();
Walidacja liczb
@FXML
private void handleOblicz(ActionEvent event) {
try {
double liczba = Double.parseDouble(txtLiczba.getText());
lblWynik.setText("Wynik: " + (liczba * 2));
} catch (NumberFormatException e) {
lblWynik.setText("Błąd: podaj liczbę!");
}
}
💡 Na egzaminie ZAWSZE obsługuj wyjątki przy konwersji! try-catch dla NumberFormatException.
📝 Zadanie do teorii
getText() a getSelectedText()? Kiedy użyjesz każdej?
getText()— zwraca CAŁY tekst z polagetSelectedText()— zwraca tylko zaznaczony fragment
Użyjesz: getText() przy odczycie wartości formularza, getSelectedText() przy operacjach kopiuj/wytnij.
☑️ CheckBox i RadioButton
„CheckBox to 'tak/nie' — można zaznaczyć wiele. RadioButton to 'wybierz jedno' — grupowane razem."
CheckBox — wielokrotny wybór
<CheckBox fx:id="chkSer" text="Ser"/>
<CheckBox fx:id="chkSzynka" text="Szynka"/>
<CheckBox fx:id="chkPieczarki" text="Pieczarki" selected="true"/>
// Sprawdzenie czy zaznaczony
if (chkSer.isSelected()) {
System.out.println("Wybrano ser");
}
// Zaznaczenie programowe
chkSzynka.setSelected(true);
// Lista wybranych
String dodatki = "";
if (chkSer.isSelected()) dodatki += "ser ";
if (chkSzynka.isSelected()) dodatki += "szynka ";
if (chkPieczarki.isSelected()) dodatki += "pieczarki";
RadioButton — pojedynczy wybór
<!-- Grupowanie przez ToggleGroup -->
<ToggleGroup fx:id="grupaRozmiar"/>
<RadioButton fx:id="rbMala" text="Mała" toggleGroup="$grupaRozmiar"/>
<RadioButton fx:id="rbSrednia" text="Średnia" toggleGroup="$grupaRozmiar" selected="true"/>
<RadioButton fx:id="rbDuza" text="Duża" toggleGroup="$grupaRozmiar"/>
@FXML private ToggleGroup grupaRozmiar;
@FXML private RadioButton rbMala, rbSrednia, rbDuza;
// Sprawdzenie który wybrany
if (rbMala.isSelected()) {
System.out.println("Mała pizza");
}
// Alternatywnie - przez grupę
RadioButton wybrany = (RadioButton) grupaRozmiar.getSelectedToggle();
String rozmiar = wybrany.getText(); // "Mała", "Średnia" lub "Duża"
ToggleGroup, żeby działało "wybierz jeden".
📝 Zadanie do teorii
- Rozmiar: RadioButton z ToggleGroup — można wybrać tylko jeden rozmiar
- Dodatki: CheckBox — można wybrać wiele dodatków naraz
📋 ComboBox i ChoiceBox
„ComboBox i ChoiceBox to listy rozwijane — wybierasz jedną opcję z wielu. ComboBox pozwala też wpisać własną wartość."
ComboBox — lista rozwijana
<ComboBox fx:id="cmbMiasto" promptText="Wybierz miasto">
<items>
<FXCollections fx:factory="observableArrayList">
<String fx:value="Warszawa"/>
<String fx:value="Kraków"/>
<String fx:value="Gdańsk"/>
</FXCollections>
</items>
</ComboBox>
@FXML private ComboBox<String> cmbMiasto;
// Inicjalizacja w kodzie (łatwiejsze!)
@Override
public void initialize(URL url, ResourceBundle rb) {
cmbMiasto.getItems().addAll("Warszawa", "Kraków", "Gdańsk", "Wrocław");
cmbMiasto.setValue("Warszawa"); // Domyślna wartość
}
// Pobranie wybranej wartości
String miasto = cmbMiasto.getValue();
// Obsługa zmiany wyboru
cmbMiasto.setOnAction(e -> {
System.out.println("Wybrano: " + cmbMiasto.getValue());
});
ChoiceBox — prostsza alternatywa
@FXML private ChoiceBox<String> chbKategoria;
@Override
public void initialize(URL url, ResourceBundle rb) {
chbKategoria.getItems().addAll("Elektronika", "AGD", "Meble");
chbKategoria.setValue("Elektronika");
}
Różnice ComboBox vs ChoiceBox
| Cecha | ComboBox | ChoiceBox |
|---|---|---|
| Edytowalny | Tak (można wpisać) | Nie |
| Placeholder | promptText | Brak |
| Wydajność | Lepsza dla dużych list | Dla małych list |
📝 Zadanie do teorii
cmbMiasto.getItems().add("Poznań"); // Na końcu
cmbMiasto.getItems().add(0, "Łódź"); // Na początku
cmbMiasto.getItems().addAll("A", "B", "C"); // Wiele naraz
⚡ Obsługa zdarzeń
„Zdarzenia to reakcje na akcje użytkownika — kliknięcie, wpisanie tekstu, wybranie opcji. To serce interaktywności."
Sposób 1: onAction w FXML
<Button text="Kliknij" onAction="#handleKlik"/>
@FXML
private void handleKlik(ActionEvent event) {
System.out.println("Kliknięto!");
}
Sposób 2: setOnAction w kodzie
@Override
public void initialize(URL url, ResourceBundle rb) {
btnOblicz.setOnAction(event -> {
// kod obsługi
lblWynik.setText("Obliczono!");
});
}
Typy zdarzeń
| Zdarzenie | Kontrolka | Kiedy |
|---|---|---|
onAction | Button, TextField (Enter) | Kliknięcie / Enter |
onKeyPressed | Wszystkie | Naciśnięcie klawisza |
onMouseClicked | Wszystkie | Kliknięcie myszą |
setOnAction | ComboBox, ChoiceBox | Zmiana wyboru |
Przykład: walidacja przy Enter
txtLiczba.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) {
oblicz();
}
});
private void oblicz() {
try {
double x = Double.parseDouble(txtLiczba.getText());
lblWynik.setText("Kwadrat: " + (x * x));
} catch (NumberFormatException e) {
lblWynik.setText("Błąd!");
}
}
Źródło zdarzenia
@FXML
private void handlePrzycisk(ActionEvent event) {
Button btn = (Button) event.getSource();
String tekst = btn.getText(); // Który przycisk kliknięto
if (tekst.equals("Dodaj")) {
// ...
} else if (tekst.equals("Usuń")) {
// ...
}
}
📝 Zadanie do teorii
Jedna metoda! Użyj event.getSource() do sprawdzenia który przycisk kliknięto:
@FXML
private void handleCyfra(ActionEvent event) {
Button btn = (Button) event.getSource();
txtWyswietlacz.appendText(btn.getText());
}
🔗 FXML + Controller
„FXML definiuje CO wyświetlić. Controller definiuje CO ZROBIĆ. Rozdzielenie widoku od logiki — jak HTML + JavaScript."
Anatomia pliku FXML
<?xml version="1.0" encoding="UTF-8"?>
<!-- Importy (jak import w Java) -->
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<!-- Główny kontener + wskazanie controllera -->
<VBox xmlns:fx="http://javafx.com/fxml"
fx:controller="kalkulator.KalkulatorController"
spacing="10" alignment="CENTER" style="-fx-padding: 20;">
<!-- fx:id = nazwa pola w controllerze -->
<TextField fx:id="txtWynik" editable="false"/>
<!-- onAction = nazwa metody z # -->
<Button text="Oblicz" onAction="#handleOblicz"/>
</VBox>
Anatomia Controllera
package kalkulator;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import java.net.URL;
import java.util.ResourceBundle;
public class KalkulatorController implements Initializable {
// Pola połączone z FXML (fx:id)
@FXML private TextField txtWynik;
@FXML private Button btnOblicz;
// Metoda uruchamiana przy starcie
@Override
public void initialize(URL url, ResourceBundle rb) {
// Inicjalizacja, np. wypełnienie ComboBox
txtWynik.setText("0");
}
// Metoda obsługi zdarzenia (onAction)
@FXML
private void handleOblicz(ActionEvent event) {
// Logika
}
}
Diagram połączeń
FXML Controller
───────────────────── ─────────────────────
fx:controller="..." ───► public class ... implements Initializable
fx:id="txtWynik" ───► @FXML private TextField txtWynik
onAction="#handleOblicz"───► @FXML private void handleOblicz(...)
- Brak
@FXMLprzed polem/metodą → NullPointerException - Literówka w fx:id vs nazwa pola → NullPointerException
- Brak # przed nazwą metody w onAction
📝 Zadanie do teorii
@FXML? Co się stanie, jeśli ją pominiesz?
@FXML oznacza, że pole/metoda ma być wstrzyknięte z pliku FXML (na podstawie fx:id lub onAction).
Bez @FXML: pole będzie null, bo JavaFX nie wie, że ma je połączyć z kontrolką w FXML. Wywołanie metody na takim polu → NullPointerException.
🔥 Kalkulator — Rozgrzewka
„Zanim zbudujemy kalkulator, sprawdźmy czy pamiętasz podstawy JavaFX."
Mini Quiz
- Jaki layout najlepiej użyć do siatki przycisków kalkulatora?
- Jak pobrać tekst z TextField?
- Co oznacza
@FXML? - Jak obsłużyć błąd konwersji String na double?
- GridPane — siatka wierszy i kolumn
txtPole.getText()- Adnotacja łącząca pole/metodę z elementem FXML
try-catchdlaNumberFormatException
📋 Kalkulator — Instrukcja
„To Twoje egzaminowe zadanie. Przeczytaj uważnie wymagania."
Treść zadania
Wykonaj aplikację okienkową "Kalkulator" spełniającą wymagania:
- Interfejs zawiera: pole wyświetlacza, przyciski cyfr 0-9, przyciski operacji (+, -, *, /), przycisk "=" i "C"
- Kliknięcie cyfry dopisuje ją do wyświetlacza
- Kliknięcie operacji zapamiętuje liczbę i operację
- Kliknięcie "=" wykonuje obliczenie i wyświetla wynik
- Kliknięcie "C" czyści wyświetlacz
- Obsłuż dzielenie przez zero (komunikat błędu)
Wymagane komponenty
| Kontrolka | fx:id | Opis |
|---|---|---|
| TextField | txtWyswietlacz | Wyświetlacz (tylko odczyt) |
| Button ×10 | btn0-btn9 | Cyfry |
| Button ×4 | btnDodaj, btnOdejmij, btnMnoz, btnDziel | Operacje |
| Button | btnRowna | Oblicz wynik |
| Button | btnCzysc | Wyczyść |
🏗️ Kalkulator — Budowa UI
„Zaczynamy od Scene Buildera. Będziemy budować interfejs krok po kroku."
Krok 1: Utwórz projekt
- File → New Project → JavaFX FXML Application
- Nazwa:
Kalkulator - Otwórz
FXMLDocument.fxmlw Scene Builder
Krok 2: Struktura layoutu
- Usuń domyślny AnchorPane, dodaj VBox
- W VBox dodaj TextField (wyświetlacz)
- Pod TextField dodaj GridPane (4 kolumny × 4 wiersze)
Krok 3: Dodaj przyciski
Wiersz 0: [7] [8] [9] [/]
Wiersz 1: [4] [5] [6] [*]
Wiersz 2: [1] [2] [3] [-]
Wiersz 3: [0] [C] [=] [+]
Krok 4: Ustaw fx:id
W Inspector → Code ustaw fx:id dla każdej kontrolki:
- TextField:
txtWyswietlacz - Przyciski:
btn0,btn1, ...btn9 - Operacje:
btnDodaj,btnOdejmij,btnMnoz,btnDziel - Specjalne:
btnRowna,btnCzysc
Krok 5: Ustaw onAction
- Wszystkie cyfry:
#handleCyfra - Wszystkie operacje:
#handleOperacja - btnRowna:
#handleRowna - btnCzysc:
#handleCzysc
💻 Kalkulator — Kod
„Teraz implementujemy logikę. Pokażę Ci wzorcowy kod controllera."
FXMLDocumentController.java
package kalkulator;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import java.net.URL;
import java.util.ResourceBundle;
public class FXMLDocumentController implements Initializable {
@FXML private TextField txtWyswietlacz;
private double liczba1 = 0;
private String operacja = "";
private boolean nowaLiczba = true;
@Override
public void initialize(URL url, ResourceBundle rb) {
txtWyswietlacz.setEditable(false);
txtWyswietlacz.setText("0");
}
@FXML
private void handleCyfra(ActionEvent event) {
Button btn = (Button) event.getSource();
String cyfra = btn.getText();
if (nowaLiczba) {
txtWyswietlacz.setText(cyfra);
nowaLiczba = false;
} else {
txtWyswietlacz.setText(txtWyswietlacz.getText() + cyfra);
}
}
@FXML
private void handleOperacja(ActionEvent event) {
Button btn = (Button) event.getSource();
operacja = btn.getText();
try {
liczba1 = Double.parseDouble(txtWyswietlacz.getText());
nowaLiczba = true;
} catch (NumberFormatException e) {
txtWyswietlacz.setText("Błąd");
}
}
@FXML
private void handleRowna(ActionEvent event) {
try {
double liczba2 = Double.parseDouble(txtWyswietlacz.getText());
double wynik = 0;
switch (operacja) {
case "+": wynik = liczba1 + liczba2; break;
case "-": wynik = liczba1 - liczba2; break;
case "*": wynik = liczba1 * liczba2; break;
case "/":
if (liczba2 == 0) {
txtWyswietlacz.setText("Nie dziel przez 0!");
return;
}
wynik = liczba1 / liczba2;
break;
}
txtWyswietlacz.setText(String.valueOf(wynik));
nowaLiczba = true;
} catch (NumberFormatException e) {
txtWyswietlacz.setText("Błąd");
}
}
@FXML
private void handleCzysc(ActionEvent event) {
txtWyswietlacz.setText("0");
liczba1 = 0;
operacja = "";
nowaLiczba = true;
}
}
✅ Kalkulator — Sprawdzian
„Sprawdźmy, czy Twoje rozwiązanie działa poprawnie."
Testy do wykonania
| Test | Oczekiwany wynik |
|---|---|
| 2 + 3 = | 5.0 |
| 10 - 4 = | 6.0 |
| 6 * 7 = | 42.0 |
| 15 / 3 = | 5.0 |
| 5 / 0 = | "Nie dziel przez 0!" lub podobny komunikat |
| C | Wyświetlacz = "0" |
Kryteria oceny (INF.04)
- ☐ Interfejs zgodny z wymaganiami (wszystkie przyciski)
- ☐ Cyfry dopisują się do wyświetlacza
- ☐ Operacje działają poprawnie
- ☐ Obsłużone dzielenie przez zero
- ☐ Przycisk C czyści kalkulator
- ☐ Kod kompiluje się bez błędów
🔥 Lista zadań — Rozgrzewka
„Aplikacja Todo wymaga ListView. Przypomnijmy sobie jak działa."
Mini Quiz
- Jak dodać element do ListView?
- Jak pobrać zaznaczony element?
- Jak usunąć element z listy?
listView.getItems().add("element")listView.getSelectionModel().getSelectedItem()listView.getItems().remove(obiekt)lub.remove(index)
📋 Lista zadań — Instrukcja
„Klasyczna aplikacja Todo — idealna do nauki CRUD w JavaFX."
Treść zadania
Wykonaj aplikację "Lista zadań do zrobienia":
- Pole tekstowe do wpisania nowego zadania
- Przycisk "Dodaj" — dodaje zadanie do listy
- ListView wyświetlający wszystkie zadania
- Przycisk "Usuń" — usuwa zaznaczone zadanie
- Przycisk "Wyczyść wszystko" — usuwa wszystkie zadania
- Walidacja: nie można dodać pustego zadania
🏗️ Lista zadań — Budowa UI
„Prosty układ: VBox z polem, przyciskami i listą."
Struktura
<VBox spacing="10" style="-fx-padding: 20;">
<HBox spacing="10">
<TextField fx:id="txtZadanie" promptText="Nowe zadanie..." HBox.hgrow="ALWAYS"/>
<Button text="Dodaj" onAction="#handleDodaj"/>
</HBox>
<ListView fx:id="listZadania" VBox.vgrow="ALWAYS"/>
<HBox spacing="10">
<Button text="Usuń zaznaczone" onAction="#handleUsun"/>
<Button text="Wyczyść wszystko" onAction="#handleWyczysc"/>
</HBox>
</VBox>
💻 Lista zadań — Kod
package listaZadan;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import java.net.URL;
import java.util.ResourceBundle;
public class FXMLDocumentController implements Initializable {
@FXML private TextField txtZadanie;
@FXML private ListView<String> listZadania;
@Override
public void initialize(URL url, ResourceBundle rb) {
// Opcjonalnie: przykładowe zadania
listZadania.getItems().addAll(
"Nauczyć się JavaFX",
"Zdać egzamin INF.04"
);
}
@FXML
private void handleDodaj(ActionEvent event) {
String zadanie = txtZadanie.getText().trim();
if (zadanie.isEmpty()) {
pokazAlert("Błąd", "Wpisz treść zadania!");
return;
}
listZadania.getItems().add(zadanie);
txtZadanie.clear();
txtZadanie.requestFocus();
}
@FXML
private void handleUsun(ActionEvent event) {
String wybrany = listZadania.getSelectionModel().getSelectedItem();
if (wybrany == null) {
pokazAlert("Błąd", "Zaznacz zadanie do usunięcia!");
return;
}
listZadania.getItems().remove(wybrany);
}
@FXML
private void handleWyczysc(ActionEvent event) {
listZadania.getItems().clear();
}
private void pokazAlert(String tytul, String tresc) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle(tytul);
alert.setHeaderText(null);
alert.setContentText(tresc);
alert.showAndWait();
}
}
✅ Lista zadań — Sprawdzian
Testy
| Test | Oczekiwany wynik |
|---|---|
| Dodaj "Test" | "Test" pojawia się na liście |
| Dodaj puste | Alert z błędem |
| Zaznacz i usuń | Element znika z listy |
| Usuń bez zaznaczenia | Alert z błędem |
| Wyczyść wszystko | Lista pusta |
🔥 Rejestr produktów — Rozgrzewka
„To zadanie wymaga TableView — tabeli z kolumnami. Powtórzmy jak działa."
Mini Quiz
- Jak powiązać kolumnę TableView z polem obiektu?
- Jaka klasa reprezentuje wiersz w tabeli?
- Jak dodać wiersz do tabeli?
kolumna.setCellValueFactory(new PropertyValueFactory<>("nazwaPolaWKlasie"))- Własna klasa (np.
Produkt) z polami i getterami tableView.getItems().add(new Produkt(...))
📋 Rejestr produktów — Instrukcja
Treść zadania
Wykonaj aplikację "Rejestr produktów":
- Pola: nazwa (TextField), cena (TextField), ilość (TextField)
- TableView z kolumnami: Nazwa, Cena, Ilość, Wartość (cena×ilość)
- Przycisk "Dodaj" — dodaje produkt do tabeli
- Przycisk "Usuń" — usuwa zaznaczony produkt
- Label pokazujący sumę wartości wszystkich produktów
- Walidacja: cena i ilość muszą być liczbami
🏗️ Rejestr produktów — Budowa UI
Struktura layoutu
<VBox spacing="10" style="-fx-padding: 20;">
<HBox spacing="10">
<TextField fx:id="txtNazwa" promptText="Nazwa"/>
<TextField fx:id="txtCena" promptText="Cena" prefWidth="80"/>
<TextField fx:id="txtIlosc" promptText="Ilość" prefWidth="80"/>
<Button text="Dodaj" onAction="#handleDodaj"/>
</HBox>
<TableView fx:id="tabProdukty" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="colNazwa" text="Nazwa" prefWidth="150"/>
<TableColumn fx:id="colCena" text="Cena" prefWidth="80"/>
<TableColumn fx:id="colIlosc" text="Ilość" prefWidth="80"/>
<TableColumn fx:id="colWartosc" text="Wartość" prefWidth="100"/>
</columns>
</TableView>
<HBox spacing="10">
<Button text="Usuń zaznaczony" onAction="#handleUsun"/>
<Label text="Suma: "/>
<Label fx:id="lblSuma" text="0.00 zł"/>
</HBox>
</VBox>
💻 Rejestr produktów — Kod
Klasa Produkt.java
package rejestr;
public class Produkt {
private String nazwa;
private double cena;
private int ilosc;
public Produkt(String nazwa, double cena, int ilosc) {
this.nazwa = nazwa;
this.cena = cena;
this.ilosc = ilosc;
}
// Gettery (wymagane przez PropertyValueFactory!)
public String getNazwa() { return nazwa; }
public double getCena() { return cena; }
public int getIlosc() { return ilosc; }
public double getWartosc() { return cena * ilosc; }
}
Controller
package rejestr;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import java.net.URL;
import java.util.ResourceBundle;
public class FXMLDocumentController implements Initializable {
@FXML private TextField txtNazwa, txtCena, txtIlosc;
@FXML private TableView<Produkt> tabProdukty;
@FXML private TableColumn<Produkt, String> colNazwa;
@FXML private TableColumn<Produkt, Double> colCena, colWartosc;
@FXML private TableColumn<Produkt, Integer> colIlosc;
@FXML private Label lblSuma;
@Override
public void initialize(URL url, ResourceBundle rb) {
// Powiązanie kolumn z polami klasy Produkt
colNazwa.setCellValueFactory(new PropertyValueFactory<>("nazwa"));
colCena.setCellValueFactory(new PropertyValueFactory<>("cena"));
colIlosc.setCellValueFactory(new PropertyValueFactory<>("ilosc"));
colWartosc.setCellValueFactory(new PropertyValueFactory<>("wartosc"));
}
@FXML
private void handleDodaj(ActionEvent event) {
try {
String nazwa = txtNazwa.getText().trim();
double cena = Double.parseDouble(txtCena.getText());
int ilosc = Integer.parseInt(txtIlosc.getText());
if (nazwa.isEmpty()) {
pokazAlert("Podaj nazwę produktu!");
return;
}
tabProdukty.getItems().add(new Produkt(nazwa, cena, ilosc));
obliczSume();
wyczyscPola();
} catch (NumberFormatException e) {
pokazAlert("Cena i ilość muszą być liczbami!");
}
}
@FXML
private void handleUsun(ActionEvent event) {
Produkt wybrany = tabProdukty.getSelectionModel().getSelectedItem();
if (wybrany != null) {
tabProdukty.getItems().remove(wybrany);
obliczSume();
}
}
private void obliczSume() {
double suma = 0;
for (Produkt p : tabProdukty.getItems()) {
suma += p.getWartosc();
}
lblSuma.setText(String.format("%.2f zł", suma));
}
private void wyczyscPola() {
txtNazwa.clear();
txtCena.clear();
txtIlosc.clear();
txtNazwa.requestFocus();
}
private void pokazAlert(String tresc) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setContentText(tresc);
alert.showAndWait();
}
}
✅ Rejestr produktów — Sprawdzian
Testy
| Test | Oczekiwany wynik |
|---|---|
| Dodaj "Jabłko", 2.50, 10 | Wiersz w tabeli, Wartość=25.00 |
| Dodaj "Mleko", 3.00, 5 | Drugi wiersz, Suma=40.00 zł |
| Dodaj z tekstem w cenie | Alert z błędem |
| Usuń zaznaczony | Wiersz znika, suma się aktualizuje |
🔥 Dziennik ocen — Rozgrzewka
„To zadanie łączy ComboBox, TableView i obliczenia. Powtórzmy ComboBox."
Mini Quiz
- Jak dodać opcje do ComboBox w metodzie initialize?
- Jak pobrać wybraną wartość z ComboBox?
- Jak obliczyć średnią z listy liczb?
cmbPrzedmiot.getItems().addAll("Matematyka", "Fizyka")cmbPrzedmiot.getValue()- Suma wszystkich / ilość elementów
📋 Dziennik ocen — Instrukcja
Treść zadania
Wykonaj aplikację "Dziennik ocen ucznia":
- ComboBox z przedmiotami (Matematyka, Fizyka, Informatyka, Polski, Angielski)
- Pole tekstowe na imię ucznia
- ComboBox lub RadioButton na ocenę (1-6)
- TableView: Uczeń, Przedmiot, Ocena
- Przycisk "Dodaj ocenę"
- Label ze średnią wszystkich ocen
- Możliwość filtrowania po przedmiocie
🏗️ Dziennik ocen — Budowa UI
Struktura
<VBox spacing="10" style="-fx-padding: 20;">
<HBox spacing="10">
<TextField fx:id="txtUczen" promptText="Imię ucznia"/>
<ComboBox fx:id="cmbPrzedmiot" promptText="Przedmiot"/>
<ComboBox fx:id="cmbOcena" promptText="Ocena"/>
<Button text="Dodaj" onAction="#handleDodaj"/>
</HBox>
<HBox spacing="10">
<Label text="Filtruj:"/>
<ComboBox fx:id="cmbFiltr" promptText="Wszystkie" onAction="#handleFiltr"/>
</HBox>
<TableView fx:id="tabOceny" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="colUczen" text="Uczeń" prefWidth="150"/>
<TableColumn fx:id="colPrzedmiot" text="Przedmiot" prefWidth="120"/>
<TableColumn fx:id="colOcena" text="Ocena" prefWidth="80"/>
</columns>
</TableView>
<HBox spacing="10">
<Label text="Średnia:"/>
<Label fx:id="lblSrednia" text="0.00"/>
<Button text="Usuń zaznaczoną" onAction="#handleUsun"/>
</HBox>
</VBox>
💻 Dziennik ocen — Kod
Klasa Ocena.java
package dziennik;
public class Ocena {
private String uczen;
private String przedmiot;
private int ocena;
public Ocena(String uczen, String przedmiot, int ocena) {
this.uczen = uczen;
this.przedmiot = przedmiot;
this.ocena = ocena;
}
public String getUczen() { return uczen; }
public String getPrzedmiot() { return przedmiot; }
public int getOcena() { return ocena; }
}
Controller
package dziennik;
import javafx.collections.*;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import java.net.URL;
import java.util.ResourceBundle;
public class FXMLDocumentController implements Initializable {
@FXML private TextField txtUczen;
@FXML private ComboBox<String> cmbPrzedmiot, cmbFiltr;
@FXML private ComboBox<Integer> cmbOcena;
@FXML private TableView<Ocena> tabOceny;
@FXML private TableColumn<Ocena, String> colUczen, colPrzedmiot;
@FXML private TableColumn<Ocena, Integer> colOcena;
@FXML private Label lblSrednia;
private ObservableList<Ocena> wszystkieOceny = FXCollections.observableArrayList();
@Override
public void initialize(URL url, ResourceBundle rb) {
// Przedmioty
String[] przedmioty = {"Matematyka", "Fizyka", "Informatyka", "Polski", "Angielski"};
cmbPrzedmiot.getItems().addAll(przedmioty);
cmbFiltr.getItems().add("Wszystkie");
cmbFiltr.getItems().addAll(przedmioty);
cmbFiltr.setValue("Wszystkie");
// Oceny 1-6
cmbOcena.getItems().addAll(1, 2, 3, 4, 5, 6);
// Kolumny tabeli
colUczen.setCellValueFactory(new PropertyValueFactory<>("uczen"));
colPrzedmiot.setCellValueFactory(new PropertyValueFactory<>("przedmiot"));
colOcena.setCellValueFactory(new PropertyValueFactory<>("ocena"));
}
@FXML
private void handleDodaj(ActionEvent event) {
String uczen = txtUczen.getText().trim();
String przedmiot = cmbPrzedmiot.getValue();
Integer ocena = cmbOcena.getValue();
if (uczen.isEmpty() || przedmiot == null || ocena == null) {
pokazAlert("Wypełnij wszystkie pola!");
return;
}
Ocena nowaOcena = new Ocena(uczen, przedmiot, ocena);
wszystkieOceny.add(nowaOcena);
handleFiltr(null); // Odśwież tabelę
obliczSrednia();
txtUczen.clear();
}
@FXML
private void handleFiltr(ActionEvent event) {
String filtr = cmbFiltr.getValue();
if (filtr == null || filtr.equals("Wszystkie")) {
tabOceny.setItems(wszystkieOceny);
} else {
ObservableList<Ocena> przefiltrowane = FXCollections.observableArrayList();
for (Ocena o : wszystkieOceny) {
if (o.getPrzedmiot().equals(filtr)) {
przefiltrowane.add(o);
}
}
tabOceny.setItems(przefiltrowane);
}
}
@FXML
private void handleUsun(ActionEvent event) {
Ocena wybrana = tabOceny.getSelectionModel().getSelectedItem();
if (wybrana != null) {
wszystkieOceny.remove(wybrana);
handleFiltr(null);
obliczSrednia();
}
}
private void obliczSrednia() {
if (wszystkieOceny.isEmpty()) {
lblSrednia.setText("0.00");
return;
}
double suma = 0;
for (Ocena o : wszystkieOceny) {
suma += o.getOcena();
}
double srednia = suma / wszystkieOceny.size();
lblSrednia.setText(String.format("%.2f", srednia));
}
private void pokazAlert(String tresc) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setContentText(tresc);
alert.showAndWait();
}
}
✅ Dziennik ocen — Sprawdzian
Testy
| Test | Oczekiwany wynik |
|---|---|
| Dodaj "Jan", Matematyka, 5 | Wiersz w tabeli |
| Dodaj "Anna", Fizyka, 4 | Drugi wiersz, średnia=4.50 |
| Filtruj: Matematyka | Tylko oceny z matematyki |
| Filtruj: Wszystkie | Wszystkie oceny widoczne |
| Dodaj bez przedmiotu | Alert z błędem |
📜 JavaFX — Ściąga
Podstawowe kontrolki
| Kontrolka | Tworzenie | Pobieranie wartości |
|---|---|---|
| Label | <Label fx:id="lbl"/> | lbl.getText() |
| TextField | <TextField fx:id="txt"/> | txt.getText() |
| Button | <Button onAction="#handle"/> | btn.getText() |
| CheckBox | <CheckBox fx:id="chk"/> | chk.isSelected() |
| RadioButton | <RadioButton toggleGroup="$grp"/> | rb.isSelected() |
| ComboBox | <ComboBox fx:id="cmb"/> | cmb.getValue() |
| ListView | <ListView fx:id="list"/> | list.getSelectionModel().getSelectedItem() |
| TableView | <TableView fx:id="tab"/> | tab.getSelectionModel().getSelectedItem() |
Layouty
| Layout | Opis | Kluczowe atrybuty |
|---|---|---|
| VBox | Pionowo | spacing, alignment |
| HBox | Poziomo | spacing, alignment |
| GridPane | Siatka | hgap, vgap, rowIndex, columnIndex |
| BorderPane | 5 stref | top, left, center, right, bottom |
| AnchorPane | Kotwiczenie | topAnchor, leftAnchor... |
Obsługa zdarzeń
// W FXML: onAction="#metodaNazwa"
@FXML
private void metodaNazwa(ActionEvent event) {
Button btn = (Button) event.getSource(); // Który element wywołał
}
Konwersja typów
// String → int/double (z obsługą błędów!)
try {
int liczba = Integer.parseInt(txt.getText());
double wartosc = Double.parseDouble(txt.getText());
} catch (NumberFormatException e) {
// Obsługa błędu
}
// int/double → String
lbl.setText(String.valueOf(liczba));
lbl.setText(String.format("%.2f", wartosc)); // 2 miejsca po przecinku
Alert (komunikaty)
Alert alert = new Alert(Alert.AlertType.WARNING); // lub INFORMATION, ERROR
alert.setTitle("Tytuł");
alert.setHeaderText(null);
alert.setContentText("Treść komunikatu");
alert.showAndWait();
ListView operacje
list.getItems().add("element"); // Dodaj
list.getItems().remove(obiekt); // Usuń
list.getItems().clear(); // Wyczyść
list.getSelectionModel().getSelectedItem(); // Zaznaczony
TableView setup
// W initialize():
colNazwa.setCellValueFactory(new PropertyValueFactory<>("nazwa"));
// "nazwa" = getNazwa() w klasie modelu
🐛 JavaFX — Typowe błędy
NullPointerException na kontrolce
Przyczyna: Brak @FXML lub literówka w fx:id
// ✅ Poprawnie:
@FXML private TextField txtPole; // fx:id="txtPole" w FXML
NumberFormatException
Przyczyna: Użytkownik wpisał tekst zamiast liczby
// ✅ Zawsze używaj try-catch:
try {
double x = Double.parseDouble(txt.getText());
} catch (NumberFormatException e) {
pokazAlert("Podaj liczbę!");
}
onAction nie działa
Przyczyny:
- Brak # przed nazwą metody:
onAction="#handleKlik" - Brak @FXML przed metodą
- Literówka w nazwie metody
RadioButton nie grupują się
Przyczyna: Brak ToggleGroup
<!-- ✅ Poprawnie: -->
<fx:define>
<ToggleGroup fx:id="grupa"/>
</fx:define>
<RadioButton toggleGroup="$grupa" text="A"/>
<RadioButton toggleGroup="$grupa" text="B"/>
TableView nie pokazuje danych
Przyczyny:
- Brak
setCellValueFactorydla kolumn - Nazwa w PropertyValueFactory nie zgadza się z getterem
// Klasa: getNazwa() → PropertyValueFactory("nazwa")
// ✅ "nazwa" = getNazwa (bez "get", małą literą)
Brak fx:controller
Przyczyna: Brak lub błędny atrybut fx:controller w FXML
<!-- ✅ Na początku głównego kontenera: -->
<VBox xmlns:fx="http://javafx.com/fxml"
fx:controller="pakiet.NazwaController">
📱 Android Studio — Podstawy
„Android Studio to oficjalne IDE do tworzenia aplikacji na Androida. Na egzaminie INF.04 część I możesz wybrać aplikację mobilną zamiast desktopowej."
Co to Android?
Android to system operacyjny dla urządzeń mobilnych (telefony, tablety). Aplikacje pisze się w Java lub Kotlin.
Porównanie z innymi technologiami
| Technologia | Platforma | Język |
|---|---|---|
| Android (natywny) | Android | Java / Kotlin |
| iOS (natywny) | iPhone/iPad | Swift |
| React Native | Android + iOS | JavaScript |
| Flutter | Android + iOS | Dart |
Android Studio
Android Studio to IDE oparte na IntelliJ IDEA. Oferuje:
- Edytor layoutów — wizualne projektowanie UI (jak Scene Builder)
- Emulator — testowanie bez fizycznego telefonu
- Gradle — system budowania projektu
- Logcat — logi i debugowanie
Tworzenie projektu
- File → New Project
- Wybierz Empty Activity
- Podaj nazwę (np.
MojaAplikacja) - Language: Java (lub Kotlin)
- Minimum SDK: API 21 (Android 5.0)
💡 Na egzaminie wybierz API 21 lub wyższe — obsługuje 98% urządzeń.
📝 Zadanie do teorii
- Android Studio → aplikacje mobilne, NetBeans → desktop i web
- Android Studio używa Gradle, NetBeans używa Ant/Maven
- Android Studio ma wbudowany emulator telefonu
📁 Struktura projektu Android
„Projekt Android ma ściśle określoną strukturę. Najważniejsze to wiedzieć gdzie jest layout XML, gdzie Java, i gdzie zasoby."
Typowa struktura
app/
├── src/main/
│ ├── java/com/example/mojaaplikacja/
│ │ └── MainActivity.java # Główna aktywność
│ ├── res/
│ │ ├── layout/
│ │ │ └── activity_main.xml # Layout głównego ekranu
│ │ ├── values/
│ │ │ ├── strings.xml # Teksty
│ │ │ └── colors.xml # Kolory
│ │ └── drawable/ # Obrazki
│ └── AndroidManifest.xml # Konfiguracja aplikacji
└── build.gradle # Zależności
Opis kluczowych plików
| Plik | Rola |
|---|---|
MainActivity.java | Główna klasa — odpowiednik Main w JavaFX |
activity_main.xml | Układ UI — odpowiednik FXML |
strings.xml | Wszystkie teksty (dla wielojęzyczności) |
AndroidManifest.xml | Uprawnienia, konfiguracja aktywności |
Activity — co to jest?
Activity to pojedynczy ekran aplikacji. Każda Activity ma:
- Plik
.java— logika - Plik
.xml— layout (wygląd)
Cykl życia Activity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // Ładowanie layoutu
// Tu inicjalizacja kontrolek
}
}
📝 Zadanie do teorii
setContentView(R.layout.activity_main)?
Ta linia ładuje layout z pliku res/layout/activity_main.xml i wyświetla go jako zawartość ekranu.
R.layout to automatycznie generowana klasa z referencjami do zasobów.
📐 Layouty XML
„Layouty w Androidzie działają podobnie jak w JavaFX. Masz różne kontenery do rozmieszczania kontrolek."
Najważniejsze layouty
| Layout | Opis | Odpowiednik JavaFX |
|---|---|---|
LinearLayout | Pionowo lub poziomo | VBox / HBox |
ConstraintLayout | Względne pozycjonowanie | AnchorPane |
RelativeLayout | Pozycja względem innych | — |
FrameLayout | Jeden na drugim | StackPane |
GridLayout | Siatka | GridPane |
LinearLayout — pionowy
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Witaj!"/>
<EditText
android:id="@+id/txtImie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Wpisz imię"/>
<Button
android:id="@+id/btnPowitaj"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Powitaj"/>
</LinearLayout>
Wymiary
| Wartość | Znaczenie |
|---|---|
match_parent | Wypełnij rodzica (100%) |
wrap_content | Dopasuj do zawartości |
100dp | Stała wielkość (dp = density-independent pixels) |
💡 Używaj dp dla wymiarów i sp dla tekstu — skalują się na różnych ekranach.
📝 Zadanie do teorii
match_parent a wrap_content?
match_parent— element zajmuje całą dostępną przestrzeń rodzicawrap_content— element jest tylko tak duży, jak potrzebuje (dopasowuje się do zawartości)
✏️ TextView i EditText
„TextView to odpowiednik Label, EditText to odpowiednik TextField. Podstawowe kontrolki do tekstu."
TextView — wyświetlanie tekstu
<TextView
android:id="@+id/txtWynik"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Witaj świecie!"
android:textSize="18sp"
android:textColor="#333333"
android:textStyle="bold"/>
// W MainActivity.java
TextView txtWynik = findViewById(R.id.txtWynik);
// Zmiana tekstu
txtWynik.setText("Nowy tekst");
// Pobranie tekstu
String tekst = txtWynik.getText().toString();
EditText — pole tekstowe
<EditText
android:id="@+id/edtImie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Wpisz imię..."
android:inputType="text"/>
<!-- Dla liczb: -->
<EditText
android:id="@+id/edtLiczba"
android:inputType="number"/>
<!-- Dla hasła: -->
<EditText
android:id="@+id/edtHaslo"
android:inputType="textPassword"/>
EditText edtImie = findViewById(R.id.edtImie);
// Pobranie tekstu
String imie = edtImie.getText().toString();
// Ustawienie tekstu
edtImie.setText("Jan");
// Wyczyszczenie
edtImie.setText("");
inputType — typy klawiatury
| inputType | Klawiatura |
|---|---|
text | Standardowa |
number | Tylko cyfry |
numberDecimal | Cyfry + przecinek |
textPassword | Hasło (ukryte) |
textEmailAddress | Email (z @) |
phone | Numer telefonu |
📝 Zadanie do teorii
try {
double liczba = Double.parseDouble(edtLiczba.getText().toString());
// użyj liczby
} catch (NumberFormatException e) {
txtWynik.setText("Błąd: wpisz liczbę!");
}
🔘 Button i ImageView
„Button i ImageView to kolejne podstawowe kontrolki. Zobaczysz jak obsługiwać kliknięcia."
Button
<Button
android:id="@+id/btnOblicz"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Oblicz"
android:onClick="oblicz"/>
// Sposób 1: android:onClick="oblicz" w XML
public void oblicz(View view) {
// obsługa kliknięcia
txtWynik.setText("Kliknięto!");
}
// Sposób 2: setOnClickListener w Java
Button btnOblicz = findViewById(R.id.btnOblicz);
btnOblicz.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
txtWynik.setText("Kliknięto!");
}
});
// Sposób 3: Lambda (krótsza wersja)
btnOblicz.setOnClickListener(v -> {
txtWynik.setText("Kliknięto!");
});
ImageView
<ImageView
android:id="@+id/imgLogo"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/logo"
android:contentDescription="Logo aplikacji"/>
ImageView imgLogo = findViewById(R.id.imgLogo);
// Zmiana obrazka
imgLogo.setImageResource(R.drawable.inny_obrazek);
Umieść pliki PNG/JPG w folderze res/drawable/. Odwołuj się przez @drawable/nazwa (bez rozszerzenia).
📝 Zadanie do teorii
setOnClickListener (sposób 2 lub 3) jest zalecany, bo:
- Logika jest w kodzie Java, łatwiejsza do debugowania
- Możesz użyć jednego listenera dla wielu przycisków i sprawdzić który kliknięto przez
v.getId() - android:onClick wymaga publicznej metody i jest trudniejsze do refaktoryzacji
⚡ onClick — praktyka
„Zobaczmy kompletny przykład — od layoutu po działającą aplikację."
Przykład: Powitanie
Aplikacja pobiera imię i wyświetla powitanie po kliknięciu przycisku.
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/edtImie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Wpisz imię"/>
<Button
android:id="@+id/btnPowitaj"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Powitaj"/>
<TextView
android:id="@+id/txtWynik"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold"/>
</LinearLayout>
MainActivity.java
package com.example.powitanie;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.*;
public class MainActivity extends AppCompatActivity {
private EditText edtImie;
private Button btnPowitaj;
private TextView txtWynik;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Pobieranie referencji do kontrolek
edtImie = findViewById(R.id.edtImie);
btnPowitaj = findViewById(R.id.btnPowitaj);
txtWynik = findViewById(R.id.txtWynik);
// Obsługa kliknięcia
btnPowitaj.setOnClickListener(v -> {
String imie = edtImie.getText().toString().trim();
if (imie.isEmpty()) {
txtWynik.setText("Wpisz imię!");
} else {
txtWynik.setText("Witaj, " + imie + "!");
}
});
}
}
💡 Porównanie z JavaFX: findViewById(R.id.xxx) to odpowiednik @FXML + fx:id
📝 Zadanie do teorii
findViewById() dla kontrolki?
Zmienna będzie null. Próba użycia metody na niej (np. edtImie.getText()) spowoduje NullPointerException i crash aplikacji.
📦 Zasoby (Resources)
„W Androidzie oddzielamy zasoby (teksty, kolory, wymiary) od kodu. To ułatwia tłumaczenie aplikacji i zmianę wyglądu."
Folder res/
| Folder | Zawartość |
|---|---|
res/layout/ | Pliki XML layoutów |
res/values/ | Teksty, kolory, style |
res/drawable/ | Obrazki |
res/mipmap/ | Ikona aplikacji |
strings.xml
<!-- res/values/strings.xml -->
<resources>
<string name="app_name">Moja Aplikacja</string>
<string name="btn_oblicz">Oblicz</string>
<string name="hint_liczba">Wpisz liczbę...</string>
<string name="error_empty">Pole nie może być puste!</string>
</resources>
<!-- Użycie w layout XML: -->
<Button android:text="@string/btn_oblicz"/>
<EditText android:hint="@string/hint_liczba"/>
// Użycie w kodzie Java:
String blad = getString(R.string.error_empty);
txtWynik.setText(blad);
colors.xml
<!-- res/values/colors.xml -->
<resources>
<color name="primary">#6200EE</color>
<color name="error">#B00020</color>
<color name="success">#00C853</color>
</resources>
<!-- Użycie: -->
<TextView android:textColor="@color/error"/>
dimens.xml
<!-- res/values/dimens.xml -->
<resources>
<dimen name="text_size_large">24sp</dimen>
<dimen name="padding_standard">16dp</dimen>
</resources>
- Łatwe tłumaczenie — dodaj folder
values-pl/ - Jedna zmiana = wszędzie zmienione
- Różne zasoby dla różnych ekranów
📝 Zadanie do teorii
primary z colors.xml w layoucie XML i w kodzie Java?
<!-- W XML: -->
android:textColor="@color/primary"
// W Java:
int color = getResources().getColor(R.color.primary);
txtWynik.setTextColor(color);
🔗 ConstraintLayout
„ConstraintLayout to zaawansowany layout — pozycjonujesz elementy względem siebie lub rodzica. Domyślny w Android Studio."
Podstawowe ograniczenia (constraints)
| Atrybut | Znaczenie |
|---|---|
app:layout_constraintTop_toTopOf="parent" | Górna krawędź do góry rodzica |
app:layout_constraintBottom_toBottomOf="parent" | Dolna krawędź do dołu rodzica |
app:layout_constraintStart_toStartOf="parent" | Lewa krawędź do lewej rodzica |
app:layout_constraintEnd_toEndOf="parent" | Prawa krawędź do prawej rodzica |
app:layout_constraintTop_toBottomOf="@id/inne" | Pod innym elementem |
Przykład: Formularz logowania
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<EditText
android:id="@+id/edtLogin"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Login"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="100dp"/>
<EditText
android:id="@+id/edtHaslo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Hasło"
android:inputType="textPassword"
app:layout_constraintTop_toBottomOf="@id/edtLogin"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
<Button
android:id="@+id/btnZaloguj"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Zaloguj"
app:layout_constraintTop_toBottomOf="@id/edtHaslo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
💡 android:layout_width="0dp" + constraints po obu stronach = element rozciąga się między ograniczeniami.
📝 Zadanie do teorii
<Button
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
Dodanie wszystkich 4 constraintów = wyśrodkowanie.
📋 ListView i RecyclerView
„ListView to lista przewijana — odpowiednik ListView w JavaFX. Na egzaminie wystarczy prosta lista."
ListView — podstawowa lista
<ListView
android:id="@+id/listZadania"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
ListView listZadania = findViewById(R.id.listZadania);
// Lista danych
ArrayList<String> zadania = new ArrayList<>();
zadania.add("Nauczyć się Androida");
zadania.add("Zdać egzamin");
// Adapter = most między danymi a widokiem
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_list_item_1,
zadania
);
listZadania.setAdapter(adapter);
// Obsługa kliknięcia elementu
listZadania.setOnItemClickListener((parent, view, position, id) -> {
String wybrany = zadania.get(position);
Toast.makeText(this, "Wybrano: " + wybrany, Toast.LENGTH_SHORT).show();
});
Dodawanie i usuwanie elementów
// Dodaj element
zadania.add("Nowe zadanie");
adapter.notifyDataSetChanged(); // WAŻNE!
// Usuń element
zadania.remove(position);
adapter.notifyDataSetChanged();
adapter.notifyDataSetChanged() żeby odświeżyć widok.
Toast — szybki komunikat
Toast.makeText(this, "Tekst komunikatu", Toast.LENGTH_SHORT).show();
// LENGTH_SHORT = krótki, LENGTH_LONG = dłuższy
📝 Zadanie do teorii
notifyDataSetChanged() po dodaniu elementu?
Dane zostaną dodane do ArrayList, ale ListView NIE odświeży się — użytkownik nie zobaczy nowego elementu do czasu, aż lista sama się odświeży (np. przy przewijaniu).
☕ Java vs Kotlin
„Na egzaminie możesz wybrać Java lub Kotlin. Java jest bardziej tradycyjna i masz ją na innych przedmiotach. Kotlin to nowoczesna alternatywa."
Porównanie składni
| Operacja | Java | Kotlin |
|---|---|---|
| Zmienna | String s = "tekst"; | val s = "tekst" |
| Zmienna (edytowalna) | int x = 5; | var x = 5 |
| Null safety | Może być null! | String? (explicit) |
| Lambda | v -> {...} | { v -> ... } |
Ten sam kod — Java vs Kotlin
Java:
public class MainActivity extends AppCompatActivity {
private EditText edtImie;
private Button btnPowitaj;
private TextView txtWynik;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edtImie = findViewById(R.id.edtImie);
btnPowitaj = findViewById(R.id.btnPowitaj);
txtWynik = findViewById(R.id.txtWynik);
btnPowitaj.setOnClickListener(v -> {
String imie = edtImie.getText().toString();
txtWynik.setText("Witaj " + imie + "!");
});
}
}
Kotlin:
class MainActivity : AppCompatActivity() {
private lateinit var edtImie: EditText
private lateinit var btnPowitaj: Button
private lateinit var txtWynik: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
edtImie = findViewById(R.id.edtImie)
btnPowitaj = findViewById(R.id.btnPowitaj)
txtWynik = findViewById(R.id.txtWynik)
btnPowitaj.setOnClickListener {
val imie = edtImie.text.toString()
txtWynik.text = "Witaj $imie!"
}
}
}
💡 Na egzaminie wybierz Java, jeśli nie znasz Kotlina. Różnice są kosmetyczne dla prostych aplikacji.
📝 Zadanie do teorii
- Null safety — kompilator wymusza obsługę nulli
- Krótszy kod — mniej boilerplate (val/var, brak średników, string templates)
#06 — Zapisy na warsztaty (instrukcja)
Typ zadania: pełna aplikacja w Android Studio · Czas: ~40 min · Java · Zakres: tablice, CheckBox, RadioButton
Treść zadania
Napisz aplikację Zapisy na warsztaty. Szkoła prowadzi cztery warsztaty, każdy w swojej cenie. Uczestnik zaznacza dowolną liczbę warsztatów i wybiera jedną kategorię zniżki. Po naciśnięciu przycisku aplikacja pokazuje podsumowanie zamówienia.
Dane (mają być w kodzie jako tablice)
| Warsztat | Cena |
|---|---|
| Fotografia | 120 zł |
| Programowanie | 180 zł |
| Robotyka | 150 zł |
| Grafika 3D | 200 zł |
| Kategoria zniżki | Zniżka |
|---|---|
| Uczeń szkoły | 20% |
| Absolwent | 10% |
| Osoba z zewnątrz | 0% |
Wymagania
- Cztery
CheckBox— warsztaty. Nazwy i ceny trzymasz w dwóch tablicach równoległych, nie w sztywnychif-ach. RadioGroupz trzemaRadioButton— kategoria zniżki. Wartości procentowe też w tablicy.- Przycisk Podsumuj wypisuje w
TextView: ile warsztatów wybrano i które, najdroższy z wybranych, sumę przed zniżką, wybraną zniżkę i kwotę do zapłaty. - Przycisk Wyczyść odznacza wszystkie CheckBoxy (pętlą po tablicy), czyści RadioGroup i wynik.
- Gdy nie zaznaczono żadnego warsztatu albo nie wybrano zniżki —
Toastz komunikatem, bez liczenia. - Wszystkie pętle po tablicach, żadnego powielania kodu dla każdego CheckBoxa osobno.
Przykład działania
Zaznaczone: Fotografia i Robotyka, kategoria: uczeń szkoły.
Wybrano 2 z 4: Fotografia, Robotyka
Najdroższy: Robotyka (150 zł)
Suma: 270 zł
Zniżka: Uczeń szkoły (-20%)
Do zapłaty: 216 zł
Drugi przypadek do sprawdzenia: wszystkie cztery warsztaty i kategoria „Absolwent" — suma 650 zł, do zapłaty 585 zł.
Punktacja (20 pkt)
| Za co | Pkt |
|---|---|
Layout: root z android:id="@+id/main", komplet kontrolek z identyfikatorami | 3 |
| Trzy tablice danych: nazwy, ceny, zniżki | 3 |
Tablica CheckBox[] wypełniona przez findViewById | 2 |
| Pętla: licznik zaznaczonych, suma cen, lista nazw po przecinku | 4 |
| Najdroższy z zaznaczonych (nie z całej tablicy) | 2 |
Zniżka z RadioGroup i obsługa braku wyboru (-1) | 3 |
| Kwota do zapłaty policzona bez dzielenia całkowitego | 2 |
| Przycisk Wyczyść działający pętlą po tablicy | 1 |
R nie powstanie, każde R.id. będzie czerwone i zaczniesz szukać błędu tam, gdzie go nie ma.ZADANIE: Zapisy na warsztaty (Android Studio, Java, ~40 min)
Szkoła prowadzi cztery warsztaty:
Fotografia 120 zł, Programowanie 180 zł, Robotyka 150 zł, Grafika 3D 200 zł
Kategorie zniżki:
Uczeń szkoły -20%, Absolwent -10%, Osoba z zewnątrz 0%
Napisz aplikację, w której:
1. Cztery CheckBoxy to warsztaty. Nazwy i ceny trzymasz w DWÓCH TABLICACH
równoległych (String[] i int[]), nie w if-ach.
2. RadioGroup z trzema RadioButtonami to kategoria zniżki. Procenty też
w tablicy int[].
3. Przycisk "Podsumuj" wypisuje w TextView:
- ile warsztatów wybrano i które (po przecinku)
- najdroższy z WYBRANYCH
- sumę przed zniżką
- wybraną zniżkę
- kwotę do zapłaty
4. Przycisk "Wyczyść" odznacza wszystkie CheckBoxy pętlą po tablicy,
czyści RadioGroup i wynik.
5. Gdy nic nie zaznaczono albo nie wybrano zniżki - Toast z komunikatem,
bez liczenia.
Przykład: Fotografia + Robotyka, uczeń szkoły
Wybrano 2 z 4: Fotografia, Robotyka
Najdroższy: Robotyka (150 zł)
Suma: 270 zł
Zniżka: Uczeń szkoły (-20%)
Do zapłaty: 216 zł
Sprawdź też: wszystkie cztery + absolwent = 650 zł, do zapłaty 585 zł.
Punktacja 20 pkt: layout 3, tablice danych 3, CheckBox[] 2, pętla
(licznik + suma + nazwy) 4, najdroższy z wybranych 2, zniżka z RadioGroup
z obsługą braku wyboru 3, kwota bez dzielenia całkowitego 2, Wyczyść 1.
#06 — Rozwiązanie krok po kroku
Kroki rozwijasz klikając w nagłówek. Odhaczaj je dopiero, gdy kod faktycznie działa — postęp zapisuje się w przeglądarce, więc po przerwie wracasz w to samo miejsce.
Nowy projekt: Empty Views Activity, nazwa Warsztaty, język Java. Możesz też pracować w istniejącym projekcie Tablice — wtedy tylko podmieniasz zawartość activity_main.xml i MainActivity.java.
W activity_main.xml: Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout. Potem w zakładce Code ustaw atrybuty roota:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
android:id="@+id/main"
Wygenerowany onCreate zawiera ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), …). Bez tego ID findViewById zwróci null i aplikacja padnie przy starcie, zanim cokolwiek zobaczysz.orientation="vertical" ustaw zanim dodasz kontrolki
Convert view tworzy LinearLayout bez atrybutu orientation, a domyślną wartością jest horizontal. Objaw: dodajesz cztery CheckBoxy, a widać tylko pierwszy i kawałek drugiego — reszta wyjechała za prawą krawędź ekranu. Sprawdzisz to w panelu Attributes: przy zaznaczonym roocie pole orientation musi mieć vertical. Zostało po szablonie ustawione tło albo napis „Hello World"? Usuń je teraz — w tym zadaniu pierwszym elementem jest nagłówek „Wybierz warsztaty".Z palety, kategoria Buttons: cztery CheckBox, potem RadioGroup i do niej trzy RadioButton. Do tego dwa Button i TextView na wynik. Identyfikatory nadajesz od razu — bez nich nie ma findViewById.
| Kontrolka | id | text |
|---|---|---|
| CheckBox | cbFoto | Fotografia (120 zł) |
| CheckBox | cbProg | Programowanie (180 zł) |
| CheckBox | cbRobo | Robotyka (150 zł) |
| CheckBox | cbGraf | Grafika 3D (200 zł) |
| RadioGroup | rgZnizka | — (orientation="vertical") |
| RadioButton | rbUczen | Uczeń szkoły (-20%) |
| RadioButton | rbAbsolwent | Absolwent (-10%) |
| RadioButton | rbGosc | Osoba z zewnątrz (0%) |
| Button | btnPodsumuj | Podsumuj |
| Button | btnWyczysc | Wyczyść |
| TextView | txtWynik | — (puste) |
Kolejność RadioButtonów w grupie ma znaczenie — w kroku 5 pobierzesz po niej zniżkę z tablicy. Pełny XML jest w zakładce Gotowy kod.
Po dodaniu kontrolek: Build → Make Project. Dopiero teraz R.id.cbFoto przestanie być czerwone.
Trzy tablice, wszystkie w jednym miejscu na górze klasy. Nazwy i ceny są równoległe: indeks 2 to Robotyka i jej 150 zł. Zniżki są równoległe do kolejności RadioButtonów w grupie.
public class MainActivity extends AppCompatActivity {
private final String[] WARSZTATY = {"Fotografia", "Programowanie", "Robotyka", "Grafika 3D"};
private final int[] CENY = {120, 180, 150, 200};
private final int[] ZNIZKI = {20, 10, 0}; // kolejność jak RadioButtony w grupie
private CheckBox[] boxy;
private RadioGroup rgZnizka;
private TextView txtWynik;
final
Te tablice są danymi zadania, nie zmieniają się w trakcie działania. final mówi to wprost i blokuje przypadkowe podstawienie innej tablicy. Zawartość i tak można by zmienić — final chroni referencję, nie elementy.Swój kod dopisujesz pod blokiem wygenerowanym przez szablon, nie zamiast niego.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // szablon
EdgeToEdge.enable(this); // szablon
setContentView(R.layout.activity_main); // szablon
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets; // szablon
});
boxy = new CheckBox[4];
boxy[0] = findViewById(R.id.cbFoto);
boxy[1] = findViewById(R.id.cbProg);
boxy[2] = findViewById(R.id.cbRobo);
boxy[3] = findViewById(R.id.cbGraf);
rgZnizka = findViewById(R.id.rgZnizka);
txtWynik = findViewById(R.id.txtWynik);
Button btnPodsumuj = findViewById(R.id.btnPodsumuj);
Button btnWyczysc = findViewById(R.id.btnWyczysc);
btnPodsumuj.setOnClickListener(v -> podsumuj());
btnWyczysc.setOnClickListener(v -> wyczysc());
}
CheckBoxy musisz mieć w tablicy, bo każdy odpytujesz osobno: pętla woła isChecked() na czterech kontrolkach. Przy RadioButtonach pytasz raz, i to grupę — rgZnizka.getCheckedRadioButtonId() zwraca ID zaznaczonego, a samą kontrolkę wyciągasz dopiero wtedy, gdy jest potrzebna jej etykieta (krok 5):
int idZnizki = rgZnizka.getCheckedRadioButtonId();
RadioButton wybrany = findViewById(idZnizki); // referencja powstaje dopiero tu
Trzy pola rbUczen, rbAbsolwent, rbGosc byłyby martwym kodem. Miałyby sens tylko wtedy, gdybyś chciał zmieniać ich teksty z poziomu kodu, blokować pojedyncze opcje przez setEnabled(false) albo zaznaczać którąś programowo.
Kolejność w tablicy boxy musi odpowiadać kolejności w WARSZTATY i CENY — boxy[2] to Robotyka, bo WARSZTATY[2] to Robotyka. Pomylenie kolejności da działającą aplikację z błędnymi cenami, czyli najgorszy rodzaj błędu: taki, którego nie widać od razu.
Jedna pętla załatwia cztery rzeczy naraz. To jest sedno zadania — wszystko inne to obudowa.
int ile = 0;
int suma = 0;
int idxNajdrozszy = -1;
StringBuilder nazwy = new StringBuilder();
for (int i = 0; i < boxy.length; i++) {
if (boxy[i].isChecked()) {
if (ile > 0) {
nazwy.append(", ");
}
nazwy.append(WARSZTATY[i]);
suma = suma + CENY[i];
ile++;
if (idxNajdrozszy == -1 || CENY[i] > CENY[idxNajdrozszy]) {
idxNajdrozszy = i;
}
}
}
idxNajdrozszy = -1 i warunek idxNajdrozszy == -1 || sprawiają, że pierwszy zaznaczony ustawia punkt odniesienia. Gdybyś zaczął od idxNajdrozszy = 0, aplikacja twierdziłaby, że najdroższa jest Fotografia nawet wtedy, gdy nikt jej nie zaznaczył.Przecinek doklejasz przed kolejną nazwą, nigdy po każdej — inaczej wynik kończy się wiszącym „, ".
To jest miejsce, w którym RadioButton pojawia się pierwszy raz — wcześniej wystarczała sama grupa. RadioGroup zwraca ID zaznaczonego przycisku, a indexOfChild zamienia je na pozycję w grupie — a ta pozycja jest indeksem w tablicy ZNIZKI. Dzięki temu dołożenie czwartej kategorii to dopisanie jednej liczby, bez ruszania logiki.
int idZnizki = rgZnizka.getCheckedRadioButtonId();
if (idZnizki == -1) {
Toast.makeText(this, "Wybierz kategorię zniżki", Toast.LENGTH_SHORT).show();
return;
}
int pozycja = rgZnizka.indexOfChild(findViewById(idZnizki));
int procent = ZNIZKI[pozycja];
RadioButton wybrany = findViewById(idZnizki);
String nazwaZnizki = wybrany.getText().toString();
indexOfChild wydaje Ci się magiczne, napisz to warunkami — ale porównuj identyfikatory, nie teksty:
int procent;
if (idZnizki == R.id.rbUczen) {
procent = 20;
} else if (idZnizki == R.id.rbAbsolwent) {
procent = 10;
} else {
procent = 0;
}
Wersja z tablicą jest lepsza, bo trzyma dane w jednym miejscu — i dokładnie o to chodzi w dziale o tablicach.int doZaplaty = (int) Math.round(suma * (100 - procent) / 100.0);
txtWynik.setText(
"Wybrano " + ile + " z " + boxy.length + ": " + nazwy + "\n"
+ "Najdroższy: " + WARSZTATY[idxNajdrozszy] + " (" + CENY[idxNajdrozszy] + " zł)\n"
+ "Suma: " + suma + " zł\n"
+ "Zniżka: " + nazwaZnizki + "\n"
+ "Do zapłaty: " + doZaplaty + " zł");
100.0, nie 100
suma * (100 - procent) / 100 to trzy liczby całkowite — Java obetnie część dziesiętną przy dzieleniu i przy niektórych kwotach wynik będzie o złotówkę za niski. Kropka zero zamienia dzielenie na zmiennoprzecinkowe, a Math.round domyka wynik do pełnych złotych.Sprawdź na danych z instrukcji: 270 × 80 / 100.0 = 216, a 650 × 90 / 100.0 = 585. Jeśli wychodzi Ci 215 albo 584, masz gdzieś dzielenie całkowite.
private void wyczysc() {
for (int i = 0; i < boxy.length; i++) {
boxy[i].setChecked(false);
}
rgZnizka.clearCheck();
txtWynik.setText("");
}
Odznaczanie idzie pętlą po tablicy — cztery osobne setChecked(false) to ten sam efekt i stracony punkt za powielony kod.
Uruchom aplikację i przejdź tabelę. Każdy wiersz to osobne kliknięcie, nie pomijaj dwóch ostatnich — to na nich wykłada się większość rozwiązań.
| Co klikasz | Co ma się stać |
|---|---|
| Fotografia + Robotyka, uczeń szkoły | 2 z 4, najdroższy Robotyka (150 zł), suma 270 zł, do zapłaty 216 zł |
| wszystkie cztery, absolwent | 4 z 4, najdroższy Grafika 3D (200 zł), suma 650 zł, do zapłaty 585 zł |
| Programowanie, osoba z zewnątrz | 1 z 4, najdroższy Programowanie (180 zł), suma 180 zł, do zapłaty 180 zł |
| nic nie zaznaczone, przycisk Podsumuj | Toast „Zaznacz co najmniej jeden warsztat", TextView bez zmian |
| zaznaczony warsztat, żadnej kategorii | Toast „Wybierz kategorię zniżki", nic nie jest liczone |
| Wyczyść po podsumowaniu | wszystkie CheckBoxy odznaczone, kategoria odznaczona, wynik pusty |
#06 — Gotowy kod
Kompletne dwa pliki. Porównujesz z nimi swoje rozwiązanie po samodzielnej próbie — wcześniejsze zerknięcie kosztuje cały sens ćwiczenia.
res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Wybierz warsztaty"
android:textSize="18sp"
android:textStyle="bold" />
<CheckBox
android:id="@+id/cbFoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Fotografia (120 zł)" />
<CheckBox
android:id="@+id/cbProg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Programowanie (180 zł)" />
<CheckBox
android:id="@+id/cbRobo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Robotyka (150 zł)" />
<CheckBox
android:id="@+id/cbGraf"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Grafika 3D (200 zł)" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kategoria zniżki"
android:textSize="18sp"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rgZnizka"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/rbUczen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Uczeń szkoły (-20%)" />
<RadioButton
android:id="@+id/rbAbsolwent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Absolwent (-10%)" />
<RadioButton
android:id="@+id/rbGosc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Osoba z zewnątrz (0%)" />
</RadioGroup>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<Button
android:id="@+id/btnPodsumuj"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Podsumuj" />
<Button
android:id="@+id/btnWyczysc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Wyczyść" />
</LinearLayout>
<TextView
android:id="@+id/txtWynik"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="16sp" />
</LinearLayout>
MainActivity.java
package com.example.warsztaty;
import android.os.Bundle;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
private final String[] WARSZTATY = {"Fotografia", "Programowanie", "Robotyka", "Grafika 3D"};
private final int[] CENY = {120, 180, 150, 200};
private final int[] ZNIZKI = {20, 10, 0};
private CheckBox[] boxy;
private RadioGroup rgZnizka;
private TextView txtWynik;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
boxy = new CheckBox[4];
boxy[0] = findViewById(R.id.cbFoto);
boxy[1] = findViewById(R.id.cbProg);
boxy[2] = findViewById(R.id.cbRobo);
boxy[3] = findViewById(R.id.cbGraf);
rgZnizka = findViewById(R.id.rgZnizka);
txtWynik = findViewById(R.id.txtWynik);
Button btnPodsumuj = findViewById(R.id.btnPodsumuj);
Button btnWyczysc = findViewById(R.id.btnWyczysc);
btnPodsumuj.setOnClickListener(v -> podsumuj());
btnWyczysc.setOnClickListener(v -> wyczysc());
}
private void podsumuj() {
int ile = 0;
int suma = 0;
int idxNajdrozszy = -1;
StringBuilder nazwy = new StringBuilder();
for (int i = 0; i < boxy.length; i++) {
if (boxy[i].isChecked()) {
if (ile > 0) {
nazwy.append(", ");
}
nazwy.append(WARSZTATY[i]);
suma = suma + CENY[i];
ile++;
if (idxNajdrozszy == -1 || CENY[i] > CENY[idxNajdrozszy]) {
idxNajdrozszy = i;
}
}
}
if (ile == 0) {
Toast.makeText(this, "Zaznacz co najmniej jeden warsztat", Toast.LENGTH_SHORT).show();
return;
}
int idZnizki = rgZnizka.getCheckedRadioButtonId();
if (idZnizki == -1) {
Toast.makeText(this, "Wybierz kategorię zniżki", Toast.LENGTH_SHORT).show();
return;
}
int pozycja = rgZnizka.indexOfChild(findViewById(idZnizki));
int procent = ZNIZKI[pozycja];
RadioButton wybrany = findViewById(idZnizki);
String nazwaZnizki = wybrany.getText().toString();
int doZaplaty = (int) Math.round(suma * (100 - procent) / 100.0);
txtWynik.setText(
"Wybrano " + ile + " z " + boxy.length + ": " + nazwy + "\n"
+ "Najdroższy: " + WARSZTATY[idxNajdrozszy] + " (" + CENY[idxNajdrozszy] + " zł)\n"
+ "Suma: " + suma + " zł\n"
+ "Zniżka: " + nazwaZnizki + "\n"
+ "Do zapłaty: " + doZaplaty + " zł");
}
private void wyczysc() {
for (int i = 0; i < boxy.length; i++) {
boxy[i].setChecked(false);
}
rgZnizka.clearCheck();
txtWynik.setText("");
}
}
Warsztaty będzie to com.example.warsztaty, przy pracy w projekcie Tablice — com.example.tablice. Skopiowanie cudzej linii package to natychmiastowy błąd kompilacji.Najczęstsze błędy w tym zadaniu
| Objaw | Przyczyna |
|---|---|
| Aplikacja pada przy starcie | Root layoutu bez android:id="@+id/main" |
| Widać tylko pierwszy CheckBox, reszta poza ekranem | LinearLayout bez android:orientation="vertical" — domyślnie układa dzieci poziomo |
| Pada przy pierwszym kliknięciu Podsumuj | boxy[i] jest null — brak findViewById dla któregoś CheckBoxa albo literówka w ID |
| Zawsze „najdroższy: Fotografia" | idxNajdrozszy zaczyna od 0 zamiast od -1 — liczy z całej tablicy, nie z zaznaczonych |
| Kwota o złotówkę za niska | Dzielenie całkowite: / 100 zamiast / 100.0 |
| Wynik zaczyna się od przecinka albo kończy „, " | Przecinek doklejany po każdej nazwie zamiast przed kolejną |
| Zniżka nie pasuje do wybranej kategorii | Kolejność w tablicy ZNIZKI inna niż kolejność RadioButtonów w XML-u |
| Ceny nie pasują do warsztatów | Kolejność w boxy inna niż w WARSZTATY i CENY |
11. Tablice w Javie
Tablica trzyma wiele wartości tego samego typu pod jedną nazwą. W Androidzie to podstawa wszystkiego, co się powtarza: pytania quizu, pozycje listy, zestaw CheckBoxów. Zakres sprawdzianu: deklaracja, odwołanie po indeksie, length, pętle, tablice równoległe.
ListView, RecyclerView i adaptery to osobny temat, poza zakresem sprawdzianu. Tablica z tego działu żyje w MainActivity.java i najwyżej trafia do jednego TextView jako gotowy napis — tak jak w ankiecie z następnego modułu.Deklaracja i inicjalizacja
// 1. gotowe wartości od razu
int[] liczby = {4, 8, 15, 16, 23, 42};
String[] dania = {"Pizza", "Burger", "Sushi"};
// 2. pusta tablica o zadanym rozmiarze — wypełniona wartościami domyślnymi
int[] punkty = new int[5]; // pięć zer
String[] imiona = new String[3]; // trzy null-e
boolean[] flagi = new boolean[4]; // cztery false
// 3. wypełnienie po utworzeniu
punkty[0] = 10;
punkty[1] = 25;
new int[5] to zawsze pięć miejsc. Gdy liczba elementów ma rosnąć, potrzebujesz ArrayList — ale na sprawdzianie z tablic trzymaj się tablicy i z góry policz rozmiar.Indeksy i length
int[] liczby = {4, 8, 15, 16, 23, 42};
liczby.length // 6 — ile elementów (bez nawiasów!)
liczby[0] // 4 — pierwszy element
liczby[5] // 42 — ostatni
liczby[liczby.length - 1] // 42 — ostatni, gdy nie znasz rozmiaru
liczby[6] // WYJĄTEK: ArrayIndexOutOfBoundsException
| Zapis | Dotyczy | Uwaga |
|---|---|---|
tablica.length | tablica | pole, bez nawiasów |
napis.length() | String | metoda, z nawiasami |
lista.size() | ArrayList | metoda |
Pierwszy indeks to zawsze 0, ostatni to length − 1. Tablica sześcioelementowa ma indeksy 0–5 — to najczęściej mylona rzecz w całym dziale.
Pętle po tablicy
int[] liczby = {4, 8, 15, 16, 23, 42};
// klasyczna — masz indeks, możesz go użyć
int suma = 0;
for (int i = 0; i < liczby.length; i++) {
suma = suma + liczby[i];
}
// suma = 108
// for-each — krótsza, gdy indeks Ci niepotrzebny
int suma2 = 0;
for (int liczba : liczby) {
suma2 = suma2 + liczba;
}
double srednia = (double) suma / liczby.length; // 18.0
i <= liczby.length zamiast i < liczby.length — pętla wchodzi na indeks 6, którego nie ma, i aplikacja się wywala. Drugi: int srednia = suma / liczby.length — dzielenie dwóch int-ów ucina część dziesiętną, 108/6 wyjdzie równo, ale 109/6 da 18 zamiast 18,17.Szukanie w tablicy
int[] liczby = {4, 8, 15, 16, 23, 42};
int max = liczby[0]; // start od pierwszego, NIE od zera
int indeksMax = 0;
for (int i = 1; i < liczby.length; i++) {
if (liczby[i] > max) {
max = liczby[i];
indeksMax = i;
}
}
// max = 42, indeksMax = 5
// zliczanie spełniających warunek
int ileParzystych = 0;
for (int liczba : liczby) {
if (liczba % 2 == 0) ileParzystych++;
}
// 5 (4, 8, 16, 42 oraz... sprawdź sam — to zadanie 11.3)
Start od liczby[0], a nie od max = 0: gdyby wszystkie wartości były ujemne, wersja z zerem zwróciłaby zero, którego w tablicy nie ma.
Tablice równoległe
Dwie tablice tej samej długości, w których ten sam indeks opisuje tę samą rzecz. Tak zbudowany jest quiz z ćwiczenia #04 i tak wygląda większość zadań na sprawdzianie.
String[] produkty = {"chleb", "mleko", "masło", "ser"};
int[] ceny = {4, 3, 9, 22};
// najdroższy produkt
int idx = 0;
for (int i = 1; i < ceny.length; i++) {
if (ceny[i] > ceny[idx]) idx = i;
}
String tekst = produkty[idx] + " — " + ceny[idx] + " zł"; // "ser — 22 zł"
Tablica dwuwymiarowa
int[][] oceny = {
{5, 4, 3},
{2, 5, 5}
};
oceny.length // 2 — liczba wierszy
oceny[0].length // 3 — liczba kolumn w pierwszym wierszu
oceny[1][2] // 5 — drugi wiersz, trzecia kolumna
for (int w = 0; w < oceny.length; w++) {
for (int k = 0; k < oceny[w].length; k++) {
// oceny[w][k]
}
}
Pułapka: tablica to referencja
int[] a = {1, 2, 3};
int[] b = a; // NIE kopia — druga nazwa dla tej samej tablicy
b[0] = 99;
// a[0] też wynosi teraz 99
int[] c = a.clone(); // dopiero to jest kopia
c[1] = 77;
// a[1] nadal wynosi 2
To ulubione pytanie „na myślenie" na sprawdzianach: kod wygląda niewinnie, a zmiana przez b widać w a. Zmienna tablicowa przechowuje adres, nie zawartość.
Arkusz zawiera teorię i treści zadań bez rozwiązań — do wysłania uczniowi przed sprawdzianem.
TABLICE - 6 zadań. Pisz na kartce albo w komentarzu w Android Studio.
Odeślij odpowiedzi na czat, po jednym zadaniu w linii.
11.1 int[] liczby = {4, 8, 15, 16, 23, 42};
Podaj: liczby.length, liczby[0], liczby[3], liczby[liczby.length-1].
Co się stanie przy liczby[6]?
11.2 Napisz pętlę licząca sumę i średnią tablicy {4, 8, 15, 16, 23, 42}.
Średnia ma mieć część dziesiętną.
11.3 Znajdź dwa błędy i popraw:
int[] liczby = {4, 8, 15, 16, 23, 42};
int ile;
for (int i = 1; i <= liczby.length; i++) {
if (liczby[i] % 2 == 0) { ile++; }
}
11.4 int[] oceny = {3, 5, 4, 2, 5, 4, 5};
Znajdź największą ocenę, jej pierwszy indeks i liczbę piątek.
11.5 String[] produkty = {"chleb", "mleko", "masło", "ser"};
int[] ceny = {4, 3, 9, 22};
Zbuduj napis z najdroższym produktem i ceną oraz policz wartość koszyka.
11.6 Co wypisze a[0] i a[1]? Uzasadnij.
int[] a = {1, 2, 3};
int[] b = a;
int[] c = a.clone();
b[0] = 99;
c[1] = 77;
Zadania — trening przed sprawdzianem
Sześć zadań w formacie, w jakim zwykle wygląda sprawdzian: „co wypisze", „napisz fragment", „znajdź błąd". Pisz na kartce albo w komentarzu w Android Studio, dopiero potem odkrywaj rozwiązanie.
Zadanie 11.1 — indeksy i length
Dana jest tablica int[] liczby = {4, 8, 15, 16, 23, 42};. Podaj wartości: liczby.length, liczby[0], liczby[3], liczby[liczby.length - 1]. Co się stanie przy liczby[6]?
| Zapis | Wynik |
|---|---|
liczby.length | 6 |
liczby[0] | 4 |
liczby[3] | 16 |
liczby[liczby.length - 1] | 42 |
liczby[6] | ArrayIndexOutOfBoundsException — aplikacja się zamyka |
Sześć elementów oznacza indeksy 0–5. Wyjątek nie jest błędem kompilacji — kod się skompiluje i wywali dopiero przy uruchomieniu, w Logcacie zobaczysz length=6; index=6.
Zadanie 11.2 — suma i średnia
Napisz pętlę, która policzy sumę i średnią tablicy {4, 8, 15, 16, 23, 42}. Średnia ma być liczbą z częścią dziesiętną.
int[] liczby = {4, 8, 15, 16, 23, 42};
int suma = 0;
for (int i = 0; i < liczby.length; i++) {
suma = suma + liczby[i];
}
double srednia = (double) suma / liczby.length;
// suma = 108, srednia = 18.0
Bez rzutowania (double) Java policzy dzielenie całkowite. Tu akurat wyjdzie równo 18, ale to przypadek — na sprawdzianie zwykle dobierają liczby tak, żeby było widać różnicę.
Zadanie 11.3 — znajdź błąd
Ten kod miał policzyć, ile liczb w tablicy jest parzystych. Wskaż dwa błędy i popraw je.
int[] liczby = {4, 8, 15, 16, 23, 42};
int ile;
for (int i = 1; i <= liczby.length; i++) {
if (liczby[i] % 2 == 0) {
ile++;
}
}
Błąd 1: int ile; bez wartości początkowej — zmienna lokalna w Javie musi być zainicjowana przed użyciem, kompilator zgłosi variable ile might not have been initialized.
Błąd 2: pętla zaczyna od i = 1 (pomija pierwszy element) i idzie do i <= length (wchodzi na nieistniejący indeks 6).
int ile = 0;
for (int i = 0; i < liczby.length; i++) {
if (liczby[i] % 2 == 0) {
ile++;
}
}
// ile = 4 (4, 8, 16, 42)
Parzyste są cztery: 4, 8, 16, 42. Liczby 15 i 23 są nieparzyste.
Zadanie 11.4 — maksimum i jego pozycja
Dla tablicy int[] oceny = {3, 5, 4, 2, 5, 4, 5}; napisz kod znajdujący największą ocenę, jej pierwszy indeks oraz liczbę piątek.
int[] oceny = {3, 5, 4, 2, 5, 4, 5};
int max = oceny[0];
int gdzie = 0;
int ilePiatek = 0;
for (int i = 0; i < oceny.length; i++) {
if (oceny[i] > max) {
max = oceny[i];
gdzie = i;
}
if (oceny[i] == 5) {
ilePiatek++;
}
}
// max = 5, gdzie = 1, ilePiatek = 3
Warunek > (nie >=) sprawia, że gdzie wskazuje pierwsze wystąpienie maksimum. Przy >= dostałbyś indeks 6, czyli ostatnie. Na sprawdzianie czytaj polecenie: „pierwsza" czy „ostatnia" pozycja.
Zadanie 11.5 — tablice równoległe
Masz String[] produkty = {"chleb", "mleko", "masło", "ser"}; oraz int[] ceny = {4, 3, 9, 22};. Napisz kod, który zbuduje napis z nazwą najdroższego produktu i jego ceną oraz policzy wartość całego koszyka.
String[] produkty = {"chleb", "mleko", "masło", "ser"};
int[] ceny = {4, 3, 9, 22};
int idx = 0;
int suma = 0;
for (int i = 0; i < ceny.length; i++) {
suma = suma + ceny[i];
if (ceny[i] > ceny[idx]) {
idx = i;
}
}
String wynik = "Najdroższy: " + produkty[idx] + " (" + ceny[idx] + " zł), koszyk: " + suma + " zł";
// "Najdroższy: ser (22 zł), koszyk: 38 zł"
Obie tablice przechodzisz jedną pętlą, bo indeks znaczy w nich to samo. Gdyby tablice miały różną długość, pętla po dłuższej wyrzuciłaby wyjątek na krótszej — dlatego przy tablicach równoległych zawsze pilnujesz, żeby powstawały razem.
Zadanie 11.6 — co wypisze
Podaj wartości a[0] i a[1] po wykonaniu tego kodu i uzasadnij.
int[] a = {1, 2, 3};
int[] b = a;
int[] c = a.clone();
b[0] = 99;
c[1] = 77;
a[0] = 99, a[1] = 2.
b = a nie tworzy nowej tablicy — b to druga nazwa tej samej tablicy w pamięci, więc zmiana przez b jest widoczna przez a. a.clone() tworzy niezależną kopię, więc c[1] = 77 nie rusza oryginału.
Test na sprawdzianie: jeśli po przypisaniu jednej tablicy do drugiej zmiana „magicznie" widać w obu — to nie błąd, to referencja.
12. CheckBox, RadioButton, Switch
Trzy kontrolki wyboru, jedna zasada podziału: CheckBox — wiele odpowiedzi naraz, RadioButton w RadioGroup — dokładnie jedna, Switch — włącz/wyłącz. Wszystkie dziedziczą po CompoundButton, więc isChecked() działa na każdej z nich.
Paleta w Android Studio — gdzie to jest i jak się nazywa
W widoku Design pliku activity_main.xml kontrolki przeciągasz z Palette. Nazwy w palecie nie są nazwami klas w XML-u — to podpowiedzi „do czego to służy". Największa pułapka siedzi w kategorii Text: pozycja nazywa się Plain Text, a do layoutu wstawia <EditText>.
| Kategoria w palecie | Pozycja | Co ląduje w XML | Do czego |
|---|---|---|---|
| Text | TextView | <TextView> | tekst tylko do odczytu — wyświetla wynik |
| Plain Text | <EditText android:inputType="text"> | pole, w które użytkownik wpisuje tekst | |
| Password | <EditText android:inputType="textPassword"> | to samo pole, znaki zakryte kropkami | |
| E-mail / Phone / Number | <EditText> z innym inputType | zmienia klawiaturę i walidację wpisu | |
| Multiline Text | <EditText android:inputType="textMultiLine"> | pole wielolinijkowe | |
| Buttons | Button | <Button> | przycisk |
| CheckBox | <CheckBox> | wiele zaznaczeń naraz | |
| RadioGroup | <RadioGroup> | kontener pilnujący jednego wyboru | |
| RadioButton | <RadioButton> | pojedyncza opcja — wrzucasz ją do RadioGroup | |
| Switch / ToggleButton | <Switch>, <ToggleButton> | przełącznik włącz/wyłącz | |
| Layouts | LinearLayout, ConstraintLayout | <LinearLayout> itd. | układ, w którym siedzą kontrolki |
| Legacy | ListView, GridView, Spinner | <ListView> itd. | starsze listy — poza zakresem tego sprawdzianu |
Button (dokładniej: po CompoundButton, czyli przycisku z dwoma stanami), dlatego paleta trzyma je razem z przyciskami. W kategorii Text ich nie znajdziesz — tam są pola tekstowe.EditText; jeśli „aplikacja wypisuje wynik" — TextView. Przeciągnięcie Plain Text daje w XML-u <EditText> z domyślnymi android:inputType="text" i android:ems="10", a Android Studio od razu podkreśli brak android:id — bez ID nie zrobisz findViewById.Przeciągnięcie z palety to wygoda, nie wymóg — na sprawdzianie pisemnym i tak piszesz XML ręcznie. Praktyczny nawyk: przeciągasz, a potem przełączasz się na zakładkę Code i poprawiasz android:id, layout_width, layout_height i text, bo generator wstawia swoje wartości domyślne.
CheckBox — XML
<CheckBox
android:id="@+id/cbPizza"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pizza" />
<CheckBox
android:id="@+id/cbBurger"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Burger"
android:checked="true" />
CheckBox — Java
CheckBox cbPizza = findViewById(R.id.cbPizza);
if (cbPizza.isChecked()) {
// zaznaczony
}
cbPizza.setChecked(true); // zaznacz z kodu
cbPizza.toggle(); // odwróć stan
// reakcja od razu przy kliknięciu (bez przycisku)
cbPizza.setOnCheckedChangeListener((buttonView, isChecked) -> {
txtInfo.setText(isChecked ? "Pizza wybrana" : "Pizza odznaczona");
});
RadioButton — XML
RadioButtony muszą siedzieć wewnątrz RadioGroup. To grupa pilnuje, żeby zaznaczony był tylko jeden — same RadioButtony tego nie potrafią.
<RadioGroup
android:id="@+id/rgOcena"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/rbSlabo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Słabo" />
<RadioButton
android:id="@+id/rbOk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Może być" />
<RadioButton
android:id="@+id/rbSwietnie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Świetnie" />
</RadioGroup>
RadioButton — Java
RadioGroup rgOcena = findViewById(R.id.rgOcena);
int id = rgOcena.getCheckedRadioButtonId(); // ID zaznaczonego albo -1
if (id == -1) {
Toast.makeText(this, "Nic nie wybrano", Toast.LENGTH_SHORT).show();
} else {
RadioButton wybrany = findViewById(id);
String tekst = wybrany.getText().toString(); // np. "Świetnie"
}
rgOcena.clearCheck(); // odznacz wszystko w grupie
// reakcja od razu przy zmianie
rgOcena.setOnCheckedChangeListener((group, checkedId) -> {
RadioButton rb = findViewById(checkedId);
// uwaga: checkedId bywa -1 po clearCheck() — wtedy rb jest null
});
Porównanie
| CheckBox | RadioButton | Switch | |
|---|---|---|---|
| Ile można zaznaczyć | dowolnie wiele | jeden w grupie | stan włącz/wyłącz |
| Wymaga kontenera | nie | tak — RadioGroup | nie |
| Odczyt stanu | cb.isChecked() | rg.getCheckedRadioButtonId() | sw.isChecked() |
| Odznaczenie z kodu | cb.setChecked(false) | rg.clearCheck() | sw.setChecked(false) |
| Typowe użycie | składniki, zgody, filtry | ocena, płeć, jedna odpowiedź w quizie | powiadomienia, tryb ciemny |
Cztery pułapki, które wchodzą na sprawdzian
-1
getCheckedRadioButtonId() zwraca -1, gdy nic nie jest zaznaczone. findViewById(-1) zwróci null i przy .getText() aplikacja padnie z NullPointerException. Sprawdzenie if (id == -1) jest obowiązkowe.CheckBox.setOnCheckedChangeListener dostaje (buttonView, isChecked) — drugi argument to boolean. RadioGroup.setOnCheckedChangeListener dostaje (group, checkedId) — drugi argument to int z ID. Podstawienie jednego pod drugie nie skompiluje się.if (wybrany.getText() == "Świetnie") nie zadziała — w Javie == na napisach porównuje referencje. Albo .equals(), albo, lepiej, porównanie ID: if (id == R.id.rbSwietnie).Wzorzec do zapamiętania: CheckBoxy w tablicy
To jest miejsce, w którym stykają się oba tematy sprawdzianu. Zamiast trzech osobnych if-ów wrzucasz kontrolki do tablicy i lecisz pętlą — ten sam indeks opisuje CheckBox i jego etykietę.
private final String[] DANIA = {"Pizza", "Burger", "Sushi"};
private CheckBox[] boxy;
boxy = new CheckBox[3];
boxy[0] = findViewById(R.id.cbPizza);
boxy[1] = findViewById(R.id.cbBurger);
boxy[2] = findViewById(R.id.cbSushi);
int ile = 0;
StringBuilder wybrane = new StringBuilder();
for (int i = 0; i < boxy.length; i++) {
if (boxy[i].isChecked()) {
if (ile > 0) wybrane.append(", ");
wybrane.append(DANIA[i]);
ile++;
}
}
Mini-aplikacja „Ankieta" — całość razem
Projekt Tablice, pakiet com.example.tablice. Trzy CheckBoxy w tablicy, RadioGroup z oceną, przycisk zbierający wynik do TextView. Napisz to samodzielnie, a dopiero potem porównaj z wzorcem.
res/layout/activity_main.xml
android:id="@+id/main"
Szablon Empty Views Activity generuje w onCreate linię ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), …). Jeśli podmienisz wygenerowany ConstraintLayout na własny LinearLayout i zgubisz to ID, findViewById(R.id.main) zwróci null, a aplikacja wywali się od razu przy starcie, zanim cokolwiek zobaczysz. Objaw w Logcacie: NullPointerException w onCreate, w linii z setOnApplyWindowInsetsListener.<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Co zamawiasz?"
android:textSize="18sp"
android:textStyle="bold" />
<CheckBox
android:id="@+id/cbPizza"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pizza" />
<CheckBox
android:id="@+id/cbBurger"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Burger" />
<CheckBox
android:id="@+id/cbSushi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sushi" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Jak oceniasz obsługę?"
android:textSize="18sp"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rgOcena"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/rbSlabo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Słabo" />
<RadioButton
android:id="@+id/rbOk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Może być" />
<RadioButton
android:id="@+id/rbSwietnie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Świetnie" />
</RadioGroup>
<Button
android:id="@+id/btnPodsumuj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Podsumuj" />
<TextView
android:id="@+id/txtWynik"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="16sp" />
</LinearLayout>
MainActivity.java — co dopisujesz do wygenerowanego szablonu
Android Studio tworzy plik z gotowym onCreate: EdgeToEdge.enable(this), setContentView i listener od marginesów systemowych (pasek stanu, pasek nawigacji). Nie kasuj tego — swój kod dopisujesz pod spodem, wewnątrz onCreate. Linie z szablonu zostawiłem z komentarzem, żeby było widać, co jest Twoje.
package com.example.tablice;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
// ↓ TWOJE: tablica etykiet i tablica kontrolek — ten sam indeks opisuje to samo
private final String[] DANIA = {"Pizza", "Burger", "Sushi"};
private CheckBox[] boxy;
private RadioGroup rgOcena;
private TextView txtWynik;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // szablon
EdgeToEdge.enable(this); // szablon
setContentView(R.layout.activity_main); // szablon — ładowanie layoutu
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets; // szablon — marginesy pod paski systemowe
});
// ↓ od tego miejsca piszesz sam
boxy = new CheckBox[3];
boxy[0] = findViewById(R.id.cbPizza);
boxy[1] = findViewById(R.id.cbBurger);
boxy[2] = findViewById(R.id.cbSushi);
rgOcena = findViewById(R.id.rgOcena);
txtWynik = findViewById(R.id.txtWynik);
Button btnPodsumuj = findViewById(R.id.btnPodsumuj);
btnPodsumuj.setOnClickListener(v -> pokazPodsumowanie());
} // koniec onCreate
private void pokazPodsumowanie() {
StringBuilder wybrane = new StringBuilder();
int ile = 0;
for (int i = 0; i < boxy.length; i++) {
if (boxy[i].isChecked()) {
if (ile > 0) {
wybrane.append(", ");
}
wybrane.append(DANIA[i]);
ile++;
}
}
if (ile == 0) {
wybrane.append("nic nie wybrano");
}
String ocena;
int id = rgOcena.getCheckedRadioButtonId();
if (id == -1) {
ocena = "brak oceny";
} else {
RadioButton wybrany = findViewById(id);
ocena = wybrany.getText().toString();
}
txtWynik.setText("Wybrano (" + ile + "): " + wybrane + "\nOcena: " + ocena);
}
}
CheckBox[] i tablica String[] przechodzone jedną pętlą, isChecked() w warunku, obsługa -1 przy RadioGroup i getText().toString() zamiast porównywania napisów operatorem ==. Cztery rzeczy, cztery punkty.Kolejność pracy w Android Studio — żeby nie gonić błędów
| Krok | Co robisz | Na czym się wywala, gdy pominiesz |
|---|---|---|
| 1 | W activity_main.xml ustawiasz root na LinearLayout z android:id="@+id/main" i orientation="vertical" | Brak ID — NullPointerException przy starcie, w linii z insetami |
| 2 | Wstawiasz kontrolki (paleta → Buttons: CheckBox, RadioGroup, RadioButton) i nadajesz każdej android:id | Bez ID nie ma R.id.…, kod się nie skompiluje |
| 3 | Budujesz projekt (Build → Make Project), żeby powstała klasa R | R.id.cbPizza świeci na czerwono, choć XML jest poprawny |
| 4 | W onCreate, pod kodem z szablonu, robisz findViewById i wypełniasz tablicę boxy | boxy[0] zostaje null — wyjątek przy pierwszym kliknięciu |
| 5 | Piszesz pokazPodsumowanie() jako osobną metodę poza onCreate | Metoda w metodzie — błąd składni, którego komunikat niewiele mówi |
R.id. to zwykle nie błąd w kodzie
Dopóki Gradle sync albo build nie przejdzie, klasa R nie istnieje i edytor podkreśla każde odwołanie do zasobu. Najpierw zbuduj projekt, dopiero potem szukaj literówki. Zwinięta linia import … u góry pliku rozwija się kliknięciem w strzałkę — tam sprawdzasz, czy Android Studio dopisało importy (Alt+Enter na czerwonej nazwie klasy dodaje brakujący).Arkusz zawiera teorię, wzorcowy kod ankiety i treści zadań bez rozwiązań.
CHECKBOX I RADIOBUTTON - 5 zadań. Zadania 12.1-12.3 pisemnie,
12.4 i 12.5 w Android Studio na projekcie Tablice.
12.1 Jakiej kontrolki użyjesz i dlaczego:
(a) wybór jednej formy płatności
(b) zaznaczenie dodatków do pizzy
(c) zgoda na regulamin
(d) przełącznik trybu ciemnego
(e) wybór jednej poprawnej odpowiedzi w quizie
12.2 Napisz fragment XML: grupa trzech RadioButtonów pionowo
(Słabo, Może być, Świetnie), z id, pierwszy zaznaczony na starcie.
12.3 Aplikacja wywala się przy kliknięciu, gdy nic nie zaznaczono.
Wskaż przyczynę i popraw:
int id = rgOcena.getCheckedRadioButtonId();
RadioButton wybrany = findViewById(id);
txtWynik.setText("Ocena: " + wybrany.getText());
12.4 Masz cbPizza, cbBurger, cbSushi oraz
String[] DANIA = {"Pizza", "Burger", "Sushi"};
Napisz metodę zwracającą liczbę zaznaczonych i ich nazwy po przecinku -
przez tablicę CheckBoxów i jedną pętlę.
12.5 Rozbuduj ankietę: pokaż procent zaznaczonych dań (2 z 3 = 67%),
a przy braku oceny wyświetl Toast zamiast wpisu w TextView.
12.6 Uczeń przeciągnął z palety: Plain Text, CheckBox, RadioGroup,
a potem dwa razy RadioButton.
(a) Jakie znaczniki pojawią się w XML?
(b) W której kategorii palety znajdzie CheckBox?
(c) Czym różni się efekt przeciągnięcia TextView od Plain Text?
(d) Czego brakuje w wygenerowanym kodzie, żeby użyć findViewById?
12.7 Uczeń podmienił wygenerowany ConstraintLayout na własny LinearLayout.
Aplikacja zamyka się natychmiast po starcie, w Logcacie
NullPointerException w onCreate. Kod w onCreate to niezmieniony
szablon plus findViewById kontrolek. Gdzie jest przyczyna
i jak ją usunąć?
Zadania — trening przed sprawdzianem
Pięć zadań. Pierwsze trzy pisemnie, dwa ostatnie w Android Studio na otwartym projekcie Tablice.
Zadanie 12.1 — wybór kontrolki
Dla każdego ekranu napisz, jakiej kontrolki użyjesz i dlaczego: (a) wybór jednej formy płatności, (b) zaznaczenie dodatków do pizzy, (c) zgoda na regulamin, (d) przełącznik trybu ciemnego, (e) wybór jednej poprawnej odpowiedzi w quizie.
| Ekran | Kontrolka | Dlaczego |
|---|---|---|
| (a) forma płatności | RadioButton w RadioGroup | dokładnie jedna opcja |
| (b) dodatki do pizzy | CheckBox | wiele naraz |
| (c) zgoda na regulamin | CheckBox | pojedynczy stan tak/nie |
| (d) tryb ciemny | Switch | włącznik ustawienia, działa od razu |
| (e) odpowiedź w quizie | RadioButton w RadioGroup | jedna odpowiedź, reszta ma się odznaczać sama |
Zadanie 12.2 — XML
Napisz fragment XML: grupa trzech RadioButtonów ustawionych pionowo (Słabo, Może być, Świetnie), z identyfikatorami, w której pierwszy jest zaznaczony na starcie.
<RadioGroup
android:id="@+id/rgOcena"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/rbSlabo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Słabo"
android:checked="true" />
<RadioButton
android:id="@+id/rbOk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Może być" />
<RadioButton
android:id="@+id/rbSwietnie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Świetnie" />
</RadioGroup>
Trzy rzeczy punktowane: RadioGroup jako rodzic, android:orientation="vertical", android:checked="true" tylko przy jednym elemencie. Każdy element musi mieć layout_width i layout_height — ich brak to błąd kompilacji zasobów.
Zadanie 12.3 — znajdź błąd
Aplikacja wywala się przy kliknięciu przycisku, gdy użytkownik nic nie zaznaczył. Wskaż przyczynę i popraw kod.
int id = rgOcena.getCheckedRadioButtonId();
RadioButton wybrany = findViewById(id);
txtWynik.setText("Ocena: " + wybrany.getText());
Gdy nic nie jest zaznaczone, getCheckedRadioButtonId() zwraca -1, findViewById(-1) daje null, a null.getText() to NullPointerException.
int id = rgOcena.getCheckedRadioButtonId();
if (id == -1) {
txtWynik.setText("Nie wybrano oceny");
} else {
RadioButton wybrany = findViewById(id);
txtWynik.setText("Ocena: " + wybrany.getText().toString());
}
W Logcacie zobaczysz Attempt to invoke virtual method … on a null object reference — to zdanie zawsze znaczy „coś było null-em", a nie „błąd w XML".
Zadanie 12.4 — CheckBoxy w tablicy
W projekcie masz trzy CheckBoxy: cbPizza, cbBurger, cbSushi i tablicę String[] DANIA = {"Pizza", "Burger", "Sushi"};. Napisz metodę, która zwróci liczbę zaznaczonych i ich nazwy po przecinku — używając tablicy CheckBoxów i jednej pętli.
CheckBox[] boxy = new CheckBox[3];
boxy[0] = findViewById(R.id.cbPizza);
boxy[1] = findViewById(R.id.cbBurger);
boxy[2] = findViewById(R.id.cbSushi);
StringBuilder wybrane = new StringBuilder();
int ile = 0;
for (int i = 0; i < boxy.length; i++) {
if (boxy[i].isChecked()) {
if (ile > 0) {
wybrane.append(", ");
}
wybrane.append(DANIA[i]);
ile++;
}
}
txtWynik.setText("Wybrano " + ile + ": " + wybrane);
Przecinek dokładasz przed kolejnym elementem, a nie po każdym — inaczej dostaniesz „Pizza, Sushi, " z wiszącym przecinkiem. Ten drobiazg bywa osobnym punktem.
Zadanie 12.5 — w Android Studio
Rozbuduj ankietę: po kliknięciu przycisku wynik ma dodatkowo pokazać, ile procent dań zostało zaznaczonych (np. 2 z 3 to 67%), a przy braku oceny wyświetlić Toast zamiast wpisu w TextView.
int procent = (int) Math.round(100.0 * ile / boxy.length);
int id = rgOcena.getCheckedRadioButtonId();
if (id == -1) {
Toast.makeText(this, "Zaznacz ocenę", Toast.LENGTH_SHORT).show();
return;
}
RadioButton wybrany = findViewById(id);
txtWynik.setText("Wybrano " + ile + " z " + boxy.length + " (" + procent + "%)"
+ "\nOcena: " + wybrany.getText().toString());
100.0 zamiast 100 — inaczej dzielenie całkowite zwróci 0 dla każdego wyniku poniżej 100%. To ta sama pułapka, co przy średniej z zadania 11.2. Do Toast potrzebny jest import android.widget.Toast.
Zadanie 12.6 — paleta a XML
Uczeń przeciągnął z palety: Plain Text, CheckBox, RadioGroup, a potem dwa razy RadioButton. (a) Jakie znaczniki pojawią się w XML-u? (b) W której kategorii palety znajdzie CheckBox? (c) Czym różni się efekt przeciągnięcia TextView od Plain Text? (d) Czego brakuje w wygenerowanym kodzie, żeby dało się użyć findViewById?
(a) <EditText> (Plain Text to EditText z android:inputType="text"), <CheckBox>, <RadioGroup> oraz dwa <RadioButton> — te ostatnie trzeba przeciągnąć na RadioGroup, inaczej wylądują obok niej i wybór jednej opcji przestanie działać.
(b) W kategorii Buttons — razem z RadioButton, Switch i ToggleButton, bo wszystkie dziedziczą po CompoundButton.
(c) TextView tylko wyświetla tekst, EditText pozwala go wpisać. Ta sama różnica co między etykietą a polem formularza.
(d) Brakuje android:id="@+id/…". Bez identyfikatora nie ma czego podać do findViewById, a Android Studio pokazuje ostrzeżenie już w widoku Design.
Zadanie 12.7 — aplikacja pada przy starcie
Uczeń podmienił wygenerowany ConstraintLayout na własny LinearLayout i przepisał kontrolki. Aplikacja zamyka się natychmiast po uruchomieniu, jeszcze zanim cokolwiek pokaże. W Logcacie widać NullPointerException w onCreate. Kod w onCreate to niezmieniony szablon plus findViewById kontrolek. Gdzie jest przyczyna i jak ją usunąć?
Szablon Empty Views Activity zawiera linię:
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { ... });
Odwołuje się do widoku o ID main — tak nazywa się root wygenerowanego layoutu. Po podmianie roota na własny LinearLayout bez android:id="@+id/main" findViewById zwraca null, a przekazanie null do listenera wysypuje aplikację przy starcie.
Rozwiązanie: dopisać android:id="@+id/main" do roota layoutu. Alternatywnie usunąć cały blok z insetami, ale wtedy górna część interfejsu wejdzie pod pasek stanu — lepiej zostawić szablon w spokoju i nadać ID.
Wniosek na sprawdzian: wyjątek przy starcie szukaj w onCreate i w zgodności ID między XML-em a kodem. Wyjątek przy kliknięciu — w metodzie obsługującej przycisk.
Sprawdzian próbny — tablice, CheckBox, RadioButton
15 pytań zamkniętych (po 1 pkt) i 2 zadania kodowe (po 4 pkt) — razem 23 pkt. Czas 15 minut na część zamkniętą. Próg zaliczenia 60%, czyli 14 pkt.
Arkusz dla ucznia to czysty sprawdzian: 12 pytań i 2 zadania kodowe, bez klucza.
SPRAWDZIAN PRÓBNY, część 1 - 15 pytań po 1 pkt, 15 minut.
Odeślij jedną linią, np.: 1b 2c 3a 4d 5b 6c 7a 8d 9b 10c 11a 12b 13b 14c 15b
1. Ile elementów ma tablica int[] t = new int[5]; i jaka jest wartość t[0]?
a) 5 elementów, t[0] = null b) 5 elementów, t[0] = 0
c) 4 elementy, t[0] = 0 d) 0 elementów, t[0] = błąd
2. Jaki jest ostatni poprawny indeks tablicy o 8 elementach?
a) 8 b) 7 c) 9 d) zależy od typu
3. Co zwróci liczby.length dla int[] liczby = {4, 8, 15}?
a) 2 b) 3 c) 15 d) błąd, bo length to metoda
4. Który zapis jest poprawny dla tablicy, a który dla napisu?
a) tablica.length() i napis.length b) tablica.size() i napis.length()
c) tablica.length i napis.length() d) oba mają length()
5. Co się stanie po uruchomieniu:
int[] t = {1, 2, 3}; System.out.println(t[3]);
a) wypisze 3 b) wypisze 0
c) błąd kompilacji d) ArrayIndexOutOfBoundsException
6. Ile razy wykona się pętla: for (int i = 0; i < t.length; i++) dla t o 6 elementach?
a) 5 b) 6 c) 7 d) zależy od zawartości
7. int[] a = {1,2,3}; int[] b = a; b[0] = 9; Ile wynosi a[0]?
a) 1 b) 9 c) 0 d) błąd kompilacji
8. Czym musi być otoczony RadioButton, żeby zaznaczał się tylko jeden?
a) LinearLayout b) CheckBoxGroup c) RadioGroup d) niczym, robi to sam
9. Jak sprawdzić, czy CheckBox cbZgoda jest zaznaczony?
a) cbZgoda.getChecked() b) cbZgoda.isChecked()
c) cbZgoda.isSelected() d) cbZgoda.getState()
10. Co zwraca rg.getCheckedRadioButtonId(), gdy nic nie jest zaznaczone?
a) 0 b) null c) -1 d) rzuca wyjątek
11. Jak odznaczyć wszystkie RadioButtony w grupie rgOcena?
a) rgOcena.clearCheck() b) rgOcena.setChecked(false)
c) rgOcena.reset() d) rgOcena.removeAllViews()
12. Który atrybut XML zaznacza kontrolkę już przy starcie aplikacji?
a) android:selected="true" b) android:checked="true"
c) android:enabled="true" d) android:default="true"
13. Przeciągasz z palety pozycję "Plain Text". Co pojawi się w XML?
a) <TextView> b) <EditText> c) <PlainText> d) <TextInputLayout>
14. W której kategorii palety znajdziesz CheckBox i RadioButton?
a) Text b) Widgets c) Buttons d) Containers
15. Szablon Empty Views Activity ma w onCreate linię
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), ...).
Co się stanie, gdy root Twojego layoutu nie ma android:id="@+id/main"?
a) nic, to tylko ostrzeżenie
b) aplikacja wywali się przy starcie (NullPointerException)
c) aplikacja wywali się przy pierwszym kliknięciu
d) projekt się nie skompiluje
SPRAWDZIAN PRÓBNY, część 2 - 2 zadania po 4 pkt.
Z1. Dane: String[] uczniowie = {"Ala", "Bartek", "Cela", "Dawid"};
int[] punkty = {32, 45, 28, 45};
Napisz kod, który znajdzie ucznia z najwyższą liczbą punktów
(pierwszego z najlepszych) i policzy średnią punktów jako liczbę
z częścią dziesiętną.
Z2. W layoucie są trzy CheckBoxy (cbA, cbB, cbC) i RadioGroup rgPoziom
z dwoma RadioButtonami. Napisz metodę podsumuj(), która wypisze
w txtWynik liczbę zaznaczonych CheckBoxów (użyj tablicy CheckBox[])
oraz tekst zaznaczonego RadioButtona, a gdy nic nie zaznaczono
w grupie - tekst "brak poziomu".
Część 2 — zadania kodowe (po 4 pkt)
Zadanie Z1 — tablice równoległe (4 pkt)
Dane: String[] uczniowie = {"Ala", "Bartek", "Cela", "Dawid"}; oraz int[] punkty = {32, 45, 28, 45};. Znajdź ucznia z najwyższą liczbą punktów (pierwszego z najlepszych) i policz średnią punktów jako liczbę z częścią dziesiętną.
String[] uczniowie = {"Ala", "Bartek", "Cela", "Dawid"};
int[] punkty = {32, 45, 28, 45};
int best = 0;
int suma = 0;
for (int i = 0; i < punkty.length; i++) {
suma = suma + punkty[i];
if (punkty[i] > punkty[best]) {
best = i;
}
}
double srednia = (double) suma / punkty.length;
String wynik = "Najlepszy: " + uczniowie[best] + " (" + punkty[best] + " pkt), "
+ "średnia: " + srednia;
// Najlepszy: Bartek (45 pkt), średnia: 37.5
Punktacja: 1 pkt — poprawna pętla po length; 1 pkt — warunek > dający pierwszego z najlepszych (Bartek, nie Dawid); 1 pkt — suma; 1 pkt — rzutowanie na double (37.5, nie 37).
Zadanie Z2 — CheckBoxy w tablicy plus RadioGroup (4 pkt)
W layoucie są trzy CheckBoxy (cbA, cbB, cbC) i RadioGroup rgPoziom z dwoma RadioButtonami. Napisz metodę podsumuj(), która wypisze w txtWynik liczbę zaznaczonych CheckBoxów (przez tablicę CheckBox[]) oraz tekst zaznaczonego RadioButtona, a gdy w grupie nic nie wybrano — tekst „brak poziomu".
private void podsumuj() {
CheckBox[] boxy = new CheckBox[3];
boxy[0] = findViewById(R.id.cbA);
boxy[1] = findViewById(R.id.cbB);
boxy[2] = findViewById(R.id.cbC);
int ile = 0;
for (int i = 0; i < boxy.length; i++) {
if (boxy[i].isChecked()) {
ile++;
}
}
RadioGroup rgPoziom = findViewById(R.id.rgPoziom);
String poziom;
int id = rgPoziom.getCheckedRadioButtonId();
if (id == -1) {
poziom = "brak poziomu";
} else {
RadioButton wybrany = findViewById(id);
poziom = wybrany.getText().toString();
}
TextView txtWynik = findViewById(R.id.txtWynik);
txtWynik.setText("Zaznaczono: " + ile + "\nPoziom: " + poziom);
}
Punktacja: 1 pkt — tablica CheckBox[] wypełniona przez findViewById; 1 pkt — pętla z isChecked(); 1 pkt — getCheckedRadioButtonId() z obsługą -1; 1 pkt — getText().toString() i wypisanie obu wyników.
Klucz części zamkniętej
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| b | b | b | c | d | b | b | c | b | c | a | b | b | c | b |
Punktacja: część zamknięta 15 pkt, kodowa 8 pkt, razem 23. Próg 60%, czyli 14 pkt. Pytania 5, 7 i 10 to trzy najczęstsze pułapki (wyjście poza zakres tablicy, referencja zamiast kopii, -1 z RadioGroup) — błąd w którymkolwiek oznacza powtórkę tego fragmentu przed szkolnym sprawdzianem, niezależnie od wyniku ogólnego.
Wersja interaktywna — do samodzielnego powtórzenia
int[] t = new int[5]; i jaka jest wartość t[0]?liczby.length dla int[] liczby = {4, 8, 15};?int[] t = {1, 2, 3}; System.out.println(t[3]);?for (int i = 0; i < t.length; i++) dla tablicy o 6 elementach?int[] a = {1,2,3}; int[] b = a; b[0] = 9; — ile wynosi a[0]?CheckBox cbZgoda jest zaznaczony?rg.getCheckedRadioButtonId(), gdy nic nie jest zaznaczone?rgOcena?onCreate linię ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), …). Co się stanie, gdy root Twojego layoutu nie ma android:id="@+id/main"?🔥 Kalkulator — Rozgrzewka
„Prosty kalkulator na liczbach całkowitych. Poznamy LinearLayout, Button i onClick."
Mini Quiz
- Czym różni się
match_parentodwrap_content? - Co robi
layout_weight? - Jak połączyć Button z XML z kodem Java?
- Co to jest
setOnClickListener?
match_parent— rozciąga na całą szerokość rodzica;wrap_content— dopasowuje do treści- Rozdziela dostępne miejsce proporcjonalnie między elementy
Button btn = findViewById(R.id.mojPrzycisk);- Metoda podpinająca kod do zdarzenia kliknięcia przycisku
📋 Kalkulator — Instrukcja
„Prosty kalkulator na liczbach całkowitych: + - * / = i C."
Treść zadania
Wykonaj aplikację "Kalkulator" — tylko liczby całkowite:
- Wyświetlacz (TextView) — pokazuje aktualną liczbę
- Przyciski cyfr 0–9
- 4 operacje: + − * /
- Przycisk = (oblicz wynik)
- Przycisk C (wyczyść wszystko)
- Dzielenie przez 0 → komunikat "Błąd"
- Dzielenie całkowite (np. 7 / 2 = 3)
Układ przycisków
┌──────────────────────┐
│ 0 │ ← wyświetlacz
│ [ 7 ][ 8 ][ 9 ][ / ]│
│ [ 4 ][ 5 ][ 6 ][ * ]│
│ [ 1 ][ 2 ][ 3 ][ - ]│
│ [ C ][ 0 ][ = ][ + ]│
└──────────────────────┘
Kolory
| Element | Kolor tła | Kolor tekstu |
|---|---|---|
| Cyfry (0-9) | #333333 | #FFFFFF |
| Operacje (+ − * / =) | #FF9500 | #FFFFFF |
| C (czyszczenie) | #A5A5A5 | #000000 |
🏗️ Kalkulator — Budowa UI
activity_main.xml
Kluczowe zasady:
- Root:
LinearLayout (vertical)zpadding="16dp" - Wyświetlacz:
TextViewztextSize="48sp", wyrównanie do prawej - 4 rzędy po 4 przyciski (siatka 4×4)
- Każdy rząd:
LinearLayout (horizontal)zlayout_height="0dp"+layout_weight="1" - Każdy przycisk:
layout_width="0dp"+layout_weight="1"(równa szerokość)
Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout
Ustaw: orientation="vertical", padding="16dp"
Palette → Text → TextView:
id | wyswietlacz |
text | 0 |
textSize | 48sp |
textAlignment | textEnd |
layout_width | match_parent |
padding | 20dp |
layout_marginBottom | 16dp |
Dodaj LinearLayout (horizontal) z layout_height="0dp", layout_weight="1"
Wewnątrz 4 Button (każdy: layout_width="0dp", layout_weight="1", layout_margin="4dp", textSize="24sp"):
| id | text | backgroundTint |
|---|---|---|
btn7 | 7 | #333333 |
btn8 | 8 | #333333 |
btn9 | 9 | #333333 |
btnDzielenie | / | #FF9500 |
Skopiuj rząd 1 trzy razy (Copy → Paste with new IDs). Zmień:
Rząd 2: btn4(4), btn5(5), btn6(6), btnMnozenie(*)
Rząd 3: btn1(1), btn2(2), btn3(3), btnOdejmowanie(-)
Rząd 4: btnC(C, tło:#A5A5A5), btn0(0), btnRowna(=, tło:#FF9500), btnDodawanie(+, tło:#FF9500)
private TextView wyswietlacz;
private int liczbaA = 0;
private int liczbaB = 0;
private String operacja = "";
private boolean nowaLiczba = true;
// w onCreate:
wyswietlacz = findViewById(R.id.wyswietlacz);
Button btn0 = findViewById(R.id.btn0);
// ... btn1-btn9
Button btnDodawanie = findViewById(R.id.btnDodawanie);
Button btnOdejmowanie = findViewById(R.id.btnOdejmowanie);
Button btnMnozenie = findViewById(R.id.btnMnozenie);
Button btnDzielenie = findViewById(R.id.btnDzielenie);
Button btnRowna = findViewById(R.id.btnRowna);
Button btnC = findViewById(R.id.btnC);
// Jeden listener dla cyfr 0-9
View.OnClickListener kliknijCyfre = v -> {
String cyfra = ((Button) v).getText().toString();
String tekst = wyswietlacz.getText().toString();
if (nowaLiczba || tekst.equals("0")) {
wyswietlacz.setText(cyfra);
nowaLiczba = false;
} else {
wyswietlacz.setText(tekst + cyfra);
}
};
btn0.setOnClickListener(kliknijCyfre);
btn1.setOnClickListener(kliknijCyfre);
// ... btn2-btn9
// Operacje
View.OnClickListener kliknijOp = v -> {
liczbaA = Integer.parseInt(wyswietlacz.getText().toString());
operacja = ((Button) v).getText().toString();
nowaLiczba = true;
};
btnDodawanie.setOnClickListener(kliknijOp);
btnOdejmowanie.setOnClickListener(kliknijOp);
btnMnozenie.setOnClickListener(kliknijOp);
btnDzielenie.setOnClickListener(kliknijOp);
// Równa się
btnRowna.setOnClickListener(v -> {
liczbaB = Integer.parseInt(wyswietlacz.getText().toString());
int wynik = 0;
switch (operacja) {
case "+": wynik = liczbaA + liczbaB; break;
case "-": wynik = liczbaA - liczbaB; break;
case "*": wynik = liczbaA * liczbaB; break;
case "/":
if (liczbaB == 0) { wyswietlacz.setText("Błąd"); return; }
wynik = liczbaA / liczbaB; break;
}
wyswietlacz.setText(String.valueOf(wynik));
nowaLiczba = true;
});
// Czyszczenie
btnC.setOnClickListener(v -> {
wyswietlacz.setText("0");
liczbaA = 0; operacja = ""; nowaLiczba = true;
});
Kliknij ▶ Run. Przetestuj: 5 + 3 = 8, 10 / 3 = 3 (całkowite!), 5 / 0 = Błąd, C = reset.
💻 Kalkulator — Gotowy kod
„Uproszczony kalkulator — tylko int, tylko + - * / = C."
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- Wyświetlacz -->
<TextView
android:id="@+id/wyswietlacz"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0"
android:textSize="48sp"
android:textAlignment="textEnd"
android:padding="20dp"
android:layout_marginBottom="16dp"/>
<!-- Rząd 1: 7 8 9 / -->
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp"
android:layout_weight="1" android:gravity="center">
<Button android:id="@+id/btn7" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="7" android:textSize="24sp"/>
<Button android:id="@+id/btn8" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="8" android:textSize="24sp"/>
<Button android:id="@+id/btn9" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="9" android:textSize="24sp"/>
<Button android:id="@+id/btnDzielenie" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="/" android:textSize="24sp"
android:backgroundTint="#FF9500" android:textColor="#FFFFFF"/>
</LinearLayout>
<!-- Rząd 2: 4 5 6 * -->
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp"
android:layout_weight="1" android:gravity="center">
<Button android:id="@+id/btn4" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="4" android:textSize="24sp"/>
<Button android:id="@+id/btn5" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="5" android:textSize="24sp"/>
<Button android:id="@+id/btn6" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="6" android:textSize="24sp"/>
<Button android:id="@+id/btnMnozenie" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="*" android:textSize="24sp"
android:backgroundTint="#FF9500" android:textColor="#FFFFFF"/>
</LinearLayout>
<!-- Rząd 3: 1 2 3 - -->
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp"
android:layout_weight="1" android:gravity="center">
<Button android:id="@+id/btn1" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="1" android:textSize="24sp"/>
<Button android:id="@+id/btn2" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="2" android:textSize="24sp"/>
<Button android:id="@+id/btn3" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="3" android:textSize="24sp"/>
<Button android:id="@+id/btnOdejmowanie" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="-" android:textSize="24sp"
android:backgroundTint="#FF9500" android:textColor="#FFFFFF"/>
</LinearLayout>
<!-- Rząd 4: C 0 = + -->
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp"
android:layout_weight="1" android:gravity="center">
<Button android:id="@+id/btnC" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="C" android:textSize="24sp"
android:backgroundTint="#A5A5A5"/>
<Button android:id="@+id/btn0" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="0" android:textSize="24sp"/>
<Button android:id="@+id/btnRowna" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="=" android:textSize="24sp"
android:backgroundTint="#FF9500" android:textColor="#FFFFFF"/>
<Button android:id="@+id/btnDodawanie" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="+" android:textSize="24sp"
android:backgroundTint="#FF9500" android:textColor="#FFFFFF"/>
</LinearLayout>
</LinearLayout>
MainActivity.java
package com.example.kalkulator;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.view.View;
public class MainActivity extends AppCompatActivity {
private TextView wyswietlacz;
private int liczbaA = 0;
private String operacja = "";
private boolean nowaLiczba = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
wyswietlacz = findViewById(R.id.wyswietlacz);
Button btn0 = findViewById(R.id.btn0);
Button btn1 = findViewById(R.id.btn1);
Button btn2 = findViewById(R.id.btn2);
Button btn3 = findViewById(R.id.btn3);
Button btn4 = findViewById(R.id.btn4);
Button btn5 = findViewById(R.id.btn5);
Button btn6 = findViewById(R.id.btn6);
Button btn7 = findViewById(R.id.btn7);
Button btn8 = findViewById(R.id.btn8);
Button btn9 = findViewById(R.id.btn9);
Button btnDodawanie = findViewById(R.id.btnDodawanie);
Button btnOdejmowanie = findViewById(R.id.btnOdejmowanie);
Button btnMnozenie = findViewById(R.id.btnMnozenie);
Button btnDzielenie = findViewById(R.id.btnDzielenie);
Button btnRowna = findViewById(R.id.btnRowna);
Button btnC = findViewById(R.id.btnC);
// Cyfry 0-9 — jeden listener
View.OnClickListener kliknijCyfre = v -> {
String cyfra = ((Button) v).getText().toString();
String tekst = wyswietlacz.getText().toString();
if (nowaLiczba || tekst.equals("0")) {
wyswietlacz.setText(cyfra);
nowaLiczba = false;
} else {
wyswietlacz.setText(tekst + cyfra);
}
};
btn0.setOnClickListener(kliknijCyfre);
btn1.setOnClickListener(kliknijCyfre);
btn2.setOnClickListener(kliknijCyfre);
btn3.setOnClickListener(kliknijCyfre);
btn4.setOnClickListener(kliknijCyfre);
btn5.setOnClickListener(kliknijCyfre);
btn6.setOnClickListener(kliknijCyfre);
btn7.setOnClickListener(kliknijCyfre);
btn8.setOnClickListener(kliknijCyfre);
btn9.setOnClickListener(kliknijCyfre);
// Operacje + - * /
View.OnClickListener kliknijOp = v -> {
liczbaA = Integer.parseInt(wyswietlacz.getText().toString());
operacja = ((Button) v).getText().toString();
nowaLiczba = true;
};
btnDodawanie.setOnClickListener(kliknijOp);
btnOdejmowanie.setOnClickListener(kliknijOp);
btnMnozenie.setOnClickListener(kliknijOp);
btnDzielenie.setOnClickListener(kliknijOp);
// = (równa się)
btnRowna.setOnClickListener(v -> {
int liczbaB = Integer.parseInt(wyswietlacz.getText().toString());
int wynik = 0;
switch (operacja) {
case "+": wynik = liczbaA + liczbaB; break;
case "-": wynik = liczbaA - liczbaB; break;
case "*": wynik = liczbaA * liczbaB; break;
case "/":
if (liczbaB == 0) {
wyswietlacz.setText("Błąd");
nowaLiczba = true;
return;
}
wynik = liczbaA / liczbaB;
break;
}
wyswietlacz.setText(String.valueOf(wynik));
nowaLiczba = true;
});
// C — wyczyść
btnC.setOnClickListener(v -> {
wyswietlacz.setText("0");
liczbaA = 0;
operacja = "";
nowaLiczba = true;
});
}
}
Kalkulator — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
7 / 2 w Javie (typ int)?int) wymaga obsługi wyjątku?"42" na liczbę całkowitą w Javie?Testy manualne
| Test | Kroki | Oczekiwany wynik |
|---|---|---|
| Dodawanie | 5 + 3 = | 8 |
| Odejmowanie | 10 - 4 = | 6 |
| Mnożenie | 6 * 7 = | 42 |
| Dzielenie | 15 / 3 = | 5 |
| Dzielenie całkowite | 7 / 2 = | 3 |
| Dzielenie przez 0 | 5 / 0 = | Błąd |
| Czyszczenie | C | 0 |
| Łańcuch | 2 + 3 + 4 = | 9 |
Kalkulator BMI — Rozgrzewka
„W tym projekcie poznamy dwa ważne komponenty Androida: SeekBar (suwak) i Spinner (lista rozwijana). Zamiast wpisywać liczby, użytkownik będzie je wybierał — to wygodniejsze i eliminuje błędy!"
Nowe komponenty
SeekBar — suwak do wybierania wartości liczbowej. Użytkownik przesuwa palcem/myszką.
Spinner — lista rozwijana (dropdown). Użytkownik wybiera jedną opcję z listy.
Mini Quiz
- Czym jest SeekBar i do czego służy?
- Czym jest Spinner i jak działa?
- Jak ustawić tekst w TextView?
- Jaki wzór opisuje BMI?
- SeekBar — suwak z wartością min-max. Nasłuchujemy zmian przez
OnSeekBarChangeListener. - Spinner — rozwijana lista opcji. Wypełniamy przez
ArrayAdapter, nasłuchujemy przezOnItemSelectedListener. textView.setText("tekst")- BMI = waga(kg) / wzrost(m)² — np. 70 / (1.75 × 1.75) = 22.86
Kalkulator BMI — Instrukcja
„Budujemy kalkulator BMI z SeekBar do wagi i Spinner do wzrostu. Dzięki temu uczeń pozna nowe komponenty UI!"
Treść zadania
Wykonaj aplikację "Kalkulator BMI" z komponentami SeekBar i Spinner:
- SeekBar do wyboru wagi (30–200 kg) — z etykietą pokazującą aktualną wartość
- Spinner do wyboru wzrostu (140–220 cm, co 1 cm)
- Przycisk "Oblicz BMI"
- Wyświetlenie wyniku BMI (liczba całkowita)
- Interpretacja wyniku (niedowaga/norma/nadwaga/otyłość) z kolorem
Nowe komponenty do nauki
| Komponent | Opis | Zastosowanie w projekcie |
|---|---|---|
| SeekBar | Suwak z wartością liczbową | Wybór wagi (30–200 kg) |
| Spinner | Lista rozwijana (dropdown) | Wybór wzrostu (140–220 cm) |
Skala BMI
| BMI | Kategoria |
|---|---|
| < 18.5 | Niedowaga |
| 18.5 – 24.9 | Norma |
| 25 – 29.9 | Nadwaga |
| ≥ 30 | Otyłość |
Kalkulator BMI — Budowa UI
File → New → New Project → Empty Views Activity
Nazwa: BMI, język: Java, kliknij Finish.
Otwórz activity_main.xml (zakładka Design).
Component Tree → kliknij prawym na ConstraintLayout → Convert view... → wpisz LinearLayout → OK
W panelu Attributes (prawa strona) ustaw:
orientation | vertical |
padding | 24dp |
orientation jest w sekcji Common Attributes w panelu Attributes po prawej. Padding znajdziesz w sekcji Layout → Padding → kliknij all i wpisz 24dp.
Palette (lewy panel) → Text → przeciągnij TextView na ekran.
Kliknij na dodany TextView. W panelu Attributes po prawej ustaw:
| Atrybut | Wartość | Gdzie znaleźć |
|---|---|---|
text | Kalkulator BMI | Common Attributes → text |
textSize | 24sp | Common Attributes → textSize |
textStyle | bold | Common Attributes → textStyle → zaznacz B |
layout_gravity | center | Layout → layout_gravity |
layout_marginBottom | 24dp | Layout → kliknij dolny margines |
Najpierw dodaj etykietę. Palette → Text → TextView, przeciągnij pod tytuł:
| Atrybut | Wartość |
|---|---|
id | txtWagaLabel |
text | Waga: 70 kg |
textSize | 18sp |
layout_marginTop | 16dp |
Teraz dodaj suwak. Palette → Widgets → SeekBar, przeciągnij pod etykietę:
| Atrybut | Wartość | Opis |
|---|---|---|
id | seekWaga | Identyfikator suwaka |
layout_width | match_parent | Pełna szerokość |
max | 170 | Zakres: 0–170 (+ 30 = 30–200 kg) |
progress | 40 | Domyślnie 70 kg (40 + 30) |
waga = progress + 30.
Dodaj etykietę. Palette → Text → TextView:
text | Wzrost: |
textSize | 18sp |
layout_marginTop | 24dp |
Teraz dodaj Spinner. Palette → Containers → Spinner, przeciągnij pod etykietę:
| Atrybut | Wartość | Opis |
|---|---|---|
id | spinnerWzrost | Identyfikator spinnera |
layout_width | match_parent | Pełna szerokość |
layout_marginTop | 8dp | Odstęp od etykiety |
Palette → Text → TextView — dodaj dwa TextViews pod Spinnerem:
TextView 1 — wynik liczbowy:
id | txtWynik |
textSize | 48sp |
textStyle | bold |
layout_gravity | center |
layout_marginTop | 32dp |
TextView 2 — kategoria:
id | txtKategoria |
textSize | 22sp |
layout_gravity | center |
layout_marginTop | 8dp |
Twoje Component Tree powinno wyglądać tak:
LinearLayout (vertical)
├── TextView "Kalkulator BMI"
├── TextView "Waga: 70 kg" (id: txtWagaLabel)
├── SeekBar (id: seekWaga)
├── TextView "Wzrost:"
├── Spinner (id: spinnerWzrost)
├── TextView (id: txtWynik)
└── TextView (id: txtKategoria)
Jeśli kolejność się nie zgadza — przeciągnij elementy w Component Tree we właściwe miejsce.
Otwórz MainActivity.java. Dodaj pola klasy nad metodą onCreate:
private SeekBar seekWaga;
private Spinner spinnerWzrost;
private TextView txtWagaLabel, txtWynik, txtKategoria;
private int waga = 70; // domyślna waga
private int wzrost = 170; // domyślny wzrost
Wewnątrz onCreate, pod setContentView, dodaj:
seekWaga = findViewById(R.id.seekWaga);
spinnerWzrost = findViewById(R.id.spinnerWzrost);
txtWagaLabel = findViewById(R.id.txtWagaLabel);
txtWynik = findViewById(R.id.txtWynik);
txtKategoria = findViewById(R.id.txtKategoria);
Dodaj w onCreate nasłuchiwanie zmian suwaka:
seekWaga.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
waga = progress + 30; // SeekBar: 0-170, waga: 30-200
txtWagaLabel.setText("Waga: " + waga + " kg");
obliczBMI(); // automatyczne przeliczenie!
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
onProgressChanged jest wywoływane automatycznie. Wartość progress (0–170) zamieniamy na wagę (30–200) dodając 30. Na końcu wywołujemy obliczBMI() — wynik aktualizuje się natychmiast!
Najpierw wypełnij Spinner wartościami. Dodaj w onCreate:
// Tworzymy listę wzrostów: 140, 141, 142, ..., 220
ArrayList<String> wzrosty = new ArrayList<>();
for (int i = 140; i <= 220; i++) {
wzrosty.add(i + " cm");
}
// Tworzymy adapter (łączy dane z widokiem)
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_spinner_item,
wzrosty
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerWzrost.setAdapter(adapter);
// Domyślnie: 170 cm (index 30, bo 170 - 140 = 30)
spinnerWzrost.setSelection(30);
Teraz dodaj nasłuchiwanie wyboru:
spinnerWzrost.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, android.view.View view, int position, long id) {
wzrost = position + 140; // index 0 = 140 cm
obliczBMI(); // automatyczne przeliczenie!
}
@Override
public void onNothingSelected(AdapterView<?> parent) { }
});
ArrayAdapter łączy listę Stringów ze Spinnerem. Gdy użytkownik wybierze opcję, onItemSelected daje nam position (index w liście). Index 0 = "140 cm", więc: wzrost = position + 140. Automatycznie przeliczamy BMI!
Dodaj metodę obliczBMI() pod metodą onCreate. Jest wywoływana automatycznie z SeekBar i Spinner:
private void obliczBMI() {
double wzrostM = wzrost / 100.0; // cm → m
double bmi = waga / (wzrostM * wzrostM);
int wynik = (int) bmi; // liczba całkowita
txtWynik.setText(String.valueOf(wynik));
// Kategoria + kolor
String kategoria;
int kolor;
if (bmi < 18.5) {
kategoria = "Niedowaga";
kolor = Color.BLUE;
} else if (bmi < 25) {
kategoria = "Waga prawidłowa";
kolor = Color.parseColor("#4CAF50");
} else if (bmi < 30) {
kategoria = "Nadwaga";
kolor = Color.parseColor("#FFA500");
} else {
kategoria = "Otyłość";
kolor = Color.RED;
}
txtKategoria.setText(kategoria);
txtKategoria.setTextColor(kolor);
}
Pod txtKategoria dodaj wizualną skalę BMI. W activity_main.xml (zakładka Code/Split):
Etykieta: Palette → Text → TextView:
text | Skala BMI: |
textSize | 14sp |
layout_marginTop | 24dp |
SeekBar wynikowy: Palette → Widgets → SeekBar:
id | seekBmiSkala |
layout_width | match_parent |
max | 40 |
progress | 0 |
enabled | false |
Pod tym SeekBar dodaj LinearLayout (horizontal) z 5 etykietami: 10, 18.5, 25, 30, 50. Każda etykieta: layout_weight="1", textSize="11sp".
To daje wizualną skalę z oznaczeniami progów BMI.
Dodaj pole klasy:
private SeekBar seekWaga, seekBmiSkala;
W onCreate dodaj:
seekBmiSkala = findViewById(R.id.seekBmiSkala);
W metodzie obliczBMI(), po txtWynik.setText(...) dodaj:
// Skala BMI: SeekBar 0-40 odpowiada BMI 10-50
int skalaPozycja = Math.max(0, Math.min(40, wynik - 10));
seekBmiSkala.setProgress(skalaPozycja);
Math.max/Math.min pilnują, żeby wartość nie wyszła poza zakres 0–40.
Na górze pliku MainActivity.java powinny być importy:
import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.*;
import java.util.ArrayList;
Uruchom aplikację: zielony trójkąt ▶ na pasku lub Shift + F10.
Przetestuj: przesuń suwak wagi, wybierz wzrost ze Spinnera, kliknij "Oblicz BMI". Suwak skali BMI powinien przesunąć się na odpowiednią pozycję!
Kalkulator BMI — Gotowy kod
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Kalkulator BMI"
android:textSize="24sp"
android:textStyle="bold"
android:layout_gravity="center"
android:layout_marginBottom="24dp" />
<TextView
android:id="@+id/txtWagaLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Waga: 70 kg"
android:textSize="18sp"
android:layout_marginTop="16dp" />
<SeekBar
android:id="@+id/seekWaga"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="170"
android:progress="40" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Wzrost:"
android:textSize="18sp"
android:layout_marginTop="24dp" />
<Spinner
android:id="@+id/spinnerWzrost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<TextView
android:id="@+id/txtWynik"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="48sp"
android:textStyle="bold"
android:layout_gravity="center"
android:layout_marginTop="32dp" />
<TextView
android:id="@+id/txtKategoria"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
android:layout_gravity="center"
android:layout_marginTop="8dp" />
<!-- Skala BMI -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skala BMI:"
android:textSize="14sp"
android:layout_marginTop="24dp" />
<SeekBar
android:id="@+id/seekBmiSkala"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="40"
android:progress="0"
android:enabled="false"
android:layout_marginTop="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="10"
android:textSize="11sp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="18.5"
android:textSize="11sp"
android:gravity="center" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="25"
android:textSize="11sp"
android:gravity="center" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="30"
android:textSize="11sp"
android:gravity="center" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="50"
android:textSize="11sp"
android:gravity="end" />
</LinearLayout>
</LinearLayout>
MainActivity.java
package com.example.bmi;
import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.*;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private SeekBar seekWaga, seekBmiSkala;
private Spinner spinnerWzrost;
private TextView txtWagaLabel, txtWynik, txtKategoria;
private int waga = 70;
private int wzrost = 170;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
seekWaga = findViewById(R.id.seekWaga);
seekBmiSkala = findViewById(R.id.seekBmiSkala);
spinnerWzrost = findViewById(R.id.spinnerWzrost);
txtWagaLabel = findViewById(R.id.txtWagaLabel);
txtWynik = findViewById(R.id.txtWynik);
txtKategoria = findViewById(R.id.txtKategoria);
// SeekBar — suwak wagi (automatycznie przelicza BMI)
seekWaga.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
waga = progress + 30;
txtWagaLabel.setText("Waga: " + waga + " kg");
obliczBMI();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
// Spinner — lista wzrostów
ArrayList<String> wzrosty = new ArrayList<>();
for (int i = 140; i <= 220; i++) {
wzrosty.add(i + " cm");
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_item, wzrosty
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerWzrost.setAdapter(adapter);
spinnerWzrost.setSelection(30); // 170 cm
spinnerWzrost.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, android.view.View view,
int position, long id) {
wzrost = position + 140;
obliczBMI();
}
@Override
public void onNothingSelected(AdapterView<?> parent) { }
});
// Oblicz BMI na starcie z domyślnymi wartościami
obliczBMI();
}
private void obliczBMI() {
double wzrostM = wzrost / 100.0;
double bmi = waga / (wzrostM * wzrostM);
int wynik = (int) bmi;
txtWynik.setText(String.valueOf(wynik));
// Skala BMI: SeekBar 0-40 odpowiada BMI 10-50
int skalaPozycja = Math.max(0, Math.min(40, wynik - 10));
seekBmiSkala.setProgress(skalaPozycja);
String kategoria;
int kolor;
if (bmi < 18.5) {
kategoria = "Niedowaga";
kolor = Color.BLUE;
} else if (bmi < 25) {
kategoria = "Waga prawidłowa";
kolor = Color.parseColor("#4CAF50");
} else if (bmi < 30) {
kategoria = "Nadwaga";
kolor = Color.parseColor("#FFA500");
} else {
kategoria = "Otyłość";
kolor = Color.RED;
}
txtKategoria.setText(kategoria);
txtKategoria.setTextColor(kolor);
}
}
Kalkulator BMI — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
max="170". Chcemy zakres wagi 30–200 kg. Jak obliczyć wagę z wartości progress?try-catch do parsowania liczb?Testy manualne
| Waga (SeekBar) | Wzrost (Spinner) | BMI | Kategoria | Skala |
|---|---|---|---|---|
| 70 kg | 175 cm | 22 | Waga prawidłowa | Suwak w strefie zielonej |
| 50 kg | 180 cm | 15 | Niedowaga | Suwak na lewo |
| 90 kg | 170 cm | 31 | Otyłość | Suwak na prawo |
| 85 kg | 175 cm | 27 | Nadwaga | Suwak w strefie pomarańczowej |
🔥 Lista zakupów — Rozgrzewka
„Lista z możliwością dodawania i usuwania. ListView + adapter."
Mini Quiz
- Jak utworzyć ArrayAdapter?
- Jak dodać element do listy?
- Co musi być wywołane po zmianie danych?
new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, lista)lista.add("element")adapter.notifyDataSetChanged()
📋 Lista zakupów — Instrukcja
Treść zadania
Wykonaj aplikację "Lista zakupów":
- Pole tekstowe na nowy produkt
- Przycisk "Dodaj"
- ListView z produktami
- Kliknięcie elementu = usunięcie (z potwierdzeniem)
- Przycisk "Wyczyść listę"
- Walidacja: nie można dodać pustego produktu
🏗️ Lista zakupów — Budowa UI
Nowy projekt: Empty Views Activity, nazwa Zakupy, język Java.
Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout
Ustaw: orientation="vertical", padding="16dp"
Dodaj LinearLayout (horizontal) — layout_width="match_parent", layout_height="wrap_content".
Wewnątrz:
| Widżet | id | Atrybuty |
|---|---|---|
| EditText | edtProdukt | layout_width="0dp", layout_weight="1", hint="Nazwa produktu" |
| Button | btnDodaj | layout_width="wrap_content", text="Dodaj" |
ListView: id="listProdukty", layout_width="match_parent", layout_height="0dp", layout_weight="1", layout_marginTop="16dp"
Button: id="btnWyczysc", layout_width="match_parent", text="Wyczyść listę"
private EditText edtProdukt;
private Button btnDodaj, btnWyczysc;
private ListView listProdukty;
private ArrayList<String> produkty;
private ArrayAdapter<String> adapter;
// w onCreate:
edtProdukt = findViewById(R.id.edtProdukt);
btnDodaj = findViewById(R.id.btnDodaj);
btnWyczysc = findViewById(R.id.btnWyczysc);
listProdukty = findViewById(R.id.listProdukty);
// w onCreate (po findViewById):
produkty = new ArrayList<>();
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, produkty);
listProdukty.setAdapter(adapter);
btnDodaj.setOnClickListener(v -> dodajProdukt());
btnWyczysc.setOnClickListener(v -> wyczyscListe());
private void dodajProdukt() {
String produkt = edtProdukt.getText().toString().trim();
if (produkt.isEmpty()) {
Toast.makeText(this, "Wpisz nazwę produktu!", Toast.LENGTH_SHORT).show();
return;
}
produkty.add(produkt);
adapter.notifyDataSetChanged();
edtProdukt.setText("");
}
private void wyczyscListe() {
produkty.clear();
adapter.notifyDataSetChanged();
}
Dodaj w onCreate listener na kliknięcie elementu listy:
// Kliknięcie = usuń z potwierdzeniem
listProdukty.setOnItemClickListener((parent, view, position, id) -> {
String produkt = produkty.get(position);
potwierdzUsuniecie(produkt, position);
});
// Nowa metoda:
private void potwierdzUsuniecie(String produkt, int position) {
new AlertDialog.Builder(this)
.setTitle("Usunąć produkt?")
.setMessage("Czy na pewno usunąć: " + produkt + "?")
.setPositiveButton("Tak", (dialog, which) -> {
produkty.remove(position);
adapter.notifyDataSetChanged();
})
.setNegativeButton("Nie", null)
.show();
}
💻 Lista zakupów — Kod
package com.example.zakupy;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.*;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private EditText edtProdukt;
private Button btnDodaj, btnWyczysc;
private ListView listProdukty;
private ArrayList<String> produkty;
private ArrayAdapter<String> adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edtProdukt = findViewById(R.id.edtProdukt);
btnDodaj = findViewById(R.id.btnDodaj);
btnWyczysc = findViewById(R.id.btnWyczysc);
listProdukty = findViewById(R.id.listProdukty);
produkty = new ArrayList<>();
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, produkty);
listProdukty.setAdapter(adapter);
btnDodaj.setOnClickListener(v -> dodajProdukt());
btnWyczysc.setOnClickListener(v -> wyczyscListe());
// Kliknięcie = usuń z potwierdzeniem
listProdukty.setOnItemClickListener((parent, view, position, id) -> {
String produkt = produkty.get(position);
potwierdzUsuniecie(produkt, position);
});
}
private void dodajProdukt() {
String produkt = edtProdukt.getText().toString().trim();
if (produkt.isEmpty()) {
Toast.makeText(this, "Wpisz nazwę produktu!", Toast.LENGTH_SHORT).show();
return;
}
produkty.add(produkt);
adapter.notifyDataSetChanged();
edtProdukt.setText("");
}
private void potwierdzUsuniecie(String produkt, int position) {
new AlertDialog.Builder(this)
.setTitle("Usunąć produkt?")
.setMessage("Czy na pewno usunąć: " + produkt + "?")
.setPositiveButton("Tak", (dialog, which) -> {
produkty.remove(position);
adapter.notifyDataSetChanged();
})
.setNegativeButton("Nie", null)
.show();
}
private void wyczyscListe() {
produkty.clear();
adapter.notifyDataSetChanged();
}
}
Lista zakupów — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
position?Testy manualne
| Test | Oczekiwany wynik |
|---|---|
| Dodaj "Mleko" | Mleko pojawia się na liście |
| Dodaj pusty produkt | Toast z błędem |
| Kliknij produkt na liście | Dialog z pytaniem o usunięcie |
| Potwierdź usunięcie | Produkt znika z listy |
| Kliknij "Wyczyść" | Lista pusta |
🔥 Notatnik — Rozgrzewka
„Notatnik z możliwością zapisu do pliku. Dodatkowa funkcjonalność!"
Mini Quiz
- Jak uzyskać dostęp do wieloliniowego pola tekstowego?
- Jak zapisać tekst do pliku w Android?
- EditText z
android:inputType="textMultiLine"iandroid:lines="10" FileOutputStreamlubSharedPreferences(prostsze dla tekstu)
📋 Notatnik — Instrukcja
Treść zadania
Wykonaj aplikację "Notatnik":
- Pole tekstowe wieloliniowe na notatkę
- Przycisk "Zapisz" — zapisuje notatkę
- Przycisk "Wczytaj" — wczytuje zapisaną notatkę
- Przycisk "Wyczyść" — czyści pole
- Notatka zapisywana w SharedPreferences
- Komunikat Toast przy zapisie/wczytaniu
🏗️ Notatnik — Budowa UI
Nowy projekt: Empty Views Activity, nazwa Notatnik, język Java.
Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout
Ustaw: orientation="vertical", padding="16dp"
Palette → Text → TextView:
text | Notatnik |
textSize | 24sp |
textStyle | bold |
layout_marginBottom | 16dp |
Palette → Text → Multiline Text:
id | edtNotatka |
layout_width | match_parent |
layout_height | 0dp |
layout_weight | 1 |
gravity | top |
hint | Wpisz notatke... |
inputType | textMultiLine |
background | @android:drawable/edit_text |
Dodaj LinearLayout (horizontal) z layout_marginTop="16dp".
Wewnątrz 3 Button (każdy: layout_width="0dp", layout_weight="1"):
| id | text |
|---|---|
btnZapisz | Zapisz |
btnWczytaj | Wczytaj |
btnWyczysc | Wyczysc |
private EditText edtNotatka;
private Button btnZapisz, btnWczytaj, btnWyczysc;
private static final String PREFS_NAME = "NotatnikPrefs";
private static final String KEY_NOTATKA = "notatka";
// w onCreate:
edtNotatka = findViewById(R.id.edtNotatka);
btnZapisz = findViewById(R.id.btnZapisz);
btnWczytaj = findViewById(R.id.btnWczytaj);
btnWyczysc = findViewById(R.id.btnWyczysc);
btnZapisz.setOnClickListener(v -> zapiszNotatke());
btnWczytaj.setOnClickListener(v -> wczytajNotatke());
btnWyczysc.setOnClickListener(v -> wyczyscNotatke());
// Automatyczne wczytanie przy starcie
wczytajNotatke();
private void zapiszNotatke() {
String notatka = edtNotatka.getText().toString();
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(KEY_NOTATKA, notatka);
editor.apply(); // lub commit() (synchroniczny)
Toast.makeText(this, "Zapisano!", Toast.LENGTH_SHORT).show();
}
private void wczytajNotatke() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
String notatka = prefs.getString(KEY_NOTATKA, ""); // "" = wartosc domyslna
edtNotatka.setText(notatka);
if (!notatka.isEmpty()) {
Toast.makeText(this, "Wczytano notatke", Toast.LENGTH_SHORT).show();
}
}
private void wyczyscNotatke() {
edtNotatka.setText("");
}
💻 Notatnik — Kod
package com.example.notatnik;
import androidx.appcompat.app.AppCompatActivity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.*;
public class MainActivity extends AppCompatActivity {
private EditText edtNotatka;
private Button btnZapisz, btnWczytaj, btnWyczysc;
private static final String PREFS_NAME = "NotatnikPrefs";
private static final String KEY_NOTATKA = "notatka";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edtNotatka = findViewById(R.id.edtNotatka);
btnZapisz = findViewById(R.id.btnZapisz);
btnWczytaj = findViewById(R.id.btnWczytaj);
btnWyczysc = findViewById(R.id.btnWyczysc);
btnZapisz.setOnClickListener(v -> zapiszNotatke());
btnWczytaj.setOnClickListener(v -> wczytajNotatke());
btnWyczysc.setOnClickListener(v -> wyczyscNotatke());
// Automatyczne wczytanie przy starcie
wczytajNotatke();
}
private void zapiszNotatke() {
String notatka = edtNotatka.getText().toString();
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(KEY_NOTATKA, notatka);
editor.apply(); // lub commit() (synchroniczny)
Toast.makeText(this, "Zapisano!", Toast.LENGTH_SHORT).show();
}
private void wczytajNotatke() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
String notatka = prefs.getString(KEY_NOTATKA, ""); // "" = wartość domyślna
edtNotatka.setText(notatka);
if (!notatka.isEmpty()) {
Toast.makeText(this, "Wczytano notatkę", Toast.LENGTH_SHORT).show();
}
}
private void wyczyscNotatke() {
edtNotatka.setText("");
}
}
Prosty sposób na przechowywanie par klucz-wartość. Idealny dla ustawień i małych danych tekstowych.
Notatnik — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
MODE_PRIVATE w SharedPreferences?Testy manualne
| Test | Oczekiwany wynik |
|---|---|
| Wpisz tekst + Zapisz | Toast "Zapisano!" |
| Wyczyść + Wczytaj | Poprzedni tekst wraca |
| Zamknij i otwórz aplikację | Notatka nadal jest |
| Wyczyść | Pole puste |
🔥 Quiz — Rozgrzewka
„Quiz to świetny projekt łączący logikę programowania z UI."
Mini Quiz
- Jak przechowywać pytania quizu?
- Jak grupować RadioButtony?
- Jak sprawdzić który RadioButton jest zaznaczony?
- Tablica obiektów lub ArrayList klasy Pytanie
<RadioGroup>w XMLradioGroup.getCheckedRadioButtonId()zwraca ID zaznaczonego
📋 Quiz — Instrukcja
Treść zadania
Wykonaj aplikację "Quiz":
- Baza 12 pytań jednokrotnego wyboru
- Losowanie 3 pytań z bazy przy każdym uruchomieniu
- 4 odpowiedzi do każdego pytania (RadioButton)
- Przycisk "Następne" — przejście do kolejnego pytania
- Na końcu — wyświetlenie wyniku (X/3)
- Możliwość rozpoczęcia od nowa (nowe losowanie!)
- Numeracja pytań (1/3, 2/3...)
🏗️ Quiz — Budowa UI
Nowy projekt: Empty Views Activity, nazwa Quiz, język Java.
Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout
Ustaw: orientation="vertical", padding="16dp"
TextView (numer):
id | txtNumerPytania |
text | Pytanie 1/3 |
textSize | 16sp |
TextView (tresc pytania):
id | txtPytanie |
text | Tresc pytania |
textSize | 20sp |
textStyle | bold |
layout_marginTop | 16dp |
layout_marginBottom | 24dp |
Palette → Buttons → RadioGroup (id="radioGroup", layout_width="match_parent")
Wewnątrz 4 RadioButton (każdy: layout_width="match_parent"):
| id | text |
|---|---|
rbOdp1 | Odpowiedz 1 |
rbOdp2 | Odpowiedz 2 |
rbOdp3 | Odpowiedz 3 |
rbOdp4 | Odpowiedz 4 |
Button: id="btnNastepne", layout_width="match_parent", text="Nastepne", layout_marginTop="32dp"
TextView (wynik): id="txtWynik", textSize="24sp", textStyle="bold", gravity="center", layout_marginTop="32dp", visibility="gone"
Prawym na pakiet → New → Java Class → Pytanie:
public class Pytanie {
String tresc;
String[] odpowiedzi;
int poprawna; // indeks 0-3
public Pytanie(String tresc, String[] odpowiedzi, int poprawna) {
this.tresc = tresc;
this.odpowiedzi = odpowiedzi;
this.poprawna = poprawna;
}
}
private TextView txtNumerPytania, txtPytanie, txtWynik;
private RadioGroup radioGroup;
private RadioButton rbOdp1, rbOdp2, rbOdp3, rbOdp4;
private Button btnNastepne;
private Pytanie[] pytania; // wylosowane 3 pytania
private int aktualnePytanie = 0;
private int wynik = 0;
// w onCreate:
txtNumerPytania = findViewById(R.id.txtNumerPytania);
txtPytanie = findViewById(R.id.txtPytanie);
txtWynik = findViewById(R.id.txtWynik);
radioGroup = findViewById(R.id.radioGroup);
rbOdp1 = findViewById(R.id.rbOdp1);
rbOdp2 = findViewById(R.id.rbOdp2);
rbOdp3 = findViewById(R.id.rbOdp3);
rbOdp4 = findViewById(R.id.rbOdp4);
btnNastepne = findViewById(R.id.btnNastepne);
losujPytania();
wyswietlPytanie();
btnNastepne.setOnClickListener(v -> sprawdzOdpowiedz());
import java.util.Random;
Metoda losujPytania() — baza 12 pytań, losujemy 3:
private void losujPytania() {
Pytanie[] baza = {
new Pytanie("Stolica Polski to:",
new String[]{"Kraków", "Warszawa", "Gdańsk", "Poznań"}, 1),
new Pytanie("2 + 2 = ?",
new String[]{"3", "4", "5", "22"}, 1),
new Pytanie("Ile dni ma tydzień?",
new String[]{"5", "6", "7", "8"}, 2),
new Pytanie("Jaki kolor ma trawa?",
new String[]{"Niebieski", "Czerwony", "Zielony", "Żółty"}, 2),
new Pytanie("Który język to Android?",
new String[]{"Python", "Java", "C++", "Ruby"}, 1),
new Pytanie("Ile miesięcy ma rok?",
new String[]{"10", "11", "12", "13"}, 2),
new Pytanie("Największy ocean to:",
new String[]{"Atlantycki", "Indyjski", "Spokojny", "Arktyczny"}, 2),
new Pytanie("Ile nóg ma pająk?",
new String[]{"6", "8", "10", "4"}, 1),
new Pytanie("Który pierwiastek ma symbol O?",
new String[]{"Złoto", "Tlen", "Ołów", "Osmium"}, 1),
new Pytanie("Ile centymetrów ma metr?",
new String[]{"10", "100", "1000", "50"}, 1),
new Pytanie("Kto namalował Mona Lisę?",
new String[]{"Picasso", "Van Gogh", "Da Vinci", "Rembrandt"}, 2),
new Pytanie("Ile planet ma Układ Słoneczny?",
new String[]{"7", "8", "9", "10"}, 1)
};
// Losujemy 3 różne indeksy
Random random = new Random();
pytania = new Pytanie[3];
boolean[] uzyte = new boolean[baza.length];
for (int i = 0; i < 3; i++) {
int los;
do {
los = random.nextInt(baza.length);
} while (uzyte[los]);
uzyte[los] = true;
pytania[i] = baza[los];
}
}
uzyte[] pamięta które pytania już wylosowaliśmy. Pętla do-while losuje tak długo, aż trafi na nieużyty indeks. Proste i skuteczne!
private void wyswietlPytanie() {
Pytanie p = pytania[aktualnePytanie];
txtNumerPytania.setText("Pytanie " + (aktualnePytanie + 1) + "/" + pytania.length);
txtPytanie.setText(p.tresc);
rbOdp1.setText(p.odpowiedzi[0]);
rbOdp2.setText(p.odpowiedzi[1]);
rbOdp3.setText(p.odpowiedzi[2]);
rbOdp4.setText(p.odpowiedzi[3]);
radioGroup.clearCheck();
}
private void sprawdzOdpowiedz() {
int zaznaczonyId = radioGroup.getCheckedRadioButtonId();
if (zaznaczonyId == -1) {
Toast.makeText(this, "Wybierz odpowiedz!", Toast.LENGTH_SHORT).show();
return;
}
int wybranaOdp = -1;
if (zaznaczonyId == R.id.rbOdp1) wybranaOdp = 0;
else if (zaznaczonyId == R.id.rbOdp2) wybranaOdp = 1;
else if (zaznaczonyId == R.id.rbOdp3) wybranaOdp = 2;
else if (zaznaczonyId == R.id.rbOdp4) wybranaOdp = 3;
if (wybranaOdp == pytania[aktualnePytanie].poprawna) {
wynik++;
}
aktualnePytanie++;
if (aktualnePytanie < pytania.length) {
wyswietlPytanie();
} else {
pokazWynik();
}
}
private void pokazWynik() {
txtPytanie.setVisibility(View.GONE);
radioGroup.setVisibility(View.GONE);
txtNumerPytania.setVisibility(View.GONE);
txtWynik.setVisibility(View.VISIBLE);
txtWynik.setText("Twoj wynik: " + wynik + "/" + pytania.length);
btnNastepne.setText("Zagraj ponownie");
btnNastepne.setOnClickListener(v -> restartQuiz());
}
private void restartQuiz() {
aktualnePytanie = 0;
wynik = 0;
txtPytanie.setVisibility(View.VISIBLE);
radioGroup.setVisibility(View.VISIBLE);
txtNumerPytania.setVisibility(View.VISIBLE);
txtWynik.setVisibility(View.GONE);
btnNastepne.setText("Nastepne");
btnNastepne.setOnClickListener(v -> sprawdzOdpowiedz());
losujPytania(); // nowe losowanie!
wyswietlPytanie();
}
losujPytania() — gracz dostaje nowy zestaw 3 pytań!
💻 Quiz — Kod
Klasa Pytanie.java
package com.example.quiz;
public class Pytanie {
String tresc;
String[] odpowiedzi;
int poprawna; // indeks 0-3
public Pytanie(String tresc, String[] odpowiedzi, int poprawna) {
this.tresc = tresc;
this.odpowiedzi = odpowiedzi;
this.poprawna = poprawna;
}
}
MainActivity.java
package com.example.quiz;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.util.Random;
public class MainActivity extends AppCompatActivity {
private TextView txtNumerPytania, txtPytanie, txtWynik;
private RadioGroup radioGroup;
private RadioButton rbOdp1, rbOdp2, rbOdp3, rbOdp4;
private Button btnNastepne;
private Pytanie[] pytania; // wylosowane 3 pytania
private int aktualnePytanie = 0;
private int wynik = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txtNumerPytania = findViewById(R.id.txtNumerPytania);
txtPytanie = findViewById(R.id.txtPytanie);
txtWynik = findViewById(R.id.txtWynik);
radioGroup = findViewById(R.id.radioGroup);
rbOdp1 = findViewById(R.id.rbOdp1);
rbOdp2 = findViewById(R.id.rbOdp2);
rbOdp3 = findViewById(R.id.rbOdp3);
rbOdp4 = findViewById(R.id.rbOdp4);
btnNastepne = findViewById(R.id.btnNastepne);
losujPytania();
wyswietlPytanie();
btnNastepne.setOnClickListener(v -> sprawdzOdpowiedz());
}
private void losujPytania() {
Pytanie[] baza = {
new Pytanie("Stolica Polski to:",
new String[]{"Kraków", "Warszawa", "Gdańsk", "Poznań"}, 1),
new Pytanie("2 + 2 = ?",
new String[]{"3", "4", "5", "22"}, 1),
new Pytanie("Ile dni ma tydzień?",
new String[]{"5", "6", "7", "8"}, 2),
new Pytanie("Jaki kolor ma trawa?",
new String[]{"Niebieski", "Czerwony", "Zielony", "Żółty"}, 2),
new Pytanie("Który język to Android?",
new String[]{"Python", "Java", "C++", "Ruby"}, 1),
new Pytanie("Ile miesięcy ma rok?",
new String[]{"10", "11", "12", "13"}, 2),
new Pytanie("Największy ocean to:",
new String[]{"Atlantycki", "Indyjski", "Spokojny", "Arktyczny"}, 2),
new Pytanie("Ile nóg ma pająk?",
new String[]{"6", "8", "10", "4"}, 1),
new Pytanie("Który pierwiastek ma symbol O?",
new String[]{"Złoto", "Tlen", "Ołów", "Osmium"}, 1),
new Pytanie("Ile centymetrów ma metr?",
new String[]{"10", "100", "1000", "50"}, 1),
new Pytanie("Kto namalował Mona Lisę?",
new String[]{"Picasso", "Van Gogh", "Da Vinci", "Rembrandt"}, 2),
new Pytanie("Ile planet ma Układ Słoneczny?",
new String[]{"7", "8", "9", "10"}, 1)
};
Random random = new Random();
pytania = new Pytanie[3];
boolean[] uzyte = new boolean[baza.length];
for (int i = 0; i < 3; i++) {
int los;
do {
los = random.nextInt(baza.length);
} while (uzyte[los]);
uzyte[los] = true;
pytania[i] = baza[los];
}
}
private void wyswietlPytanie() {
Pytanie p = pytania[aktualnePytanie];
txtNumerPytania.setText("Pytanie " + (aktualnePytanie + 1) + "/" + pytania.length);
txtPytanie.setText(p.tresc);
rbOdp1.setText(p.odpowiedzi[0]);
rbOdp2.setText(p.odpowiedzi[1]);
rbOdp3.setText(p.odpowiedzi[2]);
rbOdp4.setText(p.odpowiedzi[3]);
radioGroup.clearCheck();
}
private void sprawdzOdpowiedz() {
int zaznaczonyId = radioGroup.getCheckedRadioButtonId();
if (zaznaczonyId == -1) {
Toast.makeText(this, "Wybierz odpowiedź!", Toast.LENGTH_SHORT).show();
return;
}
int wybranaOdp = -1;
if (zaznaczonyId == R.id.rbOdp1) wybranaOdp = 0;
else if (zaznaczonyId == R.id.rbOdp2) wybranaOdp = 1;
else if (zaznaczonyId == R.id.rbOdp3) wybranaOdp = 2;
else if (zaznaczonyId == R.id.rbOdp4) wybranaOdp = 3;
if (wybranaOdp == pytania[aktualnePytanie].poprawna) {
wynik++;
}
aktualnePytanie++;
if (aktualnePytanie < pytania.length) {
wyswietlPytanie();
} else {
pokazWynik();
}
}
private void pokazWynik() {
txtPytanie.setVisibility(View.GONE);
radioGroup.setVisibility(View.GONE);
txtNumerPytania.setVisibility(View.GONE);
txtWynik.setVisibility(View.VISIBLE);
txtWynik.setText("Twój wynik: " + wynik + "/" + pytania.length);
btnNastepne.setText("Zagraj ponownie");
btnNastepne.setOnClickListener(v -> restartQuiz());
}
private void restartQuiz() {
aktualnePytanie = 0;
wynik = 0;
txtPytanie.setVisibility(View.VISIBLE);
radioGroup.setVisibility(View.VISIBLE);
txtNumerPytania.setVisibility(View.VISIBLE);
txtWynik.setVisibility(View.GONE);
btnNastepne.setText("Następne");
btnNastepne.setOnClickListener(v -> sprawdzOdpowiedz());
losujPytania();
wyswietlPytanie();
}
}
Quiz — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
getCheckedRadioButtonId() gdy nic nie jest zaznaczone?Testy manualne
| Test | Oczekiwany wynik |
|---|---|
| Rozpocznij quiz | Pytanie 1/3 wyświetlone |
| Kliknij Następne bez wyboru | Toast "Wybierz odpowiedź!" |
| Odpowiedz poprawnie | Przejście do następnego pytania |
| Ukończ quiz (5 poprawnych) | Wynik: 5/5 |
| Zagraj ponownie | Quiz od początku |
Rozgrzewka — Ekran logowania
Szybkie pytania na początek lekcji — kliknij pytanie żeby zobaczyć odpowiedź.
1. Co to jest Activity?
Activity to jeden ekran w aplikacji Android. Każdy ekran (logowanie, lista, szczegóły) to osobna Activity z własnym layoutem.
2. Czym jest Intent?
Intent (pol. "zamiar") to obiekt który mówi Androidowi "chcę otworzyć inny ekran". Jest jak wiadomość: "z ekranu A przejdź do ekranu B".
3. Jak przekazać dane między dwoma Activity?
Przez putExtra("klucz", wartosc) w Intencie. W drugiej Activity odczytujemy: getIntent().getStringExtra("klucz"). Klucze MUSZĄ być identyczne!
4. Jak ukryć tekst hasła (kropki zamiast liter)?
W EditText ustaw android:inputType="textPassword". Tekst będzie automatycznie zamieniany na kropki.
5. Co robi finish()?
Zamyka bieżącą Activity i wraca do poprzedniej. Jak zamknięcie karty w przeglądarce.
Ekran logowania — Instrukcja
Treść zadania
Wykonaj aplikację z dwoma ekranami:
- Ekran 1 (MainActivity): formularz logowania — pole nazwy użytkownika, pole hasła, przycisk "Zaloguj"
- Pole hasła musi maskować tekst (kropki zamiast liter)
- Walidacja: puste pola, hasło min. 4 znaki
- Poprawne dane:
admin/1234— przekierowanie na ekran 2 - Ekran 2 (WelcomeActivity): napis "Witaj, [nazwa]!" + przycisk "Wyloguj"
- Wylogowanie wraca do ekranu logowania
- Komunikaty błędów przez Toast
Czego się nauczysz
| Pojęcie | Co robi |
|---|---|
Intent | Przejście między dwoma Activity |
| Druga Activity | Tworzenie kolejnego ekranu (New → Activity) |
putExtra / getStringExtra | Przekazywanie danych między ekranami |
inputType="textPassword" | Maskowanie hasła kropkami |
Toast | Krótki komunikat dla użytkownika |
finish() | Zamknięcie Activity (powrót) |
Ekran logowania — Budowa krok po kroku
New Project → Empty Views Activity, nazwa Logowanie, język Java.
W activity_main.xml Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout.
Ustaw atrybuty root layoutu:
orientation | vertical |
padding | 32dp |
gravity | center |
TextView (tytuł):
id | tytul |
text | Ekran logowania |
textSize | 28sp |
textStyle | bold |
layout_marginBottom | 40dp |
EditText (login): Palette → Text → Plain Text
id | poleUzytkownik |
hint | Nazwa uzytkownika |
inputType | text |
layout_marginBottom | 16dp |
EditText (hasło): Palette → Text → Password (NIE Plain Text!)
id | poleHaslo |
hint | Haslo |
inputType | textPassword |
layout_marginBottom | 24dp |
inputType="textPassword" automatycznie zamienia litery na kropki (●●●●). Dzięki temu nikt nie podejrzy hasła zerkając na ekran.
id | btnZaloguj |
text | ZALOGUJ |
layout_width | match_parent |
backgroundTint | #2196F3 |
textColor | #FFFFFF |
Component Tree powinno wyglądać tak:
LinearLayout (vertical, gravity=center)
├── TextView "tytul"
├── EditText "poleUzytkownik"
├── EditText "poleHaslo"
└── Button "btnZaloguj"
To kluczowy moment lekcji — pierwszy raz tworzysz drugi ekran!
- W panelu Project kliknij prawym na pakiet
com.example.logowanie - New → Activity → Empty Views Activity
- Activity Name:
WelcomeActivity - Layout Name:
activity_welcome(wypełni się automatycznie) - Kliknij Finish
WelcomeActivity.java— logika drugiego ekranuactivity_welcome.xml— wygląd drugiego ekranu- Wpis w
AndroidManifest.xmlrejestrujący nową Activity
AndroidManifest.xml — tam są wszystkie Activity w aplikacji.
Otwórz activity_welcome.xml i zmień root na LinearLayout (vertical, padding 32dp, gravity center) — tak samo jak w activity_main.
TextView (powitanie):
id | tekstPowitalny |
text | Witaj! |
textSize | 32sp |
textStyle | bold |
layout_marginBottom | 40dp |
Button (wyloguj):
id | btnWyloguj |
text | WYLOGUJ |
layout_width | match_parent |
backgroundTint | #F44336 |
textColor | #FFFFFF |
Otwórz MainActivity.java. Dodaj pola klasy nad metodą onCreate:
private EditText poleUzytkownik, poleHaslo;
private Button btnZaloguj;
// Poprawne dane logowania (na sztywno)
private String poprawnyUzytkownik = "admin";
private String poprawneHaslo = "1234";
Wewnątrz onCreate, pod setContentView:
poleUzytkownik = findViewById(R.id.poleUzytkownik);
poleHaslo = findViewById(R.id.poleHaslo);
btnZaloguj = findViewById(R.id.btnZaloguj);
btnZaloguj.setOnClickListener(v -> zaloguj());
Dodaj metodę zaloguj() pod onCreate:
private void zaloguj() {
String uzytkownik = poleUzytkownik.getText().toString().trim();
String haslo = poleHaslo.getText().toString().trim();
// Walidacja
if (uzytkownik.isEmpty()) {
Toast.makeText(this, "Wpisz nazwe uzytkownika!", Toast.LENGTH_SHORT).show();
return;
}
if (haslo.isEmpty()) {
Toast.makeText(this, "Wpisz haslo!", Toast.LENGTH_SHORT).show();
return;
}
if (haslo.length() < 4) {
Toast.makeText(this, "Haslo musi miec minimum 4 znaki!", Toast.LENGTH_SHORT).show();
return;
}
// Sprawdzenie poprawności
if (uzytkownik.equals(poprawnyUzytkownik) && haslo.equals(poprawneHaslo)) {
Intent zamiar = new Intent(MainActivity.this, WelcomeActivity.class);
zamiar.putExtra("nazwaUzytkownika", uzytkownik);
startActivity(zamiar);
} else {
Toast.makeText(this, "Bledna nazwa uzytkownika lub haslo!", Toast.LENGTH_SHORT).show();
}
}
.trim()— usuwa spacje na początku i końcu (np. " admin " → "admin")return— kończy metodę, dalszy kod się nie wykonaIntent— "zamiar" przejścia do innego ekranuputExtra("klucz", wartosc)— dokleja dane do Intentu (jak list w kopercie)startActivity(zamiar)— wykonuje przejście
Otwórz WelcomeActivity.java. Dodaj pola klasy:
private TextView tekstPowitalny;
private Button btnWyloguj;
W onCreate, pod setContentView:
tekstPowitalny = findViewById(R.id.tekstPowitalny);
btnWyloguj = findViewById(R.id.btnWyloguj);
// Odbierz dane z Intentu
String uzytkownik = getIntent().getStringExtra("nazwaUzytkownika");
tekstPowitalny.setText("Witaj, " + uzytkownik + "!");
// Wyloguj = zamknij ten ekran
btnWyloguj.setOnClickListener(v -> finish());
Intent= poszedłem do sklepu (otworzyłem nowy ekran)putExtra= wziąłem ze sobą listę zakupów (dane)getStringExtra= w sklepie odczytałem listęfinish()= wyszedłem ze sklepu, wracam do domu (poprzedni ekran)
putExtra("nazwaUzytkownika", ...) i getStringExtra("nazwaUzytkownika") klucz musi być co do litery taki sam — inaczej dostaniesz null.
Ekran logowania — Gotowy kod
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="32dp"
android:gravity="center">
<TextView
android:id="@+id/tytul"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Ekran logowania"
android:textSize="28sp"
android:textStyle="bold"
android:textAlignment="center"
android:layout_marginBottom="40dp" />
<EditText
android:id="@+id/poleUzytkownik"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nazwa uzytkownika"
android:inputType="text"
android:layout_marginBottom="16dp" />
<EditText
android:id="@+id/poleHaslo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Haslo"
android:inputType="textPassword"
android:layout_marginBottom="24dp" />
<Button
android:id="@+id/btnZaloguj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ZALOGUJ"
android:textSize="16sp"
android:backgroundTint="#2196F3"
android:textColor="#FFFFFF" />
</LinearLayout>
activity_welcome.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="32dp"
android:gravity="center">
<TextView
android:id="@+id/tekstPowitalny"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Witaj!"
android:textSize="32sp"
android:textStyle="bold"
android:textAlignment="center"
android:layout_marginBottom="40dp" />
<Button
android:id="@+id/btnWyloguj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="WYLOGUJ"
android:textSize="16sp"
android:backgroundTint="#F44336"
android:textColor="#FFFFFF" />
</LinearLayout>
MainActivity.java
package com.example.logowanie;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private EditText poleUzytkownik, poleHaslo;
private Button btnZaloguj;
private String poprawnyUzytkownik = "admin";
private String poprawneHaslo = "1234";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
poleUzytkownik = findViewById(R.id.poleUzytkownik);
poleHaslo = findViewById(R.id.poleHaslo);
btnZaloguj = findViewById(R.id.btnZaloguj);
btnZaloguj.setOnClickListener(v -> zaloguj());
}
private void zaloguj() {
String uzytkownik = poleUzytkownik.getText().toString().trim();
String haslo = poleHaslo.getText().toString().trim();
if (uzytkownik.isEmpty()) {
Toast.makeText(this, "Wpisz nazwe uzytkownika!", Toast.LENGTH_SHORT).show();
return;
}
if (haslo.isEmpty()) {
Toast.makeText(this, "Wpisz haslo!", Toast.LENGTH_SHORT).show();
return;
}
if (haslo.length() < 4) {
Toast.makeText(this, "Haslo musi miec minimum 4 znaki!", Toast.LENGTH_SHORT).show();
return;
}
if (uzytkownik.equals(poprawnyUzytkownik) && haslo.equals(poprawneHaslo)) {
Intent zamiar = new Intent(MainActivity.this, WelcomeActivity.class);
zamiar.putExtra("nazwaUzytkownika", uzytkownik);
startActivity(zamiar);
} else {
Toast.makeText(this, "Bledna nazwa uzytkownika lub haslo!", Toast.LENGTH_SHORT).show();
}
}
}
WelcomeActivity.java
package com.example.logowanie;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
public class WelcomeActivity extends AppCompatActivity {
private TextView tekstPowitalny;
private Button btnWyloguj;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
tekstPowitalny = findViewById(R.id.tekstPowitalny);
btnWyloguj = findViewById(R.id.btnWyloguj);
String uzytkownik = getIntent().getStringExtra("nazwaUzytkownika");
tekstPowitalny.setText("Witaj, " + uzytkownik + "!");
btnWyloguj.setOnClickListener(v -> finish());
}
}
Scenariusze testowe
| Test | Oczekiwany wynik |
|---|---|
| Puste pola → kliknij ZALOGUJ | Toast "Wpisz nazwe uzytkownika!" |
| Tylko hasło → kliknij ZALOGUJ | Toast "Wpisz nazwe uzytkownika!" |
| Tylko login → kliknij ZALOGUJ | Toast "Wpisz haslo!" |
| admin / 12 → ZALOGUJ | Toast "Haslo musi miec minimum 4 znaki!" |
| jan / 5678 → ZALOGUJ | Toast "Bledna nazwa uzytkownika lub haslo!" |
| admin / 1234 → ZALOGUJ | Otwiera ekran "Witaj, admin!" |
| Kliknij WYLOGUJ | Powrót do ekranu logowania |
| Wpisz hasło | Widać kropki, nie litery |
Ekran logowania — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
Intent w Androidzie?finish()?getStringExtra jest INNY niż w putExtra?📜 Android — Ściąga
Podstawowe kontrolki
| Kontrolka | XML | Java |
|---|---|---|
| TextView | android:text="..." | txt.setText(...) |
| EditText | android:hint="..." | edt.getText().toString() |
| Button | android:onClick="metoda" | btn.setOnClickListener(...) |
| ImageView | android:src="@drawable/..." | img.setImageResource(...) |
| CheckBox | android:checked="true" | cb.isChecked() |
| RadioButton | W RadioGroup | rg.getCheckedRadioButtonId() |
Layouty
| Layout | Użycie |
|---|---|
| LinearLayout | android:orientation="vertical|horizontal" |
| ConstraintLayout | app:layout_constraint...="parent|@id/xxx" |
| RelativeLayout | android:layout_below="@id/xxx" |
Wymiary
| Wartość | Znaczenie |
|---|---|
match_parent | Wypełnij rodzica |
wrap_content | Dopasuj do zawartości |
dp | Density-independent pixels (wymiary) |
sp | Scale-independent pixels (tekst) |
Pobieranie kontrolek
// W onCreate():
TextView txt = findViewById(R.id.txtNazwa);
EditText edt = findViewById(R.id.edtPole);
Button btn = findViewById(R.id.btnAkcja);
Obsługa kliknięcia
btn.setOnClickListener(v -> {
// kod obsługi
});
// Lub dla wielu przycisków:
btn.setOnClickListener(v -> {
if (v.getId() == R.id.btn1) { ... }
else if (v.getId() == R.id.btn2) { ... }
});
Toast
Toast.makeText(this, "Komunikat", Toast.LENGTH_SHORT).show();
AlertDialog
new AlertDialog.Builder(this)
.setTitle("Tytuł")
.setMessage("Treść")
.setPositiveButton("OK", (dialog, which) -> { /* akcja */ })
.setNegativeButton("Anuluj", null)
.show();
ListView + Adapter
ArrayList<String> lista = new ArrayList<>();
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this, android.R.layout.simple_list_item_1, lista);
listView.setAdapter(adapter);
// Po zmianach:
adapter.notifyDataSetChanged();
SharedPreferences
// Zapis
SharedPreferences prefs = getSharedPreferences("Nazwa", MODE_PRIVATE);
prefs.edit().putString("klucz", "wartość").apply();
// Odczyt
String wartosc = prefs.getString("klucz", "domyślna");
🐛 Android — Typowe błędy
NullPointerException na kontrolce
Przyczyny:
- Literówka w ID:
R.id.txtWynik≠android:id="@+id/txtwynik" - Wywołanie przed
setContentView() - ID z innego layoutu
NumberFormatException
// ✅ Zawsze try-catch:
try {
double x = Double.parseDouble(edt.getText().toString());
} catch (NumberFormatException e) {
Toast.makeText(this, "Podaj liczbę!", Toast.LENGTH_SHORT).show();
}
Lista nie odświeża się
Przyczyna: Brak adapter.notifyDataSetChanged()
lista.add("nowy");
adapter.notifyDataSetChanged(); // WYMAGANE!
RadioGroup nie zwraca wyboru
Przyczyny:
- Użytkownik nic nie zaznaczył — sprawdź warunek!
- RadioButtony nie są w RadioGroup
int id = radioGroup.getCheckedRadioButtonId();
if (id == -1) {
Toast.makeText(this, "Wybierz opcję!", Toast.LENGTH_SHORT).show();
return;
}
Crash przy orientacji ekranu
Przyczyna: Activity jest tworzone od nowa przy obrocie.
Rozwiązanie: Zapisz stan w onSaveInstanceState() lub zablokuj orientację:
<!-- W AndroidManifest.xml: -->
<activity android:screenOrientation="portrait">
Zasób nie znaleziony
Przyczyny:
- Brak pliku w
res/drawable/ - Błędna ścieżka w
setContentView() - Brak wpisu w strings.xml
1. Podstawy Python
Python to interpretowany język o dynamicznej typizacji. Bloki kodu wyznacza indentacja (4 spacje), nie klamry.
Zmienne i typy
imie = "Maks" # str
wiek = 18 # int
srednia = 4.75 # float
zdaje = True # bool
print(type(imie)) # <class 'str'>
input() i print()
# input() ZAWSZE zwraca string!
tekst = input("Podaj imię: ")
liczba = int(input("Podaj wiek: "))
print(f"Cześć {tekst}, masz {liczba} lat") # f-string
Operatory
| Operator | Opis | Przykład |
|---|---|---|
+ - * / | Arytmetyczne | 10 / 3 → 3.333 |
// | Dzielenie całkowite | 10 // 3 → 3 |
% | Reszta z dzielenia | 10 % 3 → 1 |
** | Potęgowanie | 2 ** 3 → 8 |
== != < > | Porównania | 5 == 5 → True |
and or not | Logiczne | True and False → False |
Konwersja typów
int("42") # 42
float("3.14") # 3.14
str(100) # "100"
bool(0) # False — 0, "", [], None → False
📝 Zadanie praktyczne
Napisz program, który pobiera od użytkownika dwie liczby całkowite i wyświetla ich sumę, różnicę, iloczyn oraz resztę z dzielenia pierwszej przez drugą.
a = int(input("Podaj pierwszą liczbę: "))
b = int(input("Podaj drugą liczbę: "))
print(f"Suma: {a + b}")
print(f"Różnica: {a - b}")
print(f"Iloczyn: {a * b}")
print(f"Reszta z dzielenia: {a % b}")
2. Instrukcje warunkowe
if / elif / else
ocena = int(input("Podaj ocenę: "))
if ocena >= 5:
print("Bardzo dobrze!")
elif ocena >= 3:
print("Zaliczone")
else:
print("Niezaliczone")
if/elif/else i indentacji 4 spacje w bloku.Operatory porównania
x == y # równe
x != y # różne
x > y # większe
x >= y # większe lub równe
x < y # mniejsze
x <= y # mniejsze lub równe
Operatory logiczne
if wiek >= 18 and posiada_prawo_jazdy:
print("Może jeździć")
if miasto == "Kraków" or miasto == "Warszawa":
print("Duże miasto")
if not zalogowany:
print("Zaloguj się")
in / not in
if "Python" in jezyki:
print("Zna Pythona!")
if extension not in [".jpg", ".png", ".gif"]:
print("Nieobsługiwany format")
Zagnieżdżone warunki
if wiek >= 18:
if ma_bilet:
print("Wejście dozwolone")
else:
print("Kup bilet")
else:
print("Za młody")
📝 Zadanie praktyczne
Napisz program, który pobiera punkty z egzaminu (0-100) i wyświetla ocenę: 90-100 → 5, 75-89 → 4, 60-74 → 3, 50-59 → 2, poniżej 50 → 1.
punkty = int(input("Podaj punkty (0-100): "))
if punkty >= 90:
ocena = 5
elif punkty >= 75:
ocena = 4
elif punkty >= 60:
ocena = 3
elif punkty >= 50:
ocena = 2
else:
ocena = 1
print(f"Twoja ocena: {ocena}")
3. Pętle
for + range()
# range(stop) — od 0 do stop-1
for i in range(5):
print(i) # 0, 1, 2, 3, 4
# range(start, stop, step)
for i in range(1, 11, 2):
print(i) # 1, 3, 5, 7, 9
for po kolekcji
owoce = ["jabłko", "banan", "gruszka"]
for owoc in owoce:
print(owoc)
# z indeksem
for i, owoc in enumerate(owoce):
print(f"{i}: {owoc}")
while
licznik = 0
while licznik < 5:
print(licznik)
licznik += 1 # WAŻNE: bez tego → nieskończona pętla!
break i continue
# break — przerwij pętlę
for i in range(100):
if i == 5:
break # wychodzi z pętli
print(i) # 0, 1, 2, 3, 4
# continue — pomiń iterację
for i in range(5):
if i == 2:
continue # pomija 2
print(i) # 0, 1, 3, 4
Zagnieżdżone pętle
# Tabliczka mnożenia 3×3
for i in range(1, 4):
for j in range(1, 4):
print(f"{i}×{j}={i*j}", end=" ")
print() # nowa linia po wierszu
📝 Zadanie praktyczne
Napisz program, który pobiera 5 liczb od użytkownika (w pętli) i oblicza ich sumę oraz średnią.
suma = 0
ilosc = 5
for i in range(ilosc):
liczba = float(input(f"Podaj liczbę {i + 1}: "))
suma += liczba
srednia = suma / ilosc
print(f"Suma: {suma}")
print(f"Średnia: {srednia:.2f}")
4. Funkcje
Podstawowa składnia
def powitaj(imie):
return f"Cześć, {imie}!"
wynik = powitaj("Maks")
print(wynik) # Cześć, Maks!
Parametry domyślne
def potega(baza, wykladnik=2):
return baza ** wykladnik
print(potega(3)) # 9 (domyślnie kwadrat)
print(potega(2, 10)) # 1024
Wiele wartości (tuple)
def min_max(lista):
return min(lista), max(lista)
najmn, najw = min_max([3, 1, 7, 2])
print(najmn, najw) # 1 7
Zasięg zmiennych
x = 10 # globalna
def test():
x = 5 # lokalna — NIE zmienia globalnej
print(x) # 5
test()
print(x) # 10 (globalna niezmieniona)
Funkcja jako argument
# sorted() z key=
produkty = [("Laptop", 3500), ("Mysz", 130), ("Monitor", 1300)]
posortowane = sorted(produkty, key=lambda p: p[1])
# → [("Mysz", 130), ("Monitor", 1300), ("Laptop", 3500)]
📝 Zadanie praktyczne
Napisz funkcję pole_trojkata(a, h), która przyjmuje podstawę i wysokość, a zwraca pole trójkąta. Program ma pobierać dane i wywołać funkcję.
def pole_trojkata(a, h):
return (a * h) / 2
podstawa = float(input("Podaj podstawę: "))
wysokosc = float(input("Podaj wysokość: "))
wynik = pole_trojkata(podstawa, wysokosc)
print(f"Pole trójkąta: {wynik:.2f}")
5. Stringi
Tworzenie i f-string
imie = "Maks"
ocena = 4.5
# f-string (Python 3.6+)
print(f"{imie} ma średnią {ocena:.1f}")
# → Maks ma średnią 4.5
# Formatowanie liczb
cena = 1299.5
print(f"Cena: {cena:.2f} zł") # 1299.50 zł
print(f"ID: {42:05d}") # 00042
Metody stringów
| Metoda | Opis | Przykład |
|---|---|---|
.upper() | Wielkie litery | "abc".upper() → "ABC" |
.lower() | Małe litery | "ABC".lower() → "abc" |
.strip() | Usuwa białe znaki | " hi ".strip() → "hi" |
.split(sep) | Dzieli na listę | "a;b;c".split(";") → ["a","b","c"] |
.join(list) | Łączy listę | ", ".join(["a","b"]) → "a, b" |
.replace(a,b) | Zamiana | "kot".replace("o","a") → "kat" |
.find(x) | Indeks (lub -1) | "hello".find("ll") → 2 |
.startswith(x) | Czy zaczyna | "python".startswith("py") → True |
.count(x) | Ile wystąpień | "banana".count("a") → 3 |
Slicing
tekst = "Python"
tekst[0] # "P"
tekst[-1] # "n"
tekst[0:3] # "Pyt"
tekst[2:] # "thon"
tekst[::-1] # "nohtyP" (odwrócenie)
📝 Zadanie praktyczne
Napisz program, który pobiera imię i nazwisko użytkownika (osobno) i wyświetla: inicjały (np. "M.K."), nazwisko wielkimi literami, oraz ilość liter w imieniu.
imie = input("Podaj imię: ")
nazwisko = input("Podaj nazwisko: ")
inicjaly = f"{imie[0].upper()}.{nazwisko[0].upper()}."
print(f"Inicjały: {inicjaly}")
print(f"Nazwisko: {nazwisko.upper()}")
print(f"Ilość liter w imieniu: {len(imie)}")
6. Listy
Tworzenie i indeksowanie
oceny = [5, 4, 3, 5, 4]
print(oceny[0]) # 5 (pierwszy)
print(oceny[-1]) # 4 (ostatni)
print(len(oceny)) # 5
Metody list
| Metoda | Opis |
|---|---|
.append(x) | Dodaj na koniec |
.insert(i, x) | Wstaw na pozycji i |
.remove(x) | Usuń pierwszą wartość x |
.pop(i) | Usuń i zwróć element i |
.sort() | Sortuj rosnąco (mutuje!) |
.reverse() | Odwróć (mutuje!) |
.index(x) | Indeks pierwszego x |
.count(x) | Ile razy x |
List comprehension
# Kwadraty od 1 do 5
kwadraty = [x**2 for x in range(1, 6)]
# → [1, 4, 9, 16, 25]
# Z warunkiem — tylko parzyste
parzyste = [x for x in range(10) if x % 2 == 0]
# → [0, 2, 4, 6, 8]
# Transformacja listy stringów
imiona = ["jan", "ola", "maks"]
duze = [i.capitalize() for i in imiona]
# → ["Jan", "Ola", "Maks"]
Sortowanie
# sorted() — zwraca NOWĄ listę
posortowane = sorted(oceny) # rosnąco
malejaco = sorted(oceny, reverse=True) # malejąco
# .sort() — mutuje oryginalną listę
oceny.sort()
# Sortowanie obiektów po kluczu
produkty = [{"nazwa": "B", "cena": 50}, {"nazwa": "A", "cena": 30}]
produkty.sort(key=lambda p: p["cena"])
Slicing list
l = [10, 20, 30, 40, 50]
l[1:3] # [20, 30]
l[:2] # [10, 20]
l[::2] # [10, 30, 50] (co drugi)
📝 Zadanie praktyczne
Napisz program, który tworzy listę 10 losowych liczb (1-100). Następnie wyświetl: największą, najmniejszą, średnią oraz listę posortowaną malejąco.
import random
liczby = [random.randint(1, 100) for _ in range(10)]
print(f"Lista: {liczby}")
print(f"Maksimum: {max(liczby)}")
print(f"Minimum: {min(liczby)}")
print(f"Średnia: {sum(liczby) / len(liczby):.2f}")
print(f"Malejąco: {sorted(liczby, reverse=True)}")
7. Słowniki i krotki
Słownik (dict)
uczen = {
"imie": "Maks",
"wiek": 18,
"oceny": [5, 4, 5, 3]
}
print(uczen["imie"]) # Maks
print(uczen.get("klasa", "?")) # ? (domyślna wartość)
Modyfikacja słownika
uczen["email"] = "maks@wp.pl" # dodaj / zmień
del uczen["wiek"] # usuń klucz
uczen.pop("email") # usuń i zwróć
Iteracja po słowniku
# Po kluczach
for klucz in uczen:
print(klucz)
# Po wartościach
for wartosc in uczen.values():
print(wartosc)
# Po parach klucz-wartość
for k, v in uczen.items():
print(f"{k}: {v}")
Krotka (tuple) — niezmienna lista
punkt = (10, 20)
x, y = punkt # unpacking
print(x) # 10
# Krotka jest NIEZMIENNA — nie można zmienić elementu
# punkt[0] = 5 # ❌ TypeError!
Zbiory (set) — unikalne wartości
kategorie = {"elektronika", "odzież", "elektronika", "jedzenie"}
print(kategorie) # {"elektronika", "odzież", "jedzenie"}
# Z listy → unikalne
lista = [1, 2, 2, 3, 3, 3]
unikalne = list(set(lista)) # [1, 2, 3]
📝 Zadanie praktyczne
Utwórz słownik opisujący produkt (nazwa, cena, ilość). Napisz funkcję wartosc(produkt), która oblicza wartość magazynową (cena × ilość).
def wartosc(produkt):
return produkt["cena"] * produkt["ilosc"]
laptop = {
"nazwa": "Laptop Dell",
"cena": 3500.00,
"ilosc": 15
}
print(f"Produkt: {laptop['nazwa']}")
print(f"Wartość magazynowa: {wartosc(laptop):.2f} zł")
8. Pliki (open, CSV)
Czytanie pliku
# Najlepsza praktyka — with (auto-zamyka plik)
with open("dane.txt", "r", encoding="utf-8") as plik:
zawartosc = plik.read() # cały plik jako string
print(zawartosc)
# Linia po linii
with open("dane.txt", "r", encoding="utf-8") as plik:
for linia in plik:
linia = linia.strip() # usuń \n
print(linia)
Czytanie CSV/danych rozdzielonych
# dane.txt:
# 1;Laptop;elektronika;3499.99;15
# 2;Mysz;elektronika;129.99;50
produkty = []
with open("dane.txt", "r", encoding="utf-8") as plik:
for linia in plik:
parts = linia.strip().split(";")
produkt = {
"id": int(parts[0]),
"nazwa": parts[1],
"kategoria": parts[2],
"cena": float(parts[3]),
"ilosc": int(parts[4])
}
produkty.append(produkt)
print(produkty[0]["nazwa"]) # Laptop
; lub ,. Zawsze: open() → strip() → split(";") → parsuj typy.Zapis do pliku
# "w" — nadpisz, "a" — dopisz
with open("wynik.txt", "w", encoding="utf-8") as plik:
plik.write("Raport z dnia 2025-01-15\n")
for p in produkty:
plik.write(f"{p['nazwa']}: {p['cena']:.2f} zł\n")
Moduł csv
import csv
with open("dane.csv", "r", encoding="utf-8") as plik:
reader = csv.reader(plik, delimiter=";")
for wiersz in reader:
print(wiersz) # lista stringów
📝 Zadanie praktyczne
Stwórz plik uczniowie.txt z danymi 3 uczniów w formacie: imie;nazwisko;srednia. Wczytaj plik i wyświetl tylko uczniów ze średnią powyżej 4.0.
# Plik uczniowie.txt:
# Anna;Kowalska;4.5
# Jan;Nowak;3.8
# Ewa;Wiśniewska;4.2
with open("uczniowie.txt", "r", encoding="utf-8") as plik:
for linia in plik:
parts = linia.strip().split(";")
imie, nazwisko = parts[0], parts[1]
srednia = float(parts[2])
if srednia > 4.0:
print(f"{imie} {nazwisko}: {srednia:.1f}")
9. PyQt6 (GUI)
PyQt6 to biblioteka do tworzenia interfejsów graficznych. Na egzaminie INF.04 to wymagany element Części I. Zainstaluj: pip install PyQt6.
Obie biblioteki opakowują to samo Qt i wyglądają prawie identycznie, więc przykłady bardzo łatwo pomylić. U nas i na egzaminie INF.04 obowiązuje PyQt6. Cztery miejsca, w których kod napisany pod PySide6 wysypie się pod PyQt6:
| Element | PyQt6 — tak piszemy | PySide6 — nie używamy |
|---|---|---|
| import | from PyQt6 import QtWidgets | from PySide6 import QtWidgets |
konwersja .ui | pyuic6 mainwindow.ui -o mainwindow.py | pyside6-uic |
| stałe (enumy) | pełna ścieżka: Qt.AlignmentFlag.AlignCenter | skrót: Qt.AlignCenter |
| własne sygnały | pyqtSignal, pyqtSlot | Signal, Slot |
Jeśli w znalezionym kodzie widzisz Qt.AlignCenter albo Signal(...) — to przykład z PySide, przepisz go przed wklejeniem. Trzecia wersja, na którą trafisz w starszych poradnikach, to PyQt5: poznasz ją po app.exec_() z podkreślnikiem (w PyQt6 jest app.exec()) i po imporcie from PyQt5 ....
Workflow: Qt Creator → pyuic6 → VS Code
1. Projektujesz formularz wizualnie w Qt Creator (Designer) — przeciągasz widżety myszką
2. Zapisujesz formularz jako plik
.ui3. Konwertujesz na Python komendą:
pyuic6 mainwindow.ui -o mainwindow.py4. Tworzysz
main.py w VS Code — importujesz wygenerowany formularz i dodajesz logikę (funkcje, SQL, sygnały)Qt Creator — tworzenie formularza
- Otwórz Qt Creator → File → New → Qt Designer Form → wybierz Main Window → Create
- Z panelu Widget Box (po lewej) przeciągnij potrzebne widżety na formularz (np. QLabel, QPushButton, QListWidget)
- W panelu Property Editor (po prawej) ustaw
objectNamekażdego widżetu — to nazwa, przez którą odwołujesz się w kodzie (np.listWidget,comboBox,btnFiltruj,lblWartosc) - Ustaw layout: zaznacz formularz → prawy klik → Lay out in a Grid (lub Vertically / Horizontally)
- Zapisz jako
mainwindow.uiw folderze projektu
Konwersja: pyuic6
W terminalu (w folderze projektu) uruchom:
pyuic6 mainwindow.ui -o mainwindow.py
Komenda wygeneruje plik mainwindow.py z klasą Ui_MainWindow. Tego pliku nie edytujemy ręcznie — przy zmianach w formularzu uruchamiamy komendę ponownie.
Minimalne okno z formularzem
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Dostęp do widżetów
Po self.ui.setupUi(self) wszystkie widżety są dostępne przez self.ui:
# QLabel — odczyt i zapis tekstu
self.ui.label.setText("Nowy tekst")
# QLineEdit — pole tekstowe
tekst = self.ui.lineEdit.text()
self.ui.lineEdit.clear()
# QPushButton — podłączenie sygnału
self.ui.pushButton.clicked.connect(self.moja_funkcja)
# QListWidget — lista elementów
self.ui.listWidget.clear()
self.ui.listWidget.addItem("Element")
# QComboBox — lista rozwijana
self.ui.comboBox.addItems(["A", "B", "C"])
wybrane = self.ui.comboBox.currentText()
# QCheckBox — pole wyboru
if self.ui.checkBox.isChecked():
print("zaznaczony")
Tabela widżetów
| Widżet | Opis | Ważne metody |
|---|---|---|
| QLabel | Etykieta tekstowa | .setText(), .text() |
| QLineEdit | Pole tekstowe (input) | .text(), .setText(), .clear() |
| QPushButton | Przycisk | .clicked.connect(fn) |
| QListWidget | Lista elementów | .addItem(), .clear(), .currentRow() |
| QComboBox | Lista rozwijana | .addItems(), .currentText() |
| QCheckBox | Pole wyboru | .isChecked(), .stateChanged.connect(fn) |
| QStackedWidget | Kontener stron (widoków) | .setCurrentIndex(n), .currentIndex(), .count() |
Layouty (w Qt Creator)
| Grid Layout | Vertical / Horizontal Layout |
|---|---|
| Siatka (wiersze × kolumny) | Stackowanie (pionowo / poziomo) |
| Precyzyjna kontrola pozycji | Prosty układ liniowy |
| Prawy klik → Lay out in a Grid | Prawy klik → Lay out Vertically/Horizontally |
QMessageBox
from PyQt6.QtWidgets import QMessageBox
QMessageBox.information(window, "Sukces", "Dane zapisane!")
QMessageBox.critical(window, "Błąd", "Plik nie znaleziony")
odp = QMessageBox.question(window, "Pytanie", "Usunąć?")
📝 Zadanie praktyczne
W Qt Creator utwórz formularz (Main Window) z polem QLineEdit (objectName: lineEdit), przyciskiem QPushButton „Dodaj" (objectName: btnDodaj) i QListWidget (objectName: listWidget). Zapisz jako mainwindow.ui, skonwertuj komendą pyuic6 mainwindow.ui -o mainwindow.py. Następnie w main.py napisz logikę: po kliknięciu przycisku tekst z QLineEdit dodaje się do QListWidget.
# main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.btnDodaj.clicked.connect(self.dodaj)
def dodaj(self):
tekst = self.ui.lineEdit.text()
if tekst:
self.ui.listWidget.addItem(tekst)
self.ui.lineEdit.clear()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
10. SQLite
sqlite3 to wbudowany moduł Python do baz danych SQL. Na egzaminie INF.04 prawie zawsze trzeba utworzyć bazę, tabelę i wykonać zapytania.
Połączenie i tworzenie tabeli
import sqlite3
conn = sqlite3.connect("magazyn.db") # tworzy plik
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS produkty (
id INTEGER PRIMARY KEY,
nazwa TEXT NOT NULL,
kategoria TEXT,
cena REAL,
ilosc INTEGER
)
""")
conn.commit()
INSERT — wstawianie danych
# Jeden rekord
cursor.execute(
"INSERT INTO produkty VALUES (?, ?, ?, ?, ?)",
(1, "Laptop", "elektronika", 3499.99, 15)
)
# Wiele rekordów z pętli
for p in produkty:
cursor.execute(
"INSERT INTO produkty VALUES (?, ?, ?, ?, ?)",
(p["id"], p["nazwa"], p["kategoria"], p["cena"], p["ilosc"])
)
conn.commit()
? (parametry) zamiast f-stringów! Chroni przed SQL injection i błędami w danych.SELECT — zapytania
# Wszystkie rekordy
cursor.execute("SELECT * FROM produkty")
wyniki = cursor.fetchall() # lista krotek
for wiersz in wyniki:
print(wiersz) # (1, 'Laptop', 'elektronika', 3499.99, 15)
# Filtrowanie
cursor.execute(
"SELECT * FROM produkty WHERE kategoria = ?",
("elektronika",) # ← tuple z jednym elementem (przecinek!)
)
# Sortowanie
cursor.execute("SELECT * FROM produkty ORDER BY cena DESC")
# Agregacja
cursor.execute("SELECT kategoria, SUM(cena * ilosc) FROM produkty GROUP BY kategoria")
UPDATE i DELETE
# Aktualizacja
cursor.execute("UPDATE produkty SET cena = ? WHERE id = ?", (2999.99, 1))
# Usuwanie
cursor.execute("DELETE FROM produkty WHERE id = ?", (5,))
conn.commit()
Zamykanie połączenia
# Zawsze na końcu:
conn.close()
# Lub z with:
with sqlite3.connect("magazyn.db") as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM produkty")
print(cursor.fetchall())
Schemat typowy na egzaminie
# 1. Wczytaj dane z pliku
# 2. Utwórz bazę + tabelę
# 3. Wstaw dane z pliku do bazy
# 4. Wykonaj zapytania (filter, sort, aggregate)
# 5. Wyświetl wyniki w PyQt6
📝 Zadanie praktyczne
Utwórz bazę SQLite sklep.db z tabelą produkty (id, nazwa, cena). Wstaw 3 produkty i napisz zapytanie pokazujące produkty z ceną powyżej 100 zł, posortowane malejąco.
import sqlite3
conn = sqlite3.connect("sklep.db")
cursor = conn.cursor()
# Tworzenie tabeli
cursor.execute("""
CREATE TABLE IF NOT EXISTS produkty (
id INTEGER PRIMARY KEY,
nazwa TEXT NOT NULL,
cena REAL
)
""")
# Wstawianie danych
produkty = [
(1, "Laptop", 3500.00),
(2, "Mysz", 80.00),
(3, "Monitor", 1200.00)
]
cursor.executemany("INSERT OR REPLACE INTO produkty VALUES (?, ?, ?)", produkty)
conn.commit()
# Zapytanie
cursor.execute("SELECT * FROM produkty WHERE cena > 100 ORDER BY cena DESC")
wyniki = cursor.fetchall()
for p in wyniki:
print(f"{p[1]}: {p[2]:.2f} zł")
conn.close()
🔥 Rozgrzewka — Rejestr produktów
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
with open("dane.txt", "r", encoding="utf-8") as plik:. Dzięki with plik automatycznie się zamyka. Do odczytu: plik.read() (cały tekst) lub plik.readlines() (lista linii).split(";") i co zwraca?split(";") dzieli string na listę po podanym separatorze. Np. "a;b;c".split(";") zwraca ["a", "b", "c"]. Każdy element jest stringiem — jeśli potrzebujemy liczby, musimy rzutować: int() lub float().? zamiast f-stringa do wstawiania wartości??) chronią przed SQL Injection i unikają problemów z apostrofami w danych. cursor.execute("SELECT * FROM produkty WHERE kategoria = ?", (kategoria,)) — drugi argument to krotka z wartościami.CREATE TABLE IF NOT EXISTS. Przykład: cursor.execute("CREATE TABLE IF NOT EXISTS produkty (id INTEGER PRIMARY KEY, nazwa TEXT, cena REAL)"). Bez IF NOT EXISTS przy ponownym uruchomieniu pojawi się błąd.list_widget.addItem("tekst"). Aby wyczyścić listę przed ponownym załadowaniem: list_widget.clear(). QListWidget to widżet wyświetlający listę elementów tekstowych — odpowiednik Listboxa z tkinter.#01 — Instrukcja z arkusza egzaminacyjnego
Ćwiczenie własne na wzór INF.04 · aplikacja desktopowa · nie jest arkuszem CKE. Prawdziwa część I arkusza ze stycznia 2025 to ćwiczenie #08
Kontekst
Z zastosowaniem języka Python wykonaj aplikację desktopową realizującą funkcję rejestru produktów w magazynie.
Archiwum materiałów: pliki1.zip, hasło: M@gazyn)
Dane wejściowe — dane.txt
Plik zawiera dane produktów oddzielone średnikiem (bez nagłówka):
1;Laptop Dell;elektronika;3499.99;15
2;Mysz Logitech;elektronika;129.99;50
3;Koszulka Nike;odzież;89.99;120
4;Słuchawki Sony;elektronika;249.99;30
5;Spodnie Levi's;odzież;199.99;45
6;Monitor LG;elektronika;1299.99;20
7;Bluza Adidas;odzież;149.99;60
8;Klawiatura Corsair;elektronika;359.99;25
Format: id;nazwa;kategoria;cena;ilość
Baza danych
- Utwórz bazę SQLite: magazyn.db
- Tabela produkty: id (INTEGER PK), nazwa (TEXT), kategoria (TEXT), cena (REAL), ilosc (INTEGER)
- Wstaw dane z pliku do tabeli
Zapytania SQL
- Wyświetl wszystkie produkty posortowane po cenie malejąco
- Wyświetl produkty z wybranej kategorii
- Oblicz łączną wartość magazynu (cena × ilość) per kategoria
GUI — PyQt6
- Tytuł okna: „Rejestr produktów"
- QListWidget wyświetlający produkty (nazwa — cena — ilość)
- QComboBox z kategoriami + przycisk „Filtruj"
- QLabel z łączną wartością wyświetlonych produktów
Wymagania techniczne
- Pętle i warunki, znaczące nazwy zmiennych
- Obsługa pliku z
with open() - Parametryzowane zapytania SQL (
?)
Na lekcji: Przeczytajcie instrukcję razem. Niech uczeń zidentyfikuje 4 główne etapy: plik → baza → zapytania → GUI.
#01 — Wprowadzenie dla nauczyciela
1. Otwarcie
„To ćwiczenie w stylu egzaminu INF.04: aplikacja desktopowa w Pythonie z plikiem i bazą SQLite."
„Na egzaminie masz 180 minut na 3 części. Część I to ~60 minut. My to zrobimy w 30 minut."
2. Diagnoza wiedzy
„Co pamiętasz z Pythona? Cokolwiek."
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „print, input, pętle" | „Super — tego potrzebujemy + plik i baza." |
| „PyQt6 coś tam" | „Dobrze. Przypomnisz sobie przy pisaniu." |
| „SQL trochę" | „Wystarczy SELECT, INSERT, CREATE. Reszta to Python." |
| (cisza) | „Ok, za 30 min masz gotową aplikację." |
3. Architektura
mainwindow.ui (Qt Creator → Main Window):
QLabel "Rejestr produktów", QLabel "Kategoria:",
QComboBox, QPushButton "Filtruj", QListWidget, QLabel wartość
Terminal: pyuic6 mainwindow.ui -o mainwindow.py
main.py (VS Code):
class MainWindow(QtWidgets.QMainWindow):
1. Wczytaj dane.txt → lista dict-ów
2. Utwórz magazyn.db + tabela produkty
3. INSERT z pętli
4. from mainwindow import Ui_MainWindow
5. self.ui.comboBox — kategorie z bazy
6. Metoda zaladuj_dane() → self.ui.listWidget
7. self.ui.btnFiltruj.clicked.connect(self.filtruj)
„Jakie 3 moduły Pythona będziemy importować?"
Odpowiedź: sys, PyQt6.QtWidgets, sqlite3 (oraz PyQt6.QtGui dla czcionek).
4. Zasady prowadzenia
5. Częste blokady
| Problem | Podpowiedź |
|---|---|
| IndentationError | „Sprawdź wcięcia — 4 spacje, nie tabulatory." |
| FileNotFoundError | „Czy dane.txt jest w tym samym folderze co main.py?" |
| split() nie dzieli poprawnie | „Jaki separator? Średnik? split(';')" |
| Cena to string po split | „split() daje stringi. float(parts[3]) — rzutuj." |
| SQL — tuple z jednym elementem | „(wartość,) — nie zapomnij przecinka!" |
| QListWidget puste | „Czy wywołujesz funkcję ładującą dane? Czy window.show() jest na końcu?" |
| ModuleNotFoundError: PyQt6 | „Zainstaluj: pip install PyQt6" |
6. Po zbudowaniu
„Dlaczego używamy '?' w SQL zamiast f-stringa?"
Odp: SQL injection + apostrofy w danych mogą zepsuć zapytanie.
„Co by się stało gdybyś zapomniał conn.commit()?"
Odp: Dane nie zapiszą się na dysk. Po zamknięciu programu — pusta baza.
#01 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Wczytanie dane.txt | with open(), split(";"), parsowanie typów | ✅ |
| Baza magazyn.db | sqlite3.connect() | ✅ |
| Tabela produkty | CREATE TABLE IF NOT EXISTS | ✅ |
| INSERT danych | Parametryzowane (?) z pętli | ✅ |
| SELECT wszystkie | ORDER BY cena DESC | ✅ |
| SELECT po kategorii | WHERE kategoria = ? | ✅ |
| Agregacja | SUM(cena * ilosc) GROUP BY | ✅ |
| PyQt6 okno | QMainWindow + setWindowTitle(„Rejestr produktów") | ✅ |
| QListWidget | Wyświetla produkty | ✅ |
| QComboBox kategorii | QComboBox + addItems() z listą kategorii | ✅ |
| Przycisk Filtruj | QPushButton + clicked.connect() → odśwież listę | ✅ |
| QLabel wartość | Łączna wartość = cena × ilość | ✅ |
| Znaczące nazwy | produkty, kategoria, wartosc, filtruj | ✅ |
#01 — Budowa krok po kroku
„Startujemy. Najpierw zaprojektujemy formularz w Qt Creator, a potem napiszemy logikę w VS Code."
1. Otwórz Qt Creator → File → New → Qt Designer Form → Main Window → Create
2. Przeciągnij na formularz widżety:
•
QLabel — tekst „Rejestr produktów" (objectName: lblTytul)•
QLabel — tekst „Kategoria:" (objectName: lblKategoria)•
QComboBox (objectName: comboBox)•
QPushButton — tekst „Filtruj" (objectName: btnFiltruj)•
QListWidget (objectName: listWidget)•
QLabel — tekst „Wartość: 0.00 zł" (objectName: lblWartosc)3. Zaznacz formularz → prawy klik → Lay out in a Grid
4. Zapisz jako
mainwindow.ui„Teraz konwertujemy formularz na kod Python. W terminalu wpisz:"
pyuic6 mainwindow.ui -o mainwindow.py
„Mamy plik mainwindow.py — tego nie ruszamy. Teraz tworzymy main.py w VS Code."
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
„Uruchom — widzisz okno z widżetami z formularza? Działa? Super, to nasz szkielet."
Uwaga: self.ui.setupUi(self) tworzy wszystkie widżety z formularza. Odwołujesz się do nich przez self.ui.nazwa, np. self.ui.listWidget, self.ui.comboBox.
„Najpierw stwórz plik dane.txt obok main.py — skopiuj 8 wierszy z instrukcji."
# Wczytaj dane z pliku do listy słowników
produkty = []
with open("dane.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
produkty.append({
"id": int(parts[0]),
"nazwa": parts[1],
"kategoria": parts[2],
"cena": float(parts[3]),
"ilosc": int(parts[4])
})
print(f"Wczytano {len(produkty)} produktów") # test
for p in produkty:
print(p)
„Dlaczego strip() przed split()?"
Odp: Każda linia ma \n na końcu. strip() go usuwa.
Jeśli ValueError przy int/float:
„Wydrukuj parts — może pusta linia na końcu pliku? Dodaj: if not linia.strip(): continue"
„Teraz tworzymy bazę magazyn.db i tabelę produkty. Python sam stworzy plik .db."
conn = sqlite3.connect("magazyn.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS produkty (
id INTEGER PRIMARY KEY,
nazwa TEXT NOT NULL,
kategoria TEXT,
cena REAL,
ilosc INTEGER
)
""")
conn.commit()
„Co robi IF NOT EXISTS?"
Odp: Nie wyrzuci błędu gdy tabela już istnieje — bezpieczne przy wielokrotnym uruchomieniu.
# Wyczyść starą tabelę (przy ponownym uruchomieniu)
cursor.execute("DELETE FROM produkty")
for p in produkty:
cursor.execute(
"INSERT INTO produkty VALUES (?, ?, ?, ?, ?)",
(p["id"], p["nazwa"], p["kategoria"], p["cena"], p["ilosc"])
)
conn.commit()
print("Dane wstawione do bazy")
„Dlaczego 5 znaków zapytania?"
Odp: Bo tabela ma 5 kolumn. Każdy ? to jedno pole.
Jeśli zapomni przecinka w tuple:
„(wartość) to nawiasy. (wartość,) to tuple. Python wymaga tuple."
„Przetestujmy zapytania w konsoli, zanim podepniemy pod GUI."
# Wszystkie posortowane po cenie
cursor.execute("SELECT * FROM produkty ORDER BY cena DESC")
for row in cursor.fetchall():
print(row)
# Filtr po kategorii
cursor.execute(
"SELECT * FROM produkty WHERE kategoria = ?",
("elektronika",) # ← przecinek = tuple!
)
print(cursor.fetchall())
# Wartość magazynu per kategoria
cursor.execute("""
SELECT kategoria, SUM(cena * ilosc) as wartosc
FROM produkty
GROUP BY kategoria
""")
for row in cursor.fetchall():
print(f"{row[0]}: {row[1]:.2f} zł")
„Co robi GROUP BY?"
Odp: Grupuje wiersze po kategorii i liczy SUM osobno dla każdej grupy.
„Widżety już mamy z Qt Creator! Teraz uzupełniamy comboBox kategoriami z bazy."
# Pobierz kategorie z bazy i dodaj do comboBox
cursor.execute("SELECT DISTINCT kategoria FROM produkty ORDER BY kategoria")
kategorie = ["wszystkie"] + [row[0] for row in cursor.fetchall()]
self.ui.comboBox.addItems(kategorie)
self.ui.comboBox.setCurrentText("wszystkie")
„Dlaczego pobieramy kategorie z bazy zamiast wpisywać ręcznie?"
Odp: Bo dane mogą się zmienić. DISTINCT + ORDER BY daje unikalne, posortowane kategorie.
Dostęp do widżetów: Wszystkie widżety z formularza Qt Creator są dostępne przez self.ui.nazwa. Nazwy ustawiłeś w objectName w Qt Creator.
„Piszemy funkcję, która pobiera dane z bazy i wrzuca do QListWidget."
def zaladuj_dane(self, kategoria="wszystkie"):
self.ui.listWidget.clear()
if kategoria == "wszystkie":
cursor.execute("SELECT * FROM produkty ORDER BY cena DESC")
else:
cursor.execute(
"SELECT * FROM produkty WHERE kategoria = ? ORDER BY cena DESC",
(kategoria,)
)
wyniki = cursor.fetchall()
wartosc = 0
for row in wyniki:
linia = f"{row[1]:<25} {row[3]:>10.2f} zł x{row[4]}"
self.ui.listWidget.addItem(linia)
wartosc += row[3] * row[4]
self.ui.lblWartosc.setText(f"Wartość: {wartosc:,.2f} zł")
# Załaduj dane od razu (na starcie)
self.zaladuj_dane()
„Co robi self.ui.listWidget.clear()?"
Odp: Czyści listę przed załadowaniem nowych danych. Bez tego — zduplikowane wpisy.
Dostęp do widżetów: self.ui.listWidget to QListWidget z formularza. self.ui.lblWartosc to QLabel. Nazwy odpowiadają objectName z Qt Creator.
„Ostatni krok — podpinamy przycisk Filtruj do funkcji filtrowania."
def filtruj(self):
wybrana = self.ui.comboBox.currentText()
self.zaladuj_dane(wybrana)
# Podłącz sygnał clicked do metody:
self.ui.btnFiltruj.clicked.connect(self.filtruj)
„Dlaczego self.ui.btnFiltruj.clicked.connect(self.filtruj) a nie clicked.connect(self.filtruj())?"
Odp: Z nawiasami wywołuje się od razu. Bez — przekazujesz referencję do funkcji. To tzw. mechanizm sygnałów i slotów w Qt.
Bonus: Dodaj dynamiczne kategorie — zamiast hardcodowanych, pobierz z bazy: SELECT DISTINCT kategoria FROM produkty
#01 — Gotowy kod
Formularz Qt Creator (mainwindow.ui)
| Widżet | objectName | Właściwości |
|---|---|---|
| QLabel | lblTytul | text: „Rejestr produktów", font: bold 18pt |
| QLabel | lblKategoria | text: „Kategoria:" |
| QComboBox | comboBox | (elementy dodawane z kodu) |
| QPushButton | btnFiltruj | text: „Filtruj" |
| QListWidget | listWidget | font: Consolas 10pt |
| QLabel | lblWartosc | text: „Wartość: 0.00 zł", font: bold 12pt |
Konwersja: pyuic6 mainwindow.ui -o mainwindow.py
main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# ===== 1. WCZYTANIE DANYCH =====
produkty = []
with open("dane.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
produkty.append({
"id": int(parts[0]),
"nazwa": parts[1],
"kategoria": parts[2],
"cena": float(parts[3]),
"ilosc": int(parts[4])
})
# ===== 2. BAZA DANYCH =====
self.conn = sqlite3.connect("magazyn.db")
self.cursor = self.conn.cursor()
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS produkty (
id INTEGER PRIMARY KEY,
nazwa TEXT NOT NULL,
kategoria TEXT,
cena REAL,
ilosc INTEGER
)
""")
self.cursor.execute("DELETE FROM produkty")
for p in produkty:
self.cursor.execute(
"INSERT INTO produkty VALUES (?, ?, ?, ?, ?)",
(p["id"], p["nazwa"], p["kategoria"], p["cena"], p["ilosc"])
)
self.conn.commit()
self.setup_logic()
def setup_logic(self):
# ===== 3. WIDŻETY =====
self.cursor.execute("SELECT DISTINCT kategoria FROM produkty ORDER BY kategoria")
kategorie = ["wszystkie"] + [row[0] for row in self.cursor.fetchall()]
self.ui.comboBox.addItems(kategorie)
self.ui.comboBox.setCurrentText("wszystkie")
# ===== 4. SYGNAŁY =====
self.ui.btnFiltruj.clicked.connect(self.filtruj)
self.zaladuj_dane()
# ===== 5. METODY =====
def zaladuj_dane(self, kategoria="wszystkie"):
self.ui.listWidget.clear()
if kategoria == "wszystkie":
self.cursor.execute("SELECT * FROM produkty ORDER BY cena DESC")
else:
self.cursor.execute(
"SELECT * FROM produkty WHERE kategoria = ? ORDER BY cena DESC",
(kategoria,)
)
wyniki = self.cursor.fetchall()
wartosc = 0
for row in wyniki:
linia = f"{row[1]:<25} {row[3]:>10.2f} zł x{row[4]}"
self.ui.listWidget.addItem(linia)
wartosc += row[3] * row[4]
self.ui.lblWartosc.setText(f"Wartość: {wartosc:,.2f} zł")
def filtruj(self):
wybrana = self.ui.comboBox.currentText()
self.zaladuj_dane(wybrana)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
dane.txt
1;Laptop Dell;elektronika;3499.99;15
2;Mysz Logitech;elektronika;129.99;50
3;Koszulka Nike;odzież;89.99;120
4;Słuchawki Sony;elektronika;249.99;30
5;Spodnie Levi's;odzież;199.99;45
6;Monitor LG;elektronika;1299.99;20
7;Bluza Adidas;odzież;149.99;60
8;Klawiatura Corsair;elektronika;359.99;25
📝 Sprawdzian — Rejestr produktów
5 pytań · 5 minut · minimum 60% żeby zdać
"10;Laptop;3499.99".split(";")?? w zapytaniach SQL zamiast f-stringów?filtruj?🔥 Rozgrzewka — Dziennik ucznia
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
sum(lista) / len(lista). Przykład: oceny = [4, 5, 3]; srednia = sum(oceny) / len(oceny). Można też użyć statistics.mean(lista) po imporcie modułu statistics.SELECT uczen, AVG(ocena) FROM oceny GROUP BY uczen. Funkcja AVG() liczy średnią, a GROUP BY grupuje wyniki według unikalnych wartości w kolumnie uczen.fetchone() od fetchall()?fetchone() zwraca jeden wiersz (krotkę) lub None. fetchall() zwraca listę wszystkich wierszy. Dla dużych zbiorów danych lepiej używać fetchone() w pętli lub for row in cursor:.f"{srednia:.2f}". Lub funkcji round(srednia, 2). Przykład: srednia = 4.666667 → f"{srednia:.2f}" → "4.67".list_widget.addItem("tekst") — dodaje na koniec. Do wyczyszczenia: list_widget.clear(). QListWidget to odpowiednik Listboxa — wyświetla listę elementów tekstowych.#02 — Instrukcja z arkusza egzaminacyjnego
INF.04-06-24.06-SG · Czerwiec 2024 · Czas: 180 min · Część I: Aplikacja desktopowa
Kontekst
Z zastosowaniem języka Python wykonaj aplikację desktopową realizującą funkcję dziennika ocen ucznia.
Archiwum materiałów: pliki2.zip, hasło: Dzi3nn!k
Dane wejściowe — dane_oceny.txt
Plik zawiera dane ocen oddzielone średnikiem (bez nagłówka):
1;Jan Kowalski;matematyka;5
2;Jan Kowalski;fizyka;4
3;Jan Kowalski;informatyka;6
4;Anna Nowak;matematyka;3
5;Anna Nowak;fizyka;5
6;Anna Nowak;informatyka;4
7;Piotr Wiśniewski;matematyka;4
8;Piotr Wiśniewski;fizyka;3
9;Piotr Wiśniewski;informatyka;5
10;Maria Zielińska;matematyka;2
11;Maria Zielińska;fizyka;4
12;Maria Zielińska;informatyka;3
Format: id;imie_nazwisko;przedmiot;ocena
Baza danych
- Utwórz bazę SQLite: dziennik.db
- Tabela oceny: id (INTEGER PK), uczen (TEXT), przedmiot (TEXT), ocena (INTEGER)
- Wstaw dane z pliku do tabeli
Zapytania SQL
- Wyświetl wszystkie oceny posortowane po uczniu i przedmiocie
- Oblicz średnią ocen per przedmiot
- Znajdź ucznia z najwyższą średnią
- Filtruj oceny po wybranym przedmiocie
GUI — PyQt6
- Tytuł okna: „Dziennik ucznia"
- QListWidget wyświetlający oceny (uczeń — przedmiot — ocena)
- QComboBox z przedmiotami + przycisk „Filtruj"
- QLabel ze średnią ocen wyświetlonych pozycji
- QLabel z najlepszym uczniem
Wymagania techniczne
- Pętle i warunki, znaczące nazwy zmiennych
- Obsługa pliku z
with open() - Parametryzowane zapytania SQL (
?)
Na lekcji: Uczeń powinien rozpoznać schemat: plik → baza → zapytania → GUI — identyczny jak w #01.
#02 — Wprowadzenie dla nauczyciela
1. Otwarcie
„Drugi egzamin. Schemat ten sam: plik → baza → zapytania → PyQt6. Ale teraz mamy oceny i średnie — trochę więcej matmy."
„Po #01 wiesz jak wczytać plik i zrobić bazę. Teraz nacisk na agregację: AVG, GROUP BY, ORDER BY."
2. Diagnoza wiedzy
„Jak obliczysz średnią w SQL?"
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „AVG(ocena)" | „Dokładnie! A z GROUP BY daje średnią per przedmiot." |
| „sum / count?" | „W Pythonie tak. W SQL masz gotowy AVG()." |
| (nie wie) | „AVG() to wbudowana funkcja SQL. Średnia automatycznie." |
3. Architektura
mainwindow.ui (Qt Creator → Main Window):
QLabel „Dziennik ucznia", QLabel „Przedmiot:",
QComboBox, QPushButton „Filtruj", QListWidget,
QLabel średnia, QLabel najlepszy
Terminal: pyuic6 mainwindow.ui -o mainwindow.py
main.py (VS Code):
class MainWindow(QtWidgets.QMainWindow):
1. Wczytaj dane_oceny.txt → lista dict-ów
2. Utwórz dziennik.db + tabela oceny
3. INSERT z pętli
4. from mainwindow import Ui_MainWindow
5. self.ui.comboBox — przedmioty z bazy
6. Metoda zaladuj() → self.ui.listWidget
7. self.ui.btnFiltruj.clicked.connect(self.filtruj)
4. Zasady prowadzenia
5. Częste blokady
| Problem | Podpowiedź |
|---|---|
| Ocena to string po split | „int(parts[3]) — zawsze rzutuj po split." |
| AVG zwraca None | „Brak danych? Sprawdź czy INSERT przeszedł (commit!)." |
| Nie wie jak sformatować średnią | „f'{avg:.2f}' — dwa miejsca po przecinku." |
| GROUP BY bez AVG | „GROUP BY grupuje, AVG() liczy średnią per grupę." |
6. Po zbudowaniu
„Czym różni się ROUND(AVG(ocena),2) od formatowania w Pythonie f-stringiem?"
Odp: SQL zaokrągla w bazie — dostaniesz float. f-string formatuje tylko wyświetlanie.
#02 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Wczytanie dane_oceny.txt | with open(), split(";"), int(ocena) | ✅ |
| Baza dziennik.db | sqlite3.connect() | ✅ |
| Tabela oceny | CREATE TABLE IF NOT EXISTS | ✅ |
| INSERT danych | Parametryzowane z pętli | ✅ |
| SELECT wszystkie | ORDER BY uczen, przedmiot | ✅ |
| AVG per przedmiot | SELECT przedmiot, AVG(ocena) GROUP BY | ✅ |
| Najlepszy uczeń | AVG + ORDER BY DESC LIMIT 1 | ✅ |
| Filtr po przedmiocie | WHERE przedmiot = ? | ✅ |
| PyQt6 okno | QMainWindow + setWindowTitle(„Dziennik ucznia") | ✅ |
| QListWidget | Wyświetla oceny | ✅ |
| QComboBox przedmiotów | QComboBox + addItems() z DISTINCT | ✅ |
| QLabel średnia | Średnia wyświetlonych ocen | ✅ |
| QLabel najlepszy | Uczeń z najwyższą średnią | ✅ |
#02 — Budowa krok po kroku
„Identyczny start jak w #01. Formularz w Qt Creator, potem pyuic6."
1. File → New → Qt Designer Form → Main Window → Create
2. Przeciągnij widżety:
•
QLabel — „Dziennik ucznia" (objectName: lblTytul)•
QLabel — „Przedmiot:" (objectName: lblPrzedmiot)•
QComboBox (objectName: comboBox)•
QPushButton — „Filtruj" (objectName: btnFiltruj)•
QListWidget (objectName: listWidget)•
QLabel — „Średnia: —" (objectName: lblSrednia)•
QLabel — „Najlepszy: —" (objectName: lblNajlepszy)3. Zaznacz formularz → prawy klik → Lay out in a Grid
4. Zapisz jako
mainwindow.uipyuic6 mainwindow.ui -o mainwindow.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
„Pamiętasz ten schemat z #01? Uruchom — widzisz okno z widżetami."
„Stwórz dane_oceny.txt z 12 wierszami z instrukcji. Potem wczytaj."
dane = []
with open("dane_oceny.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
dane.append({
"id": int(parts[0]),
"uczen": parts[1],
"przedmiot": parts[2],
"ocena": int(parts[3])
})
print(f"Wczytano {len(dane)} ocen")
„Co się zmieni jeśli ocena to 3.5? Jakiego typu użyjesz?"
Odp: float() zamiast int(). Ale w tym zadaniu oceny są całkowite.
„Teraz baza, tabela, wstawienie — 3 kroki w jednym. Znasz to z #01."
conn = sqlite3.connect("dziennik.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS oceny (
id INTEGER PRIMARY KEY,
uczen TEXT NOT NULL,
przedmiot TEXT NOT NULL,
ocena INTEGER
)
""")
cursor.execute("DELETE FROM oceny")
for d in dane:
cursor.execute(
"INSERT INTO oceny VALUES (?, ?, ?, ?)",
(d["id"], d["uczen"], d["przedmiot"], d["ocena"])
)
conn.commit()
print("Dane w bazie")
„Dlaczego DELETE przed INSERT?"
Odp: Żeby przy ponownym uruchomieniu nie dublować danych. IF NOT EXISTS chroni tabelę, nie dane.
# Średnia per przedmiot
cursor.execute("""
SELECT przedmiot, ROUND(AVG(ocena), 2) as srednia
FROM oceny
GROUP BY przedmiot
""")
for row in cursor.fetchall():
print(f"{row[0]}: {row[1]}")
# Najlepszy uczeń (najwyższa średnia)
cursor.execute("""
SELECT uczen, ROUND(AVG(ocena), 2) as srednia
FROM oceny
GROUP BY uczen
ORDER BY srednia DESC
LIMIT 1
""")
najlepszy = cursor.fetchone()
print(f"Najlepszy: {najlepszy[0]} ({najlepszy[1]})")
„Co robi LIMIT 1?"
Odp: Zwraca tylko pierwszy wynik. ORDER BY DESC + LIMIT 1 = rekord z najwyższą wartością.
„Widżety mamy z Qt Creator. Teraz uzupełniamy comboBox przedmiotami z bazy."
# Pobierz przedmioty z bazy i dodaj do comboBox
self.cursor.execute("SELECT DISTINCT przedmiot FROM oceny ORDER BY przedmiot")
przedmioty = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(przedmioty)
self.ui.comboBox.setCurrentText("wszystkie")
Dostęp do widżetów: self.ui.comboBox, self.ui.listWidget, self.ui.lblSrednia, self.ui.lblNajlepszy — nazwy z objectName w Qt Creator.
def zaladuj(self, przedmiot="wszystkie"):
self.ui.listWidget.clear()
if przedmiot == "wszystkie":
self.cursor.execute("SELECT * FROM oceny ORDER BY uczen, przedmiot")
else:
self.cursor.execute(
"SELECT * FROM oceny WHERE przedmiot = ? ORDER BY uczen",
(przedmiot,)
)
wyniki = self.cursor.fetchall()
suma = 0
for row in wyniki:
linia = f"{row[1]:<22} {row[2]:<15} {row[3]}"
self.ui.listWidget.addItem(linia)
suma += row[3]
srednia = suma / len(wyniki) if wyniki else 0
self.ui.lblSrednia.setText(f"Średnia: {srednia:.2f}")
# Najlepszy uczeń (ze wszystkich danych)
self.cursor.execute("""
SELECT uczen, ROUND(AVG(ocena), 2)
FROM oceny GROUP BY uczen ORDER BY AVG(ocena) DESC LIMIT 1
""")
naj = self.cursor.fetchone()
if naj:
self.ui.lblNajlepszy.setText(f"🏆 Najlepszy: {naj[0]} (śr. {naj[1]})")
self.zaladuj()
„Co się stanie w linii ze średnią gdy lista jest pusta?"
Odp: Dzielenie przez zero! Dlatego: if wyniki else 0.
def filtruj(self):
wybrana = self.ui.comboBox.currentText()
self.zaladuj(wybrana)
self.ui.btnFiltruj.clicked.connect(self.filtruj)
Bonus: Dodaj przycisk „Statystyki" wyświetlający średnią per przedmiot w QMessageBox.
#02 — Gotowy kod
Formularz Qt Creator (mainwindow.ui)
| Widżet | objectName | Właściwości |
|---|---|---|
| QLabel | lblTytul | text: „Dziennik ucznia", font: bold 18pt |
| QLabel | lblPrzedmiot | text: „Przedmiot:" |
| QComboBox | comboBox | (elementy z kodu) |
| QPushButton | btnFiltruj | text: „Filtruj" |
| QListWidget | listWidget | font: Consolas 10pt |
| QLabel | lblSrednia | text: „Średnia: —", font: 12pt |
| QLabel | lblNajlepszy | text: „Najlepszy: —", font: bold 12pt |
Konwersja: pyuic6 mainwindow.ui -o mainwindow.py
main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# ===== 1. WCZYTANIE DANYCH =====
dane = []
with open("dane_oceny.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
dane.append({
"id": int(parts[0]),
"uczen": parts[1],
"przedmiot": parts[2],
"ocena": int(parts[3])
})
# ===== 2. BAZA DANYCH =====
self.conn = sqlite3.connect("dziennik.db")
self.cursor = self.conn.cursor()
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS oceny (
id INTEGER PRIMARY KEY,
uczen TEXT NOT NULL,
przedmiot TEXT NOT NULL,
ocena INTEGER
)
""")
self.cursor.execute("DELETE FROM oceny")
for d in dane:
self.cursor.execute(
"INSERT INTO oceny VALUES (?, ?, ?, ?)",
(d["id"], d["uczen"], d["przedmiot"], d["ocena"])
)
self.conn.commit()
self.setup_logic()
def setup_logic(self):
# ===== 3. WIDŻETY =====
self.cursor.execute("SELECT DISTINCT przedmiot FROM oceny ORDER BY przedmiot")
przedmioty = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(przedmioty)
self.ui.comboBox.setCurrentText("wszystkie")
# ===== 4. SYGNAŁY =====
self.ui.btnFiltruj.clicked.connect(self.filtruj)
self.zaladuj()
# ===== 5. METODY =====
def zaladuj(self, przedmiot="wszystkie"):
self.ui.listWidget.clear()
if przedmiot == "wszystkie":
self.cursor.execute("SELECT * FROM oceny ORDER BY uczen, przedmiot")
else:
self.cursor.execute(
"SELECT * FROM oceny WHERE przedmiot = ? ORDER BY uczen",
(przedmiot,)
)
wyniki = self.cursor.fetchall()
suma = 0
for row in wyniki:
linia = f"{row[1]:<22} {row[2]:<15} {row[3]}"
self.ui.listWidget.addItem(linia)
suma += row[3]
srednia = suma / len(wyniki) if wyniki else 0
self.ui.lblSrednia.setText(f"Średnia: {srednia:.2f}")
self.cursor.execute("""
SELECT uczen, ROUND(AVG(ocena), 2)
FROM oceny GROUP BY uczen ORDER BY AVG(ocena) DESC LIMIT 1
""")
naj = self.cursor.fetchone()
if naj:
self.ui.lblNajlepszy.setText(f"🏆 Najlepszy: {naj[0]} (śr. {naj[1]})")
def filtruj(self):
wybrana = self.ui.comboBox.currentText()
self.zaladuj(wybrana)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
dane_oceny.txt
1;Jan Kowalski;matematyka;5
2;Jan Kowalski;fizyka;4
3;Jan Kowalski;informatyka;6
4;Anna Nowak;matematyka;3
5;Anna Nowak;fizyka;5
6;Anna Nowak;informatyka;4
7;Piotr Wiśniewski;matematyka;4
8;Piotr Wiśniewski;fizyka;3
9;Piotr Wiśniewski;informatyka;5
10;Maria Zielińska;matematyka;2
11;Maria Zielińska;fizyka;4
12;Maria Zielińska;informatyka;3
📝 Sprawdzian — Dziennik ucznia
5 pytań · 5 minut · minimum 60% żeby zdać
oceny = [4, 5, 3]?cursor.fetchone() gdy nie ma więcej wierszy?3.14159 do 2 miejsc po przecinku?🔥 Rozgrzewka — Wypożyczalnia książek
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
cursor.execute("UPDATE tabela SET kolumna = ? WHERE id = ?", (nowa_wartosc, id)). Pamiętaj o WHERE — bez niego zmienisz WSZYSTKIE wiersze! Po UPDATE wywołaj conn.commit().item = list_widget.currentItem() zwraca zaznaczony element (lub None). Tekst: item.text(). Indeks: list_widget.currentRow() (zwraca -1 jeśli nic nie zaznaczono).dostepna INTEGER DEFAULT 1. Przy wypożyczeniu: UPDATE ksiazki SET dostepna = 0 WHERE id = ?.SELECT * FROM ksiazki WHERE dostepna = 1. Lub odwrotnie dla wypożyczonych: WHERE dostepna = 0. Można też użyć aliasu: SELECT *, CASE WHEN dostepna=0 THEN 'Tak' ELSE 'Nie' END as wypozyczona.try/except przy operacjach na bazie?try: ... except sqlite3.Error as e: print(f"Błąd: {e}"). Zapobiega crashowi programu.#03 — Instrukcja z arkusza egzaminacyjnego
INF.04-02-24.01-SG · Styczeń 2024 · Czas: 180 min · Część I: Aplikacja desktopowa
Kontekst
Z zastosowaniem języka Python wykonaj aplikację desktopową realizującą funkcję systemu wypożyczalni książek.
Archiwum materiałów: pliki3.zip, hasło: Ksi@zki1
Dane wejściowe — dane_ksiazki.txt
Plik zawiera dane książek oddzielone średnikiem (bez nagłówka):
1;Wiedźmin;Andrzej Sapkowski;fantasy;1993;1
2;Lalka;Bolesław Prus;powieść;1890;1
3;Pan Tadeusz;Adam Mickiewicz;epopeja;1834;0
4;Solaris;Stanisław Lem;sci-fi;1961;1
5;Ferdydurke;Witold Gombrowicz;powieść;1937;1
6;Hobbit;J.R.R. Tolkien;fantasy;1937;0
7;Diune;Frank Herbert;sci-fi;1965;1
8;Quo Vadis;Henryk Sienkiewicz;powieść;1896;1
9;Cyberiada;Stanisław Lem;sci-fi;1965;1
10;Krzyżacy;Henryk Sienkiewicz;powieść;1900;0
Format: id;tytul;autor;gatunek;rok;dostepna (1 = dostępna, 0 = wypożyczona)
Baza danych
- Utwórz bazę SQLite: biblioteka.db
- Tabela ksiazki: id (INTEGER PK), tytul (TEXT), autor (TEXT), gatunek (TEXT), rok (INTEGER), dostepna (INTEGER)
- Wstaw dane z pliku do tabeli
Zapytania SQL
- Wyświetl książki posortowane po tytule
- Filtruj po gatunku
- Pokaż tylko dostępne (dostepna = 1)
- Policz ilość książek per gatunek (COUNT + GROUP BY)
- UPDATE — zmień status wypożyczenia
GUI — PyQt6
- Tytuł okna: „Wypożyczalnia książek"
- QListWidget wyświetlający książki (tytuł — autor — rok — status)
- QComboBox z gatunkami + przycisk „Filtruj"
- QCheckBox „Tylko dostępne"
- Przycisk „Wypożycz / Zwróć" — zmienia status zaznaczonej książki
- QLabel ze statystykami (dostępne / wszystkie)
Na lekcji: Nowością jest UPDATE i QCheckBox. Reszta to znany schemat.
#03 — Wprowadzenie dla nauczyciela
1. Otwarcie
„Trzecie zadanie. Schemat znasz — plik → baza → GUI. Ale teraz mamy nowy element: UPDATE — zmianę danych w bazie."
„W #01 i #02 baza była read-only. Teraz użytkownik klika i zmienia status książki."
2. Diagnoza wiedzy
„Jak zmienisz wartość w bazie SQL?"
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „UPDATE tabela SET..." | „Dokładnie! UPDATE + WHERE żeby nie zmienić wszystkiego." |
| „INSERT?" | „INSERT dodaje nowy wiersz. UPDATE zmienia istniejący." |
| (nie wie) | „UPDATE ksiazki SET dostepna = 0 WHERE id = 3" |
3. Architektura
mainwindow.ui (Qt Creator → Main Window):
QLabel „Wypożyczalnia książek", QLabel „Gatunek:",
QComboBox, QPushButton „Filtruj", QCheckBox „Tylko dostępne",
QListWidget, QLabel statystyki, QPushButton „Wypożycz / Zwróć"
Terminal: pyuic6 mainwindow.ui -o mainwindow.py
main.py (VS Code):
class MainWindow(QtWidgets.QMainWindow):
1. Wczytaj dane_ksiazki.txt
2. Utwórz biblioteka.db + tabela ksiazki
3. INSERT dane
4. from mainwindow import Ui_MainWindow
5. self.ui.comboBox — gatunki z bazy
6. Metoda zaladuj() → self.ui.listWidget
7. self.ui.btnWypozycz → UPDATE dostepna → odśwież
8. self.ui.lblStat: „Dostępne: 7/10"
4. Częste blokady
| Problem | Podpowiedź |
|---|---|
| QListWidget — currentRow() == -1 | „Nic nie zaznaczono. Sprawdź: if list_widget.currentRow() == -1: return" |
| UPDATE bez WHERE | „Zmieni WSZYSTKIE wiersze! Zawsze dodaj WHERE id = ?" |
| Po UPDATE brak zmiany w QListWidget | „Musisz commit() + przeładować zaladuj()." |
| Status 1/0 niezrozumiały | „Mapuj: 'dostępna' if row[5]==1 else 'wypożyczona'" |
#03 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Wczytanie dane_ksiazki.txt | with open(), split(";"), int(rok), int(dostepna) | ✅ |
| Baza biblioteka.db | sqlite3.connect() | ✅ |
| Tabela ksiazki | CREATE TABLE IF NOT EXISTS, 6 kolumn | ✅ |
| INSERT danych | Parametryzowane, z pętli | ✅ |
| SELECT + sortowanie | ORDER BY tytul | ✅ |
| Filtr po gatunku | WHERE gatunek = ? | ✅ |
| Filtr „tylko dostępne" | WHERE dostepna = 1 | ✅ |
| COUNT per gatunek | SELECT gatunek, COUNT(*) GROUP BY | ✅ |
| UPDATE statusu | UPDATE ksiazki SET dostepna = ? WHERE id = ? | ✅ |
| PyQt6 okno | QMainWindow + setWindowTitle(„Wypożyczalnia książek") | ✅ |
| QListWidget | Tytuł, autor, rok, status | ✅ |
| QComboBox gatunek | DISTINCT + „wszystkie" | ✅ |
| QCheckBox dostępne | QCheckBox + isChecked() | ✅ |
| Przycisk Wypożycz/Zwróć | UPDATE + odśwież | ✅ |
| QLabel statystyki | Dostępne X / Y | ✅ |
#03 — Budowa krok po kroku
1. File → New → Qt Designer Form → Main Window → Create
2. Przeciągnij widżety:
•
QLabel — „Wypożyczalnia książek" (objectName: lblTytul)•
QLabel — „Gatunek:" (objectName: lblGatunek)•
QComboBox (objectName: comboBox)•
QPushButton — „Filtruj" (objectName: btnFiltruj)•
QCheckBox — „Tylko dostępne" (objectName: chkDostepne)•
QListWidget (objectName: listWidget)•
QLabel — „Dostępne: 0/0" (objectName: lblStat)•
QPushButton — „Wypożycz / Zwróć" (objectName: btnWypozycz)3. Zaznacz formularz → prawy klik → Lay out in a Grid
4. Zapisz jako
mainwindow.uipyuic6 mainwindow.ui -o mainwindow.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
„Klasa MainWindow dziedziczy po QMainWindow — to standardowy wzorzec PyQt6."
ksiazki = []
with open("dane_ksiazki.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
ksiazki.append({
"id": int(parts[0]),
"tytul": parts[1],
"autor": parts[2],
"gatunek": parts[3],
"rok": int(parts[4]),
"dostepna": int(parts[5])
})
print(f"Wczytano {len(ksiazki)} książek")
„Ile pól ma tutaj split? Ile w #01?"
Odp: 6 pól (id, tytuł, autor, gatunek, rok, dostępna). W #01 było 5.
conn = sqlite3.connect("biblioteka.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS ksiazki (
id INTEGER PRIMARY KEY,
tytul TEXT NOT NULL,
autor TEXT,
gatunek TEXT,
rok INTEGER,
dostepna INTEGER DEFAULT 1
)
""")
cursor.execute("DELETE FROM ksiazki")
for k in ksiazki:
cursor.execute(
"INSERT INTO ksiazki VALUES (?, ?, ?, ?, ?, ?)",
(k["id"], k["tytul"], k["autor"], k["gatunek"], k["rok"], k["dostepna"])
)
conn.commit()
„Co robi DEFAULT 1?"
Odp: Jeśli nie podasz wartości przy INSERT, domyślnie = 1 (dostępna).
„Widżety mamy z Qt Creator — łącznie z QCheckBox. Uzupełniamy comboBox gatunkami z bazy."
# Pobierz gatunki z bazy i dodaj do comboBox
self.cursor.execute("SELECT DISTINCT gatunek FROM ksiazki ORDER BY gatunek")
gatunki = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(gatunki)
self.ui.comboBox.setCurrentText("wszystkie")
„Co robi QCheckBox?"
Odp: To pole wyboru True/False. Sprawdzamy stan metodą self.ui.chkDostepne.isChecked().
def zaladuj(self, gatunek="wszystkie"):
self.ui.listWidget.clear()
self.widoczne_id = []
sql = "SELECT * FROM ksiazki WHERE 1=1"
params = []
if gatunek != "wszystkie":
sql += " AND gatunek = ?"
params.append(gatunek)
if self.ui.chkDostepne.isChecked():
sql += " AND dostepna = 1"
sql += " ORDER BY tytul"
self.cursor.execute(sql, params)
wyniki = self.cursor.fetchall()
for row in wyniki:
status = "✅ dostępna" if row[5] == 1 else "❌ wypożyczona"
linia = f"{row[1]:<22} {row[2]:<22} {row[4]} {status}"
self.ui.listWidget.addItem(linia)
self.widoczne_id.append(row[0])
# Statystyki
self.cursor.execute("SELECT COUNT(*) FROM ksiazki WHERE dostepna = 1")
dost = self.cursor.fetchone()[0]
self.cursor.execute("SELECT COUNT(*) FROM ksiazki")
total = self.cursor.fetchone()[0]
self.ui.lblStat.setText(f"Dostępne: {dost}/{total}")
self.zaladuj()
„self.widoczne_id to lista – mapujemy indeks QListWidget do id w bazie. Bez tego nie wiemy, którą książkę kliknął."
„To najważniejszy nowy element. Klikasz książkę → przycisk zmienia jej status."
def wypozycz_zwroc(self):
row_idx = self.ui.listWidget.currentRow()
if row_idx == -1:
QtWidgets.QMessageBox.warning(self, "Uwaga", "Zaznacz książkę!")
return
book_id = self.widoczne_id[row_idx]
# Sprawdź aktualny status
self.cursor.execute("SELECT dostepna FROM ksiazki WHERE id = ?", (book_id,))
obecny = self.cursor.fetchone()[0]
nowy = 0 if obecny == 1 else 1
self.cursor.execute("UPDATE ksiazki SET dostepna = ? WHERE id = ?", (nowy, book_id))
self.conn.commit()
akcja = "Wypożyczono" if nowy == 0 else "Zwrócono"
QtWidgets.QMessageBox.information(self, "OK", f"{akcja} książkę")
self.zaladuj(self.ui.comboBox.currentText())
self.ui.btnWypozycz.clicked.connect(self.wypozycz_zwroc)
„Dlaczego po UPDATE wywołujemy zaladuj()?"
Odp: Żeby QListWidget pokazał nowy status. Baza się zmieniła, ale ekran sam się nie odświeży.
def filtruj(self):
self.zaladuj(self.ui.comboBox.currentText())
self.ui.btnFiltruj.clicked.connect(self.filtruj)
# QCheckBox też filtruje przy kliknięciu
self.ui.chkDostepne.stateChanged.connect(self.filtruj)
Bonus: Dodaj kolorowanie — zielone tło dla dostępnych, czerwone dla wypożyczonych:item = self.ui.listWidget.item(i); item.setBackground(QtGui.QColor("#e8f5e9") if row[5]==1 else QtGui.QColor("#ffebee"))
#03 — Gotowy kod
Formularz Qt Creator (mainwindow.ui)
| Widżet | objectName | Właściwości |
|---|---|---|
| QLabel | lblTytul | text: „Wypożyczalnia książek", font: bold 18pt |
| QLabel | lblGatunek | text: „Gatunek:" |
| QComboBox | comboBox | (elementy z kodu) |
| QPushButton | btnFiltruj | text: „Filtruj" |
| QCheckBox | chkDostepne | text: „Tylko dostępne" |
| QListWidget | listWidget | font: Consolas 10pt |
| QLabel | lblStat | text: „Dostępne: 0/0", font: 12pt |
| QPushButton | btnWypozycz | text: „Wypożycz / Zwróć" |
Konwersja: pyuic6 mainwindow.ui -o mainwindow.py
main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# ===== 1. WCZYTANIE DANYCH =====
ksiazki = []
with open("dane_ksiazki.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
ksiazki.append({
"id": int(parts[0]),
"tytul": parts[1],
"autor": parts[2],
"gatunek": parts[3],
"rok": int(parts[4]),
"dostepna": int(parts[5])
})
# ===== 2. BAZA DANYCH =====
self.conn = sqlite3.connect("biblioteka.db")
self.cursor = self.conn.cursor()
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS ksiazki (
id INTEGER PRIMARY KEY,
tytul TEXT NOT NULL,
autor TEXT,
gatunek TEXT,
rok INTEGER,
dostepna INTEGER DEFAULT 1
)
""")
self.cursor.execute("DELETE FROM ksiazki")
for k in ksiazki:
self.cursor.execute(
"INSERT INTO ksiazki VALUES (?, ?, ?, ?, ?, ?)",
(k["id"], k["tytul"], k["autor"], k["gatunek"], k["rok"], k["dostepna"])
)
self.conn.commit()
self.setup_logic()
def setup_logic(self):
# ===== 3. WIDŻETY =====
self.cursor.execute("SELECT DISTINCT gatunek FROM ksiazki ORDER BY gatunek")
gatunki = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(gatunki)
self.ui.comboBox.setCurrentText("wszystkie")
# ===== 4. SYGNAŁY =====
self.ui.btnFiltruj.clicked.connect(self.filtruj)
self.ui.chkDostepne.stateChanged.connect(self.filtruj)
self.ui.btnWypozycz.clicked.connect(self.wypozycz_zwroc)
self.zaladuj()
# ===== 5. METODY =====
def zaladuj(self, gatunek="wszystkie"):
self.ui.listWidget.clear()
self.widoczne_id = []
sql = "SELECT * FROM ksiazki WHERE 1=1"
params = []
if gatunek != "wszystkie":
sql += " AND gatunek = ?"
params.append(gatunek)
if self.ui.chkDostepne.isChecked():
sql += " AND dostepna = 1"
sql += " ORDER BY tytul"
self.cursor.execute(sql, params)
wyniki = self.cursor.fetchall()
for row in wyniki:
status = "✅ dostępna" if row[5] == 1 else "❌ wypożyczona"
linia = f"{row[1]:<22} {row[2]:<22} {row[4]} {status}"
self.ui.listWidget.addItem(linia)
self.widoczne_id.append(row[0])
self.cursor.execute("SELECT COUNT(*) FROM ksiazki WHERE dostepna = 1")
dost = self.cursor.fetchone()[0]
self.cursor.execute("SELECT COUNT(*) FROM ksiazki")
total = self.cursor.fetchone()[0]
self.ui.lblStat.setText(f"Dostępne: {dost}/{total}")
def wypozycz_zwroc(self):
row_idx = self.ui.listWidget.currentRow()
if row_idx == -1:
QtWidgets.QMessageBox.warning(self, "Uwaga", "Zaznacz książkę!")
return
book_id = self.widoczne_id[row_idx]
self.cursor.execute("SELECT dostepna FROM ksiazki WHERE id = ?", (book_id,))
obecny = self.cursor.fetchone()[0]
nowy = 0 if obecny == 1 else 1
self.cursor.execute("UPDATE ksiazki SET dostepna = ? WHERE id = ?", (nowy, book_id))
self.conn.commit()
akcja = "Wypożyczono" if nowy == 0 else "Zwrócono"
QtWidgets.QMessageBox.information(self, "OK", f"{akcja} książkę")
self.zaladuj(self.ui.comboBox.currentText())
def filtruj(self):
self.zaladuj(self.ui.comboBox.currentText())
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
dane_ksiazki.txt
1;Wiedźmin;Andrzej Sapkowski;fantasy;1993;1
2;Lalka;Bolesław Prus;powieść;1890;1
3;Pan Tadeusz;Adam Mickiewicz;epopeja;1834;0
4;Solaris;Stanisław Lem;sci-fi;1961;1
5;Ferdydurke;Witold Gombrowicz;powieść;1937;1
6;Hobbit;J.R.R. Tolkien;fantasy;1937;0
7;Diune;Frank Herbert;sci-fi;1965;1
8;Quo Vadis;Henryk Sienkiewicz;powieść;1896;1
9;Cyberiada;Stanisław Lem;sci-fi;1965;1
10;Krzyżacy;Henryk Sienkiewicz;powieść;1900;0
📝 Sprawdzian — Wypożyczalnia książek
5 pytań · 5 minut · minimum 60% żeby zdać
try/except przy operacjach bazodanowych?🔥 Rozgrzewka — Stacja pogodowa
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
SELECT AVG(temperatura), MIN(temperatura), MAX(temperatura) FROM pomiary. Funkcje agregujące: AVG() — średnia, MIN() — najmniejsza, MAX() — największa, SUM() — suma, COUNT() — liczba wierszy.SELECT miasto, AVG(temperatura) FROM pomiary GROUP BY miasto. GROUP BY tworzy grupy po unikalnych wartościach kolumny, a funkcja agregująca działa osobno dla każdej grupy.YYYY-MM-DD. Porównania tekstowe działają poprawnie: WHERE data > '2025-01-10'. W Pythonie: datetime.strptime(tekst, "%Y-%m-%d").SELECT * FROM pomiary WHERE data BETWEEN '2025-01-10' AND '2025-01-15'. Lub: WHERE data >= '2025-01-10' AND data <= '2025-01-15'. Format daty musi być YYYY-MM-DD.HAVING w SQL?HAVING filtruje wyniki PO grupowaniu (w przeciwieństwie do WHERE, które filtruje PRZED). Przykład: SELECT miasto, AVG(temp) FROM pomiary GROUP BY miasto HAVING AVG(temp) > 5 — tylko miasta ze średnią > 5°C.#04 — Instrukcja z arkusza egzaminacyjnego
INF.04-03-23.06-SG · Czerwiec 2023 · Czas: 180 min · Część I: Aplikacja desktopowa
Kontekst
Z zastosowaniem języka Python wykonaj aplikację desktopową realizującą funkcję stacji pogodowej — przeglądanie i analizę danych pomiarowych.
Archiwum materiałów: pliki4.zip, hasło: P0g0da#
Dane wejściowe — dane_pogoda.txt
Plik zawiera pomiary pogodowe oddzielone średnikiem (bez nagłówka):
1;2025-01-10;Kraków;-5.2;78;1013
2;2025-01-10;Warszawa;-3.1;82;1015
3;2025-01-10;Gdańsk;-1.5;88;1010
4;2025-01-11;Kraków;-2.8;72;1018
5;2025-01-11;Warszawa;-0.5;76;1016
6;2025-01-11;Gdańsk;1.2;85;1012
7;2025-01-12;Kraków;0.3;65;1020
8;2025-01-12;Warszawa;2.1;70;1019
9;2025-01-12;Gdańsk;3.5;80;1014
10;2025-01-13;Kraków;3.7;60;1022
11;2025-01-13;Warszawa;5.0;68;1021
12;2025-01-13;Gdańsk;4.8;75;1017
Format: id;data;miasto;temperatura;wilgotnosc;cisnienie
Baza danych
- Utwórz bazę SQLite: pogoda.db
- Tabela pomiary: id (INTEGER PK), data (TEXT), miasto (TEXT), temperatura (REAL), wilgotnosc (INTEGER), cisnienie (INTEGER)
- Wstaw dane z pliku do tabeli
Zapytania SQL
- Wyświetl pomiary posortowane po dacie i mieście
- Filtruj po mieście
- Oblicz MIN, MAX, AVG temperatury per miasto
- Znajdź dzień z najniższą temperaturą
- Oblicz średnią wilgotność per miasto
GUI — PyQt6
- Tytuł okna: „Stacja pogodowa"
- QListWidget wyświetlający pomiary (data — miasto — temp — wilgotność — ciśnienie)
- QComboBox z miastami + przycisk „Filtruj"
- QLabel z min/max/avg temperatury
- QLabel z dniem najzimniejszym
- Przycisk „Statystyki" → QMessageBox ze średnimi per miasto
Na lekcji: Więcej typów danych (float, daty) i agregacji (MIN/MAX/AVG). Schemat ten sam, ale dane bardziej realistyczne.
#04 — Wprowadzenie dla nauczyciela
1. Otwarcie
„Czwarte zadanie — stacja pogodowa. Temat realny: temperatury, wilgotność, ciśnienie. Dane z kilku miast i kilku dni."
„Schemat identyczny: plik → baza → zapytania → GUI. Ale masz teraz REAL (float) i daty jako TEXT."
2. Diagnoza wiedzy
„Jak w SQL znajdziesz najniższą temperaturę?"
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „MIN(temperatura)" | „Idealnie! A ORDER BY + LIMIT 1 da ci cały wiersz." |
| „sort i wziąć pierwszy" | „Można. Ale SQL ma gotowy MIN(). Czyściej i szybciej." |
| (nie wie) | „SQL: MIN() = minimum, MAX() = maksimum, AVG() = średnia." |
3. Architektura
mainwindow.ui (Qt Creator → Main Window):
QLabel „Stacja pogodowa", QLabel „Miasto:",
QComboBox, QPushButton „Filtruj", QPushButton „Statystyki",
QListWidget, QLabel temperatury, QLabel najzimniejszy
Terminal: pyuic6 mainwindow.ui -o mainwindow.py
main.py (VS Code):
class MainWindow(QtWidgets.QMainWindow):
1. Wczytaj dane_pogoda.txt
2. Utwórz pogoda.db + tabela pomiary
3. INSERT dane
4. from mainwindow import Ui_MainWindow
5. self.ui.comboBox — miasta z bazy
6. Metoda zaladuj() → self.ui.listWidget + self.ui.lblTemp
7. self.ui.btnStatystyki → QMessageBox ze średnimi
8. self.ui.lblZimno: najzimniejszy dzień
4. Częste blokady
| Problem | Podpowiedź |
|---|---|
| Temperatura ujemna się nie parsuje | „float('-5.2') działa! Python radzi sobie z minusem." |
| Data jako tekst, nie datetime | „W SQLite daty to TEXT. Sortuj normalnie — format YYYY-MM-DD sortuje się poprawnie." |
| MIN() vs ORDER BY LIMIT 1 | „MIN() daje samą wartość. Jeśli chcesz cały wiersz → ORDER BY temp ASC LIMIT 1." |
| Messagebox za długi tekst | „Buduj stringa z pętli: tekst += f'{row[0]}: {row[1]:.1f}°C\n'" |
#04 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Wczytanie dane_pogoda.txt | with open(), split(";"), float(temp), int(wilg) | ✅ |
| Baza pogoda.db | sqlite3.connect() | ✅ |
| Tabela pomiary | CREATE TABLE IF NOT EXISTS, 6 kolumn | ✅ |
| INSERT danych | Parametryzowane, z pętli | ✅ |
| SELECT + sortowanie | ORDER BY data, miasto | ✅ |
| Filtr po mieście | WHERE miasto = ? | ✅ |
| MIN/MAX/AVG temp | Agregacja per wyświetlone dane | ✅ |
| Najzimniejszy dzień | ORDER BY temperatura ASC LIMIT 1 | ✅ |
| Średnia wilgotność | AVG(wilgotnosc) GROUP BY miasto | ✅ |
| PyQt6 okno | QMainWindow + setWindowTitle(„Stacja pogodowa") | ✅ |
| QListWidget pomiarów | Data, miasto, temp, wilg, ciśn | ✅ |
| QComboBox miast | DISTINCT + „wszystkie" | ✅ |
| QLabel min/max/avg | Temperatury | ✅ |
| QLabel najzimniejszy | Data + miasto + temperatura | ✅ |
| Przycisk Statystyki | QMessageBox ze średnimi per miasto | ✅ |
#04 — Budowa krok po kroku
1. File → New → Qt Designer Form → Main Window → Create
2. Przeciągnij widżety:
•
QLabel — „🌤️ Stacja pogodowa" (objectName: lblTytul)•
QLabel — „Miasto:" (objectName: lblMiasto)•
QComboBox (objectName: comboBox)•
QPushButton — „Filtruj" (objectName: btnFiltruj)•
QPushButton — „📊 Statystyki" (objectName: btnStatystyki)•
QListWidget (objectName: listWidget)•
QLabel — „Min: — | Max: — | Avg: —" (objectName: lblTemp)•
QLabel — „Najzimniej: —" (objectName: lblZimno)3. Zaznacz formularz → prawy klik → Lay out in a Grid
4. Zapisz jako
mainwindow.uipyuic6 mainwindow.ui -o mainwindow.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
„Znasz ten schemat. Formularz z Qt Creator, pyuic6, main.py. Jazda."
pomiary = []
with open("dane_pogoda.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
pomiary.append({
"id": int(parts[0]),
"data": parts[1],
"miasto": parts[2],
"temperatura": float(parts[3]),
"wilgotnosc": int(parts[4]),
"cisnienie": int(parts[5])
})
print(f"Wczytano {len(pomiary)} pomiarów")
„Dlaczego temperatura to float a wilgotność to int?"
Odp: Temperatura ma część dziesiętną (-5.2). Wilgotność to procent — całkowita.
conn = sqlite3.connect("pogoda.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS pomiary (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
miasto TEXT NOT NULL,
temperatura REAL,
wilgotnosc INTEGER,
cisnienie INTEGER
)
""")
cursor.execute("DELETE FROM pomiary")
for p in pomiary:
cursor.execute(
"INSERT INTO pomiary VALUES (?, ?, ?, ?, ?, ?)",
(p["id"], p["data"], p["miasto"], p["temperatura"],
p["wilgotnosc"], p["cisnienie"])
)
conn.commit()
print("Dane w bazie")
„Dlaczego data to TEXT a nie DATE?"
Odp: SQLite nie ma typu DATE. Używa TEXT. Format YYYY-MM-DD sortuje się poprawnie alfanumerycznie.
# MIN / MAX / AVG temperatury
cursor.execute("""
SELECT MIN(temperatura), MAX(temperatura), ROUND(AVG(temperatura), 1)
FROM pomiary
""")
row = cursor.fetchone()
print(f"Min: {row[0]}°C, Max: {row[1]}°C, Avg: {row[2]}°C")
# Najzimniejszy dzień
cursor.execute("""
SELECT data, miasto, temperatura FROM pomiary
ORDER BY temperatura ASC LIMIT 1
""")
naj = cursor.fetchone()
print(f"Najzimniej: {naj[0]} {naj[1]} → {naj[2]}°C")
# Średnia per miasto
cursor.execute("""
SELECT miasto, ROUND(AVG(temperatura), 1), ROUND(AVG(wilgotnosc), 0)
FROM pomiary GROUP BY miasto
""")
for row in cursor.fetchall():
print(f"{row[0]}: temp {row[1]}°C, wilg {row[2]}%")
„Czym się różni ROUND(AVG(...),1) od ROUND(AVG(...),2)?"
Odp: Liczba miejsc po przecinku. 1 → jedno (np. 3.5), 2 → dwa (3.50).
„Widżety mamy z Qt Creator. Uzupełniamy comboBox miastami z bazy."
self.cursor.execute("SELECT DISTINCT miasto FROM pomiary ORDER BY miasto")
miasta = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(miasta)
self.ui.comboBox.setCurrentText("wszystkie")
Dostęp do widżetów: self.ui.comboBox, self.ui.listWidget, self.ui.lblTemp, self.ui.lblZimno — nazwy z objectName w Qt Creator.
def zaladuj(self, miasto="wszystkie"):
self.ui.listWidget.clear()
if miasto == "wszystkie":
self.cursor.execute("SELECT * FROM pomiary ORDER BY data, miasto")
else:
self.cursor.execute(
"SELECT * FROM pomiary WHERE miasto = ? ORDER BY data",
(miasto,)
)
wyniki = self.cursor.fetchall()
for row in wyniki:
linia = f"{row[1]} {row[2]:<12} {row[3]:>6.1f}°C 💧{row[4]}% {row[5]} hPa"
self.ui.listWidget.addItem(linia)
# Statystyki temperatur (z wyświetlonych danych)
if wyniki:
temps = [r[3] for r in wyniki]
mn, mx, avg = min(temps), max(temps), sum(temps)/len(temps)
self.ui.lblTemp.setText(f"🌡️ Min: {mn:.1f}°C | Max: {mx:.1f}°C | Avg: {avg:.1f}°C")
else:
self.ui.lblTemp.setText("Brak danych")
# Najzimniejszy (ze wszystkich)
self.cursor.execute("""
SELECT data, miasto, temperatura FROM pomiary
ORDER BY temperatura ASC LIMIT 1
""")
naj = self.cursor.fetchone()
if naj:
self.ui.lblZimno.setText(f"❄️ Najzimniej: {naj[0]} {naj[1]} → {naj[2]}°C")
self.zaladuj()
„Statystyki liczymy w Pythonie z wyfiltrowanych danych. Najzimniejszy dzień — zawsze ze wszystkich."
def filtruj(self):
self.zaladuj(self.ui.comboBox.currentText())
self.ui.btnFiltruj.clicked.connect(self.filtruj)
„Przetestuj — wybierz Kraków i kliknij Filtruj. Labele się aktualizują?"
„Ostatni krok — przycisk otwierający podsumowanie per miasto."
def pokaz_statystyki(self):
self.cursor.execute("""
SELECT miasto,
ROUND(AVG(temperatura), 1),
ROUND(MIN(temperatura), 1),
ROUND(MAX(temperatura), 1),
ROUND(AVG(wilgotnosc), 0)
FROM pomiary GROUP BY miasto ORDER BY miasto
""")
tekst = ""
for row in self.cursor.fetchall():
tekst += f"📍 {row[0]}\n"
tekst += f" Temp: avg {row[1]}°C (min {row[2]}, max {row[3]})\n"
tekst += f" Wilgotność: {int(row[4])}%\n\n"
QtWidgets.QMessageBox.information(self, "Statystyki per miasto", tekst)
self.ui.btnStatystyki.clicked.connect(self.pokaz_statystyki)
Bonus: Dodaj kolorowanie w QListWidget: temperatury ujemne → niebieskie tło, powyżej 0 → zielone.
#04 — Gotowy kod
Formularz Qt Creator (mainwindow.ui)
| Widżet | objectName | Właściwości |
|---|---|---|
| QLabel | lblTytul | text: „🌤️ Stacja pogodowa", font: bold 18pt |
| QLabel | lblMiasto | text: „Miasto:" |
| QComboBox | comboBox | (elementy z kodu) |
| QPushButton | btnFiltruj | text: „Filtruj" |
| QPushButton | btnStatystyki | text: „📊 Statystyki" |
| QListWidget | listWidget | font: Consolas 10pt |
| QLabel | lblTemp | text: „Min: — | Max: — | Avg: —", font: 11pt |
| QLabel | lblZimno | text: „Najzimniej: —", font: bold 11pt |
Konwersja: pyuic6 mainwindow.ui -o mainwindow.py
main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# ===== 1. WCZYTANIE DANYCH =====
pomiary = []
with open("dane_pogoda.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
pomiary.append({
"id": int(parts[0]),
"data": parts[1],
"miasto": parts[2],
"temperatura": float(parts[3]),
"wilgotnosc": int(parts[4]),
"cisnienie": int(parts[5])
})
# ===== 2. BAZA DANYCH =====
self.conn = sqlite3.connect("pogoda.db")
self.cursor = self.conn.cursor()
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS pomiary (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
miasto TEXT NOT NULL,
temperatura REAL,
wilgotnosc INTEGER,
cisnienie INTEGER
)
""")
self.cursor.execute("DELETE FROM pomiary")
for p in pomiary:
self.cursor.execute(
"INSERT INTO pomiary VALUES (?, ?, ?, ?, ?, ?)",
(p["id"], p["data"], p["miasto"], p["temperatura"],
p["wilgotnosc"], p["cisnienie"])
)
self.conn.commit()
self.setup_logic()
def setup_logic(self):
# ===== 3. WIDŻETY =====
self.cursor.execute("SELECT DISTINCT miasto FROM pomiary ORDER BY miasto")
miasta = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(miasta)
self.ui.comboBox.setCurrentText("wszystkie")
# ===== 4. SYGNAŁY =====
self.ui.btnFiltruj.clicked.connect(self.filtruj)
self.ui.btnStatystyki.clicked.connect(self.pokaz_statystyki)
self.zaladuj()
# ===== 5. METODY =====
def zaladuj(self, miasto="wszystkie"):
self.ui.listWidget.clear()
if miasto == "wszystkie":
self.cursor.execute("SELECT * FROM pomiary ORDER BY data, miasto")
else:
self.cursor.execute(
"SELECT * FROM pomiary WHERE miasto = ? ORDER BY data",
(miasto,)
)
wyniki = self.cursor.fetchall()
for row in wyniki:
linia = f"{row[1]} {row[2]:<12} {row[3]:>6.1f}°C 💧{row[4]}% {row[5]} hPa"
self.ui.listWidget.addItem(linia)
if wyniki:
temps = [r[3] for r in wyniki]
mn, mx, avg = min(temps), max(temps), sum(temps)/len(temps)
self.ui.lblTemp.setText(f"🌡️ Min: {mn:.1f}°C | Max: {mx:.1f}°C | Avg: {avg:.1f}°C")
else:
self.ui.lblTemp.setText("Brak danych")
self.cursor.execute("""
SELECT data, miasto, temperatura FROM pomiary
ORDER BY temperatura ASC LIMIT 1
""")
naj = self.cursor.fetchone()
if naj:
self.ui.lblZimno.setText(f"❄️ Najzimniej: {naj[0]} {naj[1]} → {naj[2]}°C")
def filtruj(self):
self.zaladuj(self.ui.comboBox.currentText())
def pokaz_statystyki(self):
self.cursor.execute("""
SELECT miasto,
ROUND(AVG(temperatura), 1),
ROUND(MIN(temperatura), 1),
ROUND(MAX(temperatura), 1),
ROUND(AVG(wilgotnosc), 0)
FROM pomiary GROUP BY miasto ORDER BY miasto
""")
tekst = ""
for row in self.cursor.fetchall():
tekst += f"📍 {row[0]}\n"
tekst += f" Temp: avg {row[1]}°C (min {row[2]}, max {row[3]})\n"
tekst += f" Wilgotność: {int(row[4])}%\n\n"
QtWidgets.QMessageBox.information(self, "Statystyki per miasto", tekst)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
dane_pogoda.txt
1;2025-01-10;Kraków;-5.2;78;1013
2;2025-01-10;Warszawa;-3.1;82;1015
3;2025-01-10;Gdańsk;-1.5;88;1010
4;2025-01-11;Kraków;-2.8;72;1018
5;2025-01-11;Warszawa;-0.5;76;1016
6;2025-01-11;Gdańsk;1.2;85;1012
7;2025-01-12;Kraków;0.3;65;1020
8;2025-01-12;Warszawa;2.1;70;1019
9;2025-01-12;Gdańsk;3.5;80;1014
10;2025-01-13;Kraków;3.7;60;1022
11;2025-01-13;Warszawa;5.0;68;1021
12;2025-01-13;Gdańsk;4.8;75;1017
📝 Sprawdzian — Stacja pogodowa
5 pytań · 5 minut · minimum 60% żeby zdać
WHERE od HAVING?🔥 Rozgrzewka — Logowanie z nawigacją menu
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
QStackedWidget w PyQt6?QStackedWidget to kontener stron — wyświetla jedną stronę naraz. Przełączamy strony: stacked.setCurrentIndex(n). Idealny do nawigacji przez menu — użytkownik wybiera opcję z rozwijanego menu, a QStackedWidget pokazuje odpowiedni widok.QLineEdit?.text(): wartosc = entry.text(). Zwraca string. Aby wyczyścić pole: entry.clear(). Aby wstawić tekst: entry.setText("tekst").with open("plik.txt", "r", encoding="utf-8") as f:. Następnie iterujemy: for linia in f:. Każda linia ma na końcu \n, więc warto użyć linia.strip() żeby usunąć białe znaki.==: if wpisane_imie == "Jan":. Uwaga: porównanie jest case-sensitive — "jan" != "Jan". Aby ignorować wielkość liter: wpisane_imie.strip().lower() == "jan".QStackedWidget przez rozwijane menu?QMenu i podpinamy akcje: menu.addAction("Nazwa"). Każda akcja przełącza stronę: akcja.triggered.connect(lambda: stacked.setCurrentIndex(n)). Menu podpinamy do przycisku: btn.setMenu(menu).#05 — Instrukcja — Logowanie z nawigacją menu
Ćwiczenie szkolne · Czas: ~45 min · Aplikacja desktopowa w Pythonie
Kontekst
Z zastosowaniem języka Python i biblioteki PyQt6 wykonaj aplikację desktopową realizującą prosty system logowania z nawigacją menu.
Aplikacja zawiera 3 widoki przełączane rozwijanym menu (navbar) opartym o QPushButton + QMenu + QStackedWidget.
Dane wejściowe — dane.txt
Plik tekstowy zawiera dane jednego użytkownika (imię, nazwisko i klasę), każda wartość w osobnej linii:
Jan
Kowalski
3TIP
Format pliku: linia 1 = imię, linia 2 = nazwisko, linia 3 = klasa.
Widok 1 — „Zaloguj"
- Wyświetla napis powitalny, np. „Witaj! Kliknij ☰ Menu → Logowanie."
- Może zawierać krótką instrukcję obsługi
Widok 2 — „Logowanie"
- Dwa pola tekstowe (
QLineEdit): Imię i Nazwisko - Przycisk „Zaloguj"
- Po kliknięciu program sprawdza, czy wpisane imię i nazwisko zgadzają się z danymi w pliku
dane.txt - Jeśli dane są poprawne — przejście do widoku Profil i wyświetlenie danych
- Jeśli dane są niepoprawne — komunikat o błędzie (np.
QMessageBox.critical)
Widok 3 — „Profil"
- Wyświetla imię, nazwisko i klasę zalogowanego użytkownika
- Opcja „Profil" w menu powinna być niedostępna (szara) dopóki użytkownik się nie zaloguje
Wymagania techniczne
- Użycie
QStackedWidget+QMenudo nawigacji między widokami - Odczyt danych z pliku
dane.txtza pomocąwith open() - Porównanie stringów (imię + nazwisko)
- Obsługa błędnego logowania —
QMessageBox - Znaczące nazwy zmiennych
Na lekcji: Przeczytajcie instrukcję razem. Niech uczeń zidentyfikuje 3 główne etapy: wczytanie danych z pliku → budowa GUI z navbar i widokami → logika logowania.
#05 — Wprowadzenie dla nauczyciela
1. Otwarcie
„Dzisiejsze zadanie jest trochę inne niż poprzednie — tym razem nie pracujemy z bazą danych. Skupiamy się na nawigacji przez rozwijane menu (navbar), przełączaniu widoków i sprawdzaniu danych z pliku."
„Na egzaminie często pada zadanie z nawigacją i przełączaniem widoków — to bardzo powszechny element GUI. Nauczysz się go w ~30 minut."
2. Diagnoza wiedzy
„Czy korzystałeś kiedyś z rozwijanego menu w programie? Takie ☰ na górze — klikasz i rozwijają się opcje?"
Cel: Sprawdzić czy uczeń rozumie koncept nawigacji — jedno okno, kilka widoków przełączanych przez menu.
„Jak odczytać plik tekstowy w Pythonie? Pamiętasz with open()?"
Cel: Upewnić się, że uczeń pamięta open() z wcześniejszych ćwiczeń. Jeśli nie — szybkie przypomnienie.
3. Architektura
„Program ma 3 etapy: (1) wczytaj dane z pliku, (2) zbuduj okno z navbar i 3 widokami, (3) napisz funkcję logowania."
mainwindow.ui → pyuic6 mainwindow.ui -o mainwindow.py → from mainwindow import Ui_MainWindowzaloguj.ui → pyuic6 zaloguj.ui -o zaloguj.py → from zaloguj import Ui_Zalogujformularz.ui → pyuic6 formularz.ui -o formularz.py → from formularz import Ui_Formularzwyswietl.ui → pyuic6 wyswietl.ui -o wyswietl.py → from wyswietl import Ui_Wyswietldane.txt → open() → zmienne (imie, nazwisko, klasa)self.ui.btnMenu + QMenu — rozwijane menu nawigacji (navbar)QStackedWidget w kodzie → 3 widoki (Zaloguj, Logowanie, Profil)self.ui_formularz.btnZaloguj → porównanie → self.stacked.setCurrentIndex(2) lub QtWidgets.QMessageBox.critical()
4. Zasady prowadzenia
Kluczowa zasada: Uczeń pisze kod SAM. Ty pytasz, naprowadzasz, ale NIE dyktuj. Przy błędzie — pytaj: „Co ta linia robi? Czego tu brakuje?"
Tempo: To zadanie jest prostsze niż poprzednie (brak SQL/bazy). Jeśli uczeń jest zaawansowany, ukończy w 20 min. Jeśli początkujący — 45 min z wyjaśnieniami.
5. Częste blokady
| Problem | Podpowiedź |
|---|---|
| ImportError — brak PyQt6 | „Zainstaluj: pip install PyQt6. Zaimportuj potrzebne klasy." |
| QStackedWidget nie przełącza stron | „Czy podłączyłeś akcje menu do ui.stackedWidget.setCurrentIndex(n)?" |
| QLineEdit.text() zwraca pusty string | „Czy pole jest puste? Wpisz coś i kliknij przycisk." |
| Dane z pliku mają \n | „Użyj .strip() na każdej linii — usuwa \n." |
| Porównanie nie działa | „Sprawdź wielkość liter. Użyj .strip() po obu stronach." |
| Profil dostępny przed logowaniem | „Użyj akcja_profil.setEnabled(False) na starcie — zablokuje opcję w menu." |
6. Po zbudowaniu
„Co by się stało, gdyby plik dane.txt nie istniał?"
Odp: Program rzuci FileNotFoundError. Można to obsłużyć try/except.
„Czy przechowywanie hasła/danych w pliku .txt jest bezpieczne?"
Odp: Nie — to tylko ćwiczenie. W prawdziwej aplikacji dane trzyma się w bazie z hashowaniem haseł.
#05 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Plik dane.txt | 3 linie: imię, nazwisko, klasa | ✅ |
| Wczytanie pliku | with open(), .strip() na liniach | ✅ |
| Okno PyQt6 | QMainWindow (class MainWindow), tytuł, rozmiar | ✅ |
| QPushButton + QMenu (navbar) | Rozwijane menu z 3 opcjami nawigacji | ✅ |
| QStackedWidget | Tworzony w kodzie, 3 widoki z osobnych .ui | ✅ |
| Widok 1 — napis | Label z tekstem powitalnym | ✅ |
| Widok 2 — QLineEdit × 2 | Pola: Imię, Nazwisko | ✅ |
| Widok 2 — QPushButton | Przycisk „Zaloguj" z clicked.connect() | ✅ |
| Logika logowania | Porównanie .text() z danymi z pliku | ✅ |
| Błędne logowanie | QMessageBox.critical() | ✅ |
| Widok 3 — profil | QLabel z imieniem, nazwiskiem, klasą | ✅ |
| Profil zablokowany w menu | akcja_profil.setEnabled(False) do momentu logowania | ✅ |
| Znaczące nazwy | entryImie, entryNazwisko, zaloguj, itp. | ✅ |
#05 — Budowa krok po kroku
„Najpierw tworzymy plik z danymi. Utwórz nowy plik dane.txt obok main.py, z trzema liniami: imię, nazwisko i klasa."
Zawartość dane.txt:
Jan
Kowalski
3TIP
„Teraz otwórz Qt Creator → File → New → Qt Designer Form → Widget. To będzie nasz formularz."
1. mainwindow.ui — File → New → Qt Designer Form → Main Window → Create
Przeciągnij na formularz:
QPushButton (objectName: btnMenu, tekst: „☰ Menu")Zaznacz centralWidget → prawy klik → Lay out Vertically. Zapisz jako
mainwindow.ui2. zaloguj.ui — File → New → Qt Designer Form → Widget → Create
Przeciągnij:
QLabel (objectName: lblPowitanie)Lay out Vertically. Zapisz jako
zaloguj.ui3. formularz.ui — File → New → Qt Designer Form → Widget → Create
Przeciągnij:
•
QLabel (lblImie) — tekst: „Imię:"•
QLineEdit (entryImie)•
QLabel (lblNazwisko) — tekst: „Nazwisko:"•
QLineEdit (entryNazwisko)•
QPushButton (btnZaloguj) — tekst: „Zaloguj"Lay out Vertically. Zapisz jako
formularz.ui4. wyswietl.ui — File → New → Qt Designer Form → Widget → Create
Przeciągnij:
QLabel (objectName: lblProfil)Lay out Vertically. Zapisz jako
wyswietl.ui
„Teraz konwertujemy każdy formularz na kod Pythona. W terminalu wpisz 4 komendy:"
pyuic6 mainwindow.ui -o mainwindow.py
pyuic6 zaloguj.ui -o zaloguj.py
pyuic6 formularz.ui -o formularz.py
pyuic6 wyswietl.ui -o wyswietl.py
„Utwórz plik main.py w VS Code ze szkieletem."
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
from zaloguj import Ui_Zaloguj
from formularz import Ui_Formularz
from wyswietl import Ui_Wyswietl
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.setWindowTitle("System logowania")
self.resize(400, 300)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
„Uruchom — widzisz okno z przyciskiem ☰ Menu na górze? Super, szkielet działa."
„Teraz wczytujemy dane z pliku dane.txt. Pamiętaj — każda linia to jedna wartość: imię, nazwisko, klasa."
# ===== 1. WCZYTANIE DANYCH Z PLIKU =====
with open("dane.txt", "r", encoding="utf-8") as plik:
linie = plik.readlines()
prawidlowe_imie = linie[0].strip()
prawidlowe_nazwisko = linie[1].strip()
klasa = linie[2].strip()
print(f"Dane z pliku: {prawidlowe_imie} {prawidlowe_nazwisko}, klasa: {klasa}")
„Dlaczego używamy .strip() na każdej linii?"
Odp: Każda linia z pliku kończy się znakiem nowej linii \n. strip() go usuwa. Bez tego porównanie „Jan\n" == „Jan" zwróci False.
Jeśli FileNotFoundError:
„Czy plik dane.txt jest w tym samym folderze co main.py? Sprawdź ścieżkę."
„Co robi readlines()?"
Odp: Zwraca listę wszystkich linii z pliku. linie[0] to pierwsza linia (imię), linie[1] to druga (nazwisko), linie[2] to trzecia (klasa).
„Przycisk menu i QStackedWidget zostały już zaprojektowane w Qt Creator. Zobaczmy jakie widżety mamy dostępne przez obiekt ui."
| Plik .ui | objectName | Typ | Opis |
|---|---|---|---|
| mainwindow.ui | btnMenu | QPushButton | Przycisk „☰ Menu" z QMenu |
| zaloguj.ui | lblPowitanie | QLabel | Tekst powitalny |
| formularz.ui | lblImie | QLabel | Etykieta „Imię:" |
| formularz.ui | entryImie | QLineEdit | Pole na imię |
| formularz.ui | lblNazwisko | QLabel | Etykieta „Nazwisko:" |
| formularz.ui | entryNazwisko | QLineEdit | Pole na nazwisko |
| formularz.ui | btnZaloguj | QPushButton | Przycisk „Zaloguj" |
| wyswietl.ui | lblProfil | QLabel | Dane profilu |
„Jak to działa? QPushButton na górze to nasz navbar — po kliknięciu rozwija się menu z opcjami. QStackedWidget przełącza widoki (strony)."
• Każdy widok (zaloguj, formularz, wyswietl) ma własny plik
.ui → .py•
QStackedWidget tworzymy w kodzie i dodajemy do centralwidget• Każdy widok:
QWidget() → Ui_Xxx() → setupUi(widget) → stacked.addWidget(widget)•
self.stacked.setCurrentIndex(n) — przełącza widok•
QMenu — tworzymy w kodzie i podpinamy: self.ui.btnMenu.setMenu(menu)
„Tworzymy rozwijane menu (navbar) i podpinamy je do przycisku ☰ Menu. Każda opcja w menu przełączy widok QStackedWidget."
# ===== 2. WIDOKI + QStackedWidget =====
self.stacked = QtWidgets.QStackedWidget()
layout = QtWidgets.QVBoxLayout(self.ui.centralwidget)
layout.addWidget(self.stacked)
# Widok 0 — Zaloguj
self.w_zaloguj = QtWidgets.QWidget()
self.ui_zaloguj = Ui_Zaloguj()
self.ui_zaloguj.setupUi(self.w_zaloguj)
self.stacked.addWidget(self.w_zaloguj)
# Widok 1 — Formularz logowania
self.w_formularz = QtWidgets.QWidget()
self.ui_formularz = Ui_Formularz()
self.ui_formularz.setupUi(self.w_formularz)
self.stacked.addWidget(self.w_formularz)
# Widok 2 — Profil
self.w_wyswietl = QtWidgets.QWidget()
self.ui_wyswietl = Ui_Wyswietl()
self.ui_wyswietl.setupUi(self.w_wyswietl)
self.stacked.addWidget(self.w_wyswietl)
# ===== 3. NAVBAR — ROZWIJANE MENU =====
self.menu = QtWidgets.QMenu(self)
self.akcja_zaloguj = self.menu.addAction("Zaloguj")
self.akcja_logowanie = self.menu.addAction("Logowanie")
self.akcja_profil = self.menu.addAction("Profil")
self.ui.btnMenu.setMenu(self.menu)
# Kliknięcie opcji → przełącz widok
self.akcja_zaloguj.triggered.connect(lambda: self.stacked.setCurrentIndex(0))
self.akcja_logowanie.triggered.connect(lambda: self.stacked.setCurrentIndex(1))
self.akcja_profil.triggered.connect(lambda: self.stacked.setCurrentIndex(2))
# Tekst powitalny na widoku 0
self.ui_zaloguj.lblPowitanie.setText("Witaj!\nKliknij ☰ Menu → 'Logowanie'\naby się zalogować.")
„Uruchom. Kliknij przycisk ☰ Menu — rozwija się lista 3 opcji? Klikasz 'Logowanie' — widzisz formularz?"
„Skąd biorą się widżety self.ui_zaloguj.lblPowitanie, self.ui_formularz.entryImie itd.?"
Odp: Z plików wygenerowanych przez pyuic6. Widżety z zaloguj.ui dostępne przez self.ui_zaloguj., z formularz.ui przez self.ui_formularz., z wyswietl.ui przez self.ui_wyswietl.
„Co robi self.ui.btnMenu.setMenu(self.menu)?"
Odp: Podpina QMenu do przycisku — po kliknięciu rozwija się lista opcji (dropdown). To nasz navbar.
„Teraz najważniejsza część — funkcja zaloguj(). Odczytuje dane z pól, porównuje z danymi z pliku i reaguje."
# ===== 3. FUNKCJA LOGOWANIA =====
def zaloguj(self):
wpisane_imie = self.ui_formularz.entryImie.text().strip()
wpisane_nazwisko = self.ui_formularz.entryNazwisko.text().strip()
if wpisane_imie == self.prawidlowe_imie and wpisane_nazwisko == self.prawidlowe_nazwisko:
# Logowanie poprawne — pokaż profil
self.ui_wyswietl.lblProfil.setText(
f"Zalogowano!\n\nImię: {self.prawidlowe_imie}\nNazwisko: {self.prawidlowe_nazwisko}\nKlasa: {self.klasa}"
)
self.akcja_profil.setEnabled(True) # odblokuj opcję Profil w menu
self.stacked.setCurrentIndex(2) # przełącz na widok Profil
else:
QtWidgets.QMessageBox.critical(
self,
"Błąd logowania",
"Nieprawidłowe imię lub nazwisko!\nSpróbuj ponownie."
)
„Co robi self.ui_formularz.entryImie.text()?"
Odp: Pobiera tekst wpisany w pole QLineEdit z widoku formularz. Zwraca string. Dlatego .strip() na wypadek spacji.
„Dlaczego self.akcja_profil.setEnabled(True)?"
Odp: Odblokowujemy opcję „Profil" w menu, która wcześniej była szara i nieaktywna. Dopiero po poprawnym logowaniu użytkownik może ją kliknąć.
„Co robi self.stacked.setCurrentIndex(2)?"
Odp: Programowo przełącza QStackedWidget na stronę 2 (Profil) — tak jakby użytkownik wybrał tę opcję z menu.
zaloguj(self) umieść przed linią self.ui_formularz.btnZaloguj.clicked.connect(self.zaloguj) — Python musi znać metodę zanim ją podłączysz do sygnału.
„Ostatni krok — widok Profil. Na starcie opcja 'Profil' w menu jest wyłączona (szara). Odblokuje się dopiero po poprawnym logowaniu."
# ===== 4. USTAWIENIA PROFILU + BLOKADA =====
self.ui_wyswietl.lblProfil.setText("Nie jesteś zalogowany.")
# Zablokuj opcję Profil w menu na starcie
self.akcja_profil.setEnabled(False)
# ===== 5. PODŁĄCZENIE SYGNAŁÓW =====
self.ui_formularz.btnZaloguj.clicked.connect(self.zaloguj)
„Uruchom. Kliknij ☰ Menu — opcja 'Profil' jest szara/nieaktywna? Nie da się jej kliknąć?"
„Teraz kliknij 'Logowanie' w menu, wpisz poprawne dane (Jan, Kowalski) i kliknij Zaloguj. Co widzisz?"
Bonus: Dodaj przycisk „Wyloguj" na widoku Profil, który wróci do widoku 0, wyczyści pola i zablokuje z powrotem opcję Profil w menu.
#05 — Gotowy kod
Pliki .ui — widżety w Qt Creator
| Plik .ui | objectName | Typ | Opis |
|---|---|---|---|
| mainwindow.ui | btnMenu | QPushButton | Przycisk „☰ Menu" z rozwijanym QMenu |
| zaloguj.ui | lblPowitanie | QLabel | Tekst powitalny |
| formularz.ui | lblImie | QLabel | Etykieta „Imię:" |
| formularz.ui | entryImie | QLineEdit | Pole na imię |
| formularz.ui | lblNazwisko | QLabel | Etykieta „Nazwisko:" |
| formularz.ui | entryNazwisko | QLineEdit | Pole na nazwisko |
| formularz.ui | btnZaloguj | QPushButton | Przycisk „Zaloguj" |
| wyswietl.ui | lblProfil | QLabel | Dane profilu |
Konwersja:
pyuic6 mainwindow.ui -o mainwindow.py
pyuic6 zaloguj.ui -o zaloguj.py
pyuic6 formularz.ui -o formularz.py
pyuic6 wyswietl.ui -o wyswietl.py
main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
from zaloguj import Ui_Zaloguj
from formularz import Ui_Formularz
from wyswietl import Ui_Wyswietl
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.setWindowTitle("System logowania")
self.resize(400, 300)
# ===== 1. WCZYTANIE DANYCH Z PLIKU =====
with open("dane.txt", "r", encoding="utf-8") as plik:
linie = plik.readlines()
self.prawidlowe_imie = linie[0].strip()
self.prawidlowe_nazwisko = linie[1].strip()
self.klasa = linie[2].strip()
# ===== 2. WIDOKI + QStackedWidget =====
self.stacked = QtWidgets.QStackedWidget()
layout = QtWidgets.QVBoxLayout(self.ui.centralwidget)
layout.addWidget(self.stacked)
self.w_zaloguj = QtWidgets.QWidget()
self.ui_zaloguj = Ui_Zaloguj()
self.ui_zaloguj.setupUi(self.w_zaloguj)
self.stacked.addWidget(self.w_zaloguj)
self.w_formularz = QtWidgets.QWidget()
self.ui_formularz = Ui_Formularz()
self.ui_formularz.setupUi(self.w_formularz)
self.stacked.addWidget(self.w_formularz)
self.w_wyswietl = QtWidgets.QWidget()
self.ui_wyswietl = Ui_Wyswietl()
self.ui_wyswietl.setupUi(self.w_wyswietl)
self.stacked.addWidget(self.w_wyswietl)
# ===== 3. NAVBAR — ROZWIJANE MENU =====
self.menu = QtWidgets.QMenu(self)
self.akcja_zaloguj = self.menu.addAction("Zaloguj")
self.akcja_logowanie = self.menu.addAction("Logowanie")
self.akcja_profil = self.menu.addAction("Profil")
self.ui.btnMenu.setMenu(self.menu)
self.akcja_zaloguj.triggered.connect(lambda: self.stacked.setCurrentIndex(0))
self.akcja_logowanie.triggered.connect(lambda: self.stacked.setCurrentIndex(1))
self.akcja_profil.triggered.connect(lambda: self.stacked.setCurrentIndex(2))
# ===== 4. USTAWIENIA WIDŻETÓW =====
self.ui_zaloguj.lblPowitanie.setText(
"Witaj!\nKliknij ☰ Menu → 'Logowanie'\naby się zalogować."
)
self.ui_wyswietl.lblProfil.setText("Nie jesteś zalogowany.")
self.akcja_profil.setEnabled(False)
# ===== 5. PODŁĄCZENIE SYGNAŁÓW =====
self.ui_formularz.btnZaloguj.clicked.connect(self.zaloguj)
# ===== 6. METODA LOGOWANIA =====
def zaloguj(self):
wpisane_imie = self.ui_formularz.entryImie.text().strip()
wpisane_nazwisko = self.ui_formularz.entryNazwisko.text().strip()
if wpisane_imie == self.prawidlowe_imie and wpisane_nazwisko == self.prawidlowe_nazwisko:
self.ui_wyswietl.lblProfil.setText(
f"Zalogowano!\n\nImię: {self.prawidlowe_imie}\n"
f"Nazwisko: {self.prawidlowe_nazwisko}\nKlasa: {self.klasa}"
)
self.akcja_profil.setEnabled(True)
self.stacked.setCurrentIndex(2)
else:
QtWidgets.QMessageBox.critical(
self,
"Błąd logowania",
"Nieprawidłowe imię lub nazwisko!\nSpróbuj ponownie."
)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
dane.txt
Jan
Kowalski
3TIP
1. Zaprojektuj 4 formularze w Qt Creator → zapisz jako
mainwindow.ui, zaloguj.ui, formularz.ui, wyswietl.ui2. Konwertuj każdy:
pyuic6 mainwindow.ui -o mainwindow.py (i analogicznie pozostałe 3)3. Utwórz
main.py i dane.txt w tym samym folderze4. Uruchom:
python main.py5. Kliknij ☰ Menu → „Logowanie", wpisz Jan i Kowalski, kliknij „Zaloguj"
6. Widok przełączy się na „Profil" z danymi: Jan Kowalski, klasa 3TIP
📝 Sprawdzian — Logowanie z nawigacją menu
5 pytań · 5 minut · minimum 60% żeby zdać
entry.text()?readlines() należy użyć .strip() na każdej linii?Rozgrzewka — Konwerter walut
Szybkie pytania na początek lekcji — kliknij pytanie żeby zobaczyć odpowiedź.
1. Czym różni się input() od print()?
input() CZYTA tekst od użytkownika i go zwraca. print() WYPISUJE tekst na ekran. Pamiętaj: input() zawsze zwraca string — żeby dostać liczbę musisz użyć int() lub float().
2. Co to jest słownik (dict) w Pythonie?
Słownik to zbiór par klucz-wartość: {"USD": 4.05, "EUR": 4.32}. Dostęp przez klucz: kursy["USD"] daje 4.05. Idealny do tabeli kursów walut.
3. Kiedy użyć pętli while a kiedy for?
for — gdy znamy ile razy iterować (po liście, range). while — gdy iterujemy DOPÓKI warunek prawdziwy (np. menu programu działa do wyboru "0 - Wyjście").
4. Jak sformatować liczbę do 2 miejsc po przecinku?
F-string z formatowaniem: f"{kwota:.2f}". Np. f"{4.0567:.2f}" → "4.06". Dwie cyfry po kropce + zaokrąglanie.
5. Co robi try / except?
Łapie błędy. try próbuje wykonać kod, except ValueError łapie błąd jeśli się pojawi. Idealne gdy użytkownik wpisze tekst zamiast liczby — program nie crashuje, tylko pokazuje komunikat.
Konwerter walut — Instrukcja
Treść zadania
Napisz program konsolowy który:
- Wyświetla menu z 4 walutami: USD, EUR, GBP, CHF (kursy w PLN)
- Pyta użytkownika ile PLN chce przeliczyć
- Pyta na jaką walutę przeliczyć
- Wyświetla wynik z dokładnością do 2 miejsc po przecinku
- Po przeliczeniu pyta czy kontynuować — pętla działa do wyjścia
- Łapie błędy gdy użytkownik wpisze tekst zamiast liczby (try/except)
Czego się nauczysz
| Pojęcie | Co robi |
|---|---|
dict (słownik) | Tabela kursów walut: nazwa → kurs |
input() | Pobranie danych od użytkownika |
float() | Konwersja stringa na liczbę zmiennoprzecinkową |
while True | Pętla menu — działa do break |
try / except | Obsługa błędów (np. tekst zamiast liczby) |
f-string :.2f | Formatowanie liczby do 2 miejsc po przecinku |
def | Wydzielenie kodu do funkcji |
Przykładowe działanie
=== KONWERTER WALUT ===
Dostępne waluty:
USD - 4.05 PLN
EUR - 4.32 PLN
GBP - 5.18 PLN
CHF - 4.61 PLN
Ile PLN chcesz przeliczyć? 100
Na jaką walutę? (USD/EUR/GBP/CHF): EUR
100.00 PLN = 23.15 EUR
Kontynuować? (t/n): n
Do widzenia!
Konwerter walut — Budowa krok po kroku
Utwórz plik konwerter.py. Na górze pliku dodaj słownik kursów oraz funkcję wyświetlającą menu:
kursy = {
"USD": 4.05,
"EUR": 4.32,
"GBP": 5.18,
"CHF": 4.61
}
def pokaz_menu():
print("\n=== KONWERTER WALUT ===\n")
print("Dostępne waluty:")
for waluta, kurs in kursy.items():
print(f" {waluta} - {kurs} PLN")
waluta i kurs.
def przelicz(kwota_pln, waluta):
kurs = kursy[waluta]
wynik = kwota_pln / kurs
return wynik
Funkcja przyjmuje kwotę w PLN i symbol waluty, dzieli przez kurs i zwraca wynik.
100 / 4.05 ≈ 24.69 USD.
while True:
pokaz_menu()
# Pobierz kwotę
kwota_str = input("\nIle PLN chcesz przeliczyć? ")
kwota = float(kwota_str)
# Pobierz walutę
waluta = input("Na jaką walutę? (USD/EUR/GBP/CHF): ").upper()
# Przelicz
wynik = przelicz(kwota, waluta)
print(f"\n{kwota:.2f} PLN = {wynik:.2f} {waluta}")
# Czy kontynuować?
odp = input("\nKontynuować? (t/n): ").lower()
if odp == "n":
print("Do widzenia!")
break
.upper()— zamienia "eur" → "EUR" (klucze w słowniku są wielkimi literami):.2f— formatuje liczbę do 2 miejsc po przecinku (np. 23.149 → 23.15)while True ... break— pętla nieskończona aż dobreak
Co jeśli użytkownik wpisze "JPY"? Słownik nie zawiera tej waluty — program crashuje. Dodaj sprawdzenie:
waluta = input("Na jaką walutę? (USD/EUR/GBP/CHF): ").upper()
if waluta not in kursy:
print(f"Nieznana waluta: {waluta}")
continue # wraca na początek pętli
Co jeśli zamiast "100" użytkownik wpisze "sto"? float("sto") rzuca ValueError — program się wywala.
kwota_str = input("\nIle PLN chcesz przeliczyć? ")
try:
kwota = float(kwota_str)
except ValueError:
print(f"To nie jest liczba: {kwota_str}")
continue
try— "spróbuj wykonać ten kod"except ValueError— "jeśli wystąpi błąd ValueError, zrób to"- Bez try/except program by crashował przy złym wejściu
W terminalu uruchom: python konwerter.py
Sprawdź każdy scenariusz:
| Test | Oczekiwany wynik |
|---|---|
| Wpisz: 100, EUR | 23.15 EUR |
| Wpisz: 200, usd (małe litery) | 49.38 USD (działa dzięki .upper()) |
| Wpisz: 50, JPY | "Nieznana waluta: JPY", wraca do menu |
| Wpisz: sto, EUR | "To nie jest liczba: sto", wraca do menu |
| Po przeliczeniu wpisz: n | "Do widzenia!" — program kończy |
| Po przeliczeniu wpisz: t | Wraca do menu, można przeliczać dalej |
Konwerter walut — Gotowy kod
konwerter.py
kursy = {
"USD": 4.05,
"EUR": 4.32,
"GBP": 5.18,
"CHF": 4.61
}
def pokaz_menu():
print("\n=== KONWERTER WALUT ===\n")
print("Dostępne waluty:")
for waluta, kurs in kursy.items():
print(f" {waluta} - {kurs} PLN")
def przelicz(kwota_pln, waluta):
kurs = kursy[waluta]
wynik = kwota_pln / kurs
return wynik
while True:
pokaz_menu()
# Pobierz kwotę z obsługą błędu
kwota_str = input("\nIle PLN chcesz przeliczyć? ")
try:
kwota = float(kwota_str)
except ValueError:
print(f"To nie jest liczba: {kwota_str}")
continue
# Pobierz walutę i sprawdź czy istnieje
waluta = input("Na jaką walutę? (USD/EUR/GBP/CHF): ").upper()
if waluta not in kursy:
print(f"Nieznana waluta: {waluta}")
continue
# Przelicz i wyświetl wynik
wynik = przelicz(kwota, waluta)
print(f"\n{kwota:.2f} PLN = {wynik:.2f} {waluta}")
# Czy kontynuować?
odp = input("\nKontynuować? (t/n): ").lower()
if odp == "n":
print("Do widzenia!")
break
Co warto rozszerzyć (zadania domowe)
- Dodaj odwrotną konwersję (z USD na PLN)
- Wczytaj kursy z pliku
kursy.txtzamiast trzymać w kodzie - Zapisuj historię przeliczeń do pliku
historia.txt - Dodaj walutę z największym kursem (znajdź
max(kursy.values()))
Konwerter walut — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
input()?continue w pętli?float("abc") bez try/except?kursy?Rozgrzewka — Generator haseł
Pięć pytań na pierwsze minuty sesji. Każde dotyczy jednej linijki, która pojawi się dziś w programie. Kliknij pytanie, żeby zobaczyć odpowiedź.
1. Jakie liczby może zwrócić random.randint(0, 5)?
Od 0 do 5 włącznie, czyli sześć różnych wartości. To odróżnia randint od range(0, 5), który kończy się na 4. Na tej różnicy przewraca się połowa generatorów haseł.
2. Co wypisze print("kot"[1]), a co print("kot"[3])?
Pierwsze wypisze o, bo indeksy liczymy od zera. Drugie skończy się błędem IndexError, bo napis ma 3 znaki o indeksach 0, 1 i 2. Ostatni poprawny indeks to zawsze len(napis) - 1.
3. Jak wstawić literę X w środek napisu "kot", żeby wyszło "koXt"?
Napisu w Pythonie nie da się zmienić w miejscu. Trzeba go skleić na nowo z trzech kawałków: "kot"[:2] + "X" + "kot"[2:]. [:2] to wszystko przed indeksem 2, [2:] to wszystko od indeksu 2 do końca.
4. Czym różni się funkcja, która robi print(haslo), od funkcji, która robi return haslo?
Pierwsza tylko pokazuje hasło na ekranie, a program nie może go potem nigdzie użyć. Druga oddaje hasło do miejsca, z którego ją wywołano. Arkusz wymaga funkcji, która hasło zwraca, więc wersja z samym print traci punkt nawet wtedy, gdy na ekranie wszystko wygląda dobrze.
5. Ile wynosi len("ąęł") w Pythonie?
3. Python liczy znaki, a nie bajty, więc polska litera to jeden znak, tak samo jak a. W C++ ten sam napis miałby 6 bajtów. To jeden z powodów, dla których do tego zadania wybieramy Pythona.
Generator haseł — Instrukcja
Cały egzamin trwa 180 minut i ma trzy części: aplikację konsolową, aplikację webową (Angular albo React) i dokumentację. Dziś robimy część I od pustego folderu do gotowego konsola.zip, łącznie z tą częścią dokumentacji, która jej dotyczy. Cel na koniec sesji: część I razem z dokumentacją w 40 minut. Tyle czasu zostawia ponad dwie godziny na aplikację webową, a na niej zwykle brakuje czasu.
Treść zadania
Za pomocą narzędzi do tworzenia aplikacji konsolowych zaimplementuj program do tworzenia bezpiecznych haseł.
- Język obiektowy zainstalowany na stanowisku: C++, C#, Java albo Python. Podejście strukturalne albo obiektowe
- Znaczące nazewnictwo (polskie albo angielskie), czytelny kod, zrozumiała komunikacja z użytkownikiem
- Nie można zastosować metody wbudowanej ani funkcji bibliotecznej do generowania gotowego hasła
- Hasło przechowywane w zmiennej typu napisowego
- Funkcja zwraca 12-znakowe hasło, w którym są wylosowane:
- 3 małe litery z alfabetu łacińskiego (26 znaków a–z)
- 3 wielkie litery z alfabetu łacińskiego (26 znaków A–Z)
- 2 litery ze znakami diakrytycznymi ze zbioru
ą ę ł ń ó ś ż ź Ą Ę Ł Ń Ó Ś Ż Ź(plikznaki.txt) - 2 cyfry 0–9
- 2 znaki specjalne z zestawu
!@#$%^&*()_+
- Każdy z wylosowanych 12 znaków jest umieszczany w losowym miejscu w haśle
- Program główny generuje 5 haseł i wyświetla je ze znaczącym komunikatem
- W folderze
konsolowa: archiwum całego projektukonsola.zip, skopiowany plik z kodem źródłowym i plik wykonywalny, jeżeli istnieje
Materiały: archiwum Web1.7z na pulpicie, hasło *LoGiny&H@sla8. W środku są znaki.txt (polskie litery i znaki specjalne) i szablon.txt (formularz Bootstrap do części II).
Pliki do ćwiczeń: Web1.7z (to samo archiwum co na egzaminie, z tym samym hasłem) oraz sprawdz_hasla.py, czyli sprawdzarka z kroku 7. Pełny arkusz w PDF: arkusze.pl.
Co sprawdza egzaminator i gdzie to jest w kodzie
| Wymaganie z arkusza | Gdzie w programie | Jak łatwo to stracić |
|---|---|---|
| Funkcja zwracająca hasło | def generuj_haslo() zakończona return haslo | funkcja robi print zamiast return |
| Liczba znaków z każdego zbioru | losuj_znaki(ZBIOR, ile) wywołana pięć razy | losowanie z jednego wspólnego zbioru, wtedy proporcje wychodzą przypadkowe |
| Każdy znak w losowym miejscu | pętla z random.randint(0, len(haslo)) | random.shuffle albo hasło zawsze w kolejności „małe, wielkie, polskie, cyfry, znaki” |
| Hasło w zmiennej napisowej | haslo = "" i doklejanie znaków | funkcja zwraca listę znaków |
| Bez gotowego generatora | tylko random.randint do losowania liczb | secrets.token_urlsafe, random.choices, random.sample |
| 5 haseł ze znaczącym komunikatem | pętla for numer in range(1, 6) na dole pliku | same hasła bez opisu, albo jedno hasło |
| Nagłówek dokumentacji funkcji | komentarz według listingu 1 pod def generuj_haslo() | brak pola autor albo wpisane imię zamiast numeru zdającego |
| Zrzuty, plik egzamin, konsola.zip | folder z numerem zdającego (krok 8) | zrzut samego okna bez paska zadań, zip bez całego projektu |
Czego się nauczysz
| Pojęcie | Do czego dziś służy |
|---|---|
random.randint(a, b) | losuje indeks znaku i miejsce w haśle, oba końce włącznie |
indeks napisu zbior[i] | wybiera jeden znak ze zbioru |
wycinek haslo[:p], haslo[p:] | wstawia znak w środek napisu |
funkcja z parametrami i return | losuj_znaki(zbior, ile) zamiast pięciu kopii tej samej pętli |
| stała (nazwa wielkimi literami) | zbiory znaków, których program nie zmienia |
| docstring | nagłówek dokumentacji, który egzaminator ocenia w części III |
Generator haseł — Budowa krok po kroku
Uczeń pisze w swoim środowisku (PyCharm albo VS Code) i udostępnia ekran. Ty pytasz „co teraz?” i nie dyktujesz kodu. Na egzaminie nikt mu go nie podyktuje.
Pierwsze trzy minuty egzaminu wyglądają zawsze tak samo. Warto je wyćwiczyć, żeby nie myśleć o nich w dniu egzaminu.
00000000000/ <- numer zdającego (PESEL), na ćwiczeniach dowolny
├── konsolowa/
├── webowa/
└── dokumentacja/
- Utwórz folder z numerem zdającego i w nim trzy podfoldery. Nazwy dokładnie jak w arkuszu, małymi literami.
- Rozpakuj
Web1.7zhasłem*LoGiny&H@sla8. Na Windows: prawy przycisk → 7-Zip → Wypakuj tutaj. Na macOS: Keka albo The Unarchiver, bo oba obsługują hasło. - Utwórz projekt
konsola: w PyCharm New Project, w VS Code nowy folder. W środku plikmain.py.
*, w środku ma & i @. Najczęstszy błąd to pominięta gwiazdka. Przy błędnym haśle 7-Zip pokazuje „Wrong password”, a nie „brak pliku”.Na górze main.py wpisz pięć zbiorów z arkusza. Polskie litery i znaki specjalne skopiuj z znaki.txt, nie wpisuj ich z klawiatury.
import random
MALE_LITERY = "abcdefghijklmnopqrstuvwxyz"
WIELKIE_LITERY = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
POLSKIE_LITERY = "ąęłńóśżźĄĘŁŃÓŚŻŹ"
CYFRY = "0123456789"
ZNAKI_SPECJALNE = "!@#$%^&*()_+"
ż łatwo wpisać ź. CKE dołącza znaki.txt właśnie po to, żeby zbiór dało się skopiować. Wolno też użyć string.ascii_lowercase, ale zbiór przepisany 1:1 z arkusza egzaminator sprawdzi jednym spojrzeniem.Pięć razy trzeba zrobić to samo: wylosować kilka znaków z jednego zbioru. Zamiast pięciu kopii pętli piszemy jedną funkcję z dwoma parametrami.
def losuj_znaki(zbior, ile):
wylosowane = ""
for i in range(ile):
indeks = random.randint(0, len(zbior) - 1)
wylosowane += zbior[indeks]
return wylosowane
Sprawdzenie od razu pod funkcją (po sprawdzeniu usuń tę linijkę):
print(losuj_znaki(CYFRY, 2)) # np. 70
print(losuj_znaki(POLSKIE_LITERY, 2)) # np. śŁ
randint(0, len(zbior))
randint losuje z górną granicą włącznie. Dla 10 cyfr randint(0, 10) czasem wylosuje 10, a CYFRY[10] nie istnieje. Program działa poprawnie dziewięć razy na dziesięć i pada na pokazie przed egzaminatorem. Ostatni indeks to zawsze len(zbior) - 1.random.choice(zbior)?
Losuje jeden znak, a nie gotowe hasło, więc arkusz go nie zabrania. Wersja z randint i indeksem pokazuje jednak, że uczeń rozumie, co się dzieje, i nie zostawia egzaminatorowi pola do interpretacji. Na egzaminie wybieramy wersję pewną.Funkcja ma dwa etapy. Pierwszy: zebrać wszystkie 12 znaków w liczbie, jakiej wymaga arkusz.
def generuj_haslo():
znaki = (losuj_znaki(MALE_LITERY, 3)
+ losuj_znaki(WIELKIE_LITERY, 3)
+ losuj_znaki(POLSKIE_LITERY, 2)
+ losuj_znaki(CYFRY, 2)
+ losuj_znaki(ZNAKI_SPECJALNE, 2))
return znaki
Uruchom print(generuj_haslo()). Wynik wygląda np. tak: qbmTKAźŻ48#(. Długość się zgadza, ale znaki stoją zawsze w tej samej kolejności grup. Takie hasło nie spełnia wymagania „każdy znak w losowym miejscu”, i to właśnie naprawia krok 5.
\. Każda linia to jeden wiersz z listy wymagań, więc egzaminator odhacza je po kolei.Zaczynamy od pustego hasła. Każdy z 12 znaków wstawiamy w wylosowane miejsce: na początek, na koniec albo między znaki, które już tam są. Zamień return znaki na:
haslo = ""
for znak in znaki:
pozycja = random.randint(0, len(haslo))
haslo = haslo[:pozycja] + znak + haslo[pozycja:]
return haslo
Rozpisz z uczniem trzy pierwsze obroty na udostępnionym ekranie, zanim cokolwiek uruchomi:
| Obrót | znak | len(haslo) | wylosowana pozycja | haslo po obrocie |
|---|---|---|---|---|
| 1 | q | 0 | 0 (jedyna możliwa) | q |
| 2 | b | 1 | 1 (na koniec) | qb |
| 3 | m | 2 | 1 (w środek) | qmb |
randint(0, len(haslo) - 1)
Tu odejmowanie jedynki jest błędem, odwrotnie niż w kroku 3. Pozycja len(haslo) oznacza „za ostatnim znakiem”, czyli dopisanie na koniec, i musi być możliwa do wylosowania. Bez niej pierwszy obrót daje randint(0, -1) i program pada z ValueError.random.shuffle
Arkusz mówi wprost, że każdy znak ma być umieszczany w losowym miejscu. Wymieszanie gotowego ciągu jedną funkcją biblioteczną to dokładnie to, czego zabrania punkt o metodach wbudowanych. Do tego shuffle działa tylko na liście, więc trzeba by jeszcze zamieniać hasło z napisu na listę i z powrotem.Na samym dole pliku, bez wcięcia:
print("Generator bezpiecznych haseł - 5 nowych haseł:")
for numer in range(1, 6):
print(f"Hasło {numer}: {generuj_haslo()}")
Generator bezpiecznych haseł - 5 nowych haseł:
Hasło 1: ś^DĄ7U1xof)W
Hasło 2: a@źLQv5iĘ_Q8
Hasło 3: FXmpŚc5^3S#Ł
Hasło 4: T2j%Ąv1UuIĄ&
Hasło 5: C%*t2LRkś8Ęe
range(1, 6), a nie range(5)
Oba dają pięć obrotów, ale pierwszy numeruje hasła od 1. „Hasło 0” to drobiazg, a takie drobiazgi egzaminator ocenia jako komunikację z użytkownikiem.Pięć haseł na ekranie nie dowodzi, że program jest dobry. Błąd z kroku 3 wychodzi średnio raz na kilka uruchomień, a hasła ułożone zawsze w tej samej kolejności grup trudno zauważyć gołym okiem. Sprawdzarka wywołuje funkcję 10 000 razy i sprawdza każde wymaganie z arkusza osobno.
- Pobierz
sprawdz_hasla.pydo folderu, w którym leżymain.py. - W terminalu IDE uruchom:
python sprawdz_hasla.py main.py # Windows
python3 sprawdz_hasla.py main.py # macOS
2. Funkcja generuj_haslo() - 10000 wywołań
OK żadne wywołanie nie kończy się błędem
OK zwraca napis (str), nie listę
OK każde hasło ma 12 znaków
...
OK każdy rodzaj znaku trafia na każdą pozycję równie często
...
Wynik: 15/15 wymagań spełnionych
Każdy BŁĄD ma pod spodem wskazówkę, co poprawić. Cel to 15/15. Zanim uczeń pobiegnie dalej, niech celowo wprowadzi błąd z kroku 3 (randint(0, len(zbior))) i zobaczy, jak sprawdzarka go łapie. Tego błędu nie da się potem zapomnieć.
a) Nagłówek funkcji według listingu 1. Jeden nagłówek dla jednej funkcji wystarczy. Pisz go pod def generuj_haslo(): jako docstring:
def generuj_haslo():
"""
nazwa funkcji: generuj_haslo
opis funkcji: losuje 3 małe litery, 3 wielkie litery, 2 polskie litery,
2 cyfry i 2 znaki specjalne, a następnie wstawia każdy
z tych znaków w losowe miejsce hasła
parametry: brak
zwracany typ i opis: str - wygenerowane 12-znakowe hasło
autor: 00000000000
"""
W polu autor wpisujesz numer zdającego, czyli PESEL. Nie imię i nie „Maks”. Wszystkie pięć pól musi wystąpić, nawet jeśli wartość to „brak”.
b) Zrzuty ekranu. Cały ekran z widocznym paskiem zadań: okno z wynikiem programu i otwarte środowisko z projektem w jednym kadrze. Plik nazwij konsola1.png i zapisz w dokumentacja.
| System | Skrót | Gdzie ląduje plik |
|---|---|---|
| Windows (egzamin) | Win + PrtSc | Obrazy → Zrzuty ekranu, potem zmień nazwę i przenieś |
| macOS (ćwiczenia w domu) | Cmd + Shift + 3 | Biurko. Na zrzucie musi być widoczny Dock |
Program nie prowadzi interakcji z użytkownikiem, więc jeden zrzut wystarcza.
c) Plik egzamin.txt w folderze dokumentacja, z prawdziwymi nazwami ze stanowiska:
System operacyjny: Windows 11 Pro
Środowisko programistyczne: PyCharm Community, Visual Studio Code
Języki programowania: Python, JavaScript
d) Folder konsolowa:
konsola.zip, czyli spakowany cały folder projektu. Na Windows: prawy przycisk na folderze → Wyślij do → Folder skompresowany, potem zmiana nazwy.- skopiowany
main.py, czyli plik, który był modyfikowany - pliku wykonywalnego Python nie tworzy, więc nic nie dorabiasz (bez PyInstallera)
sprawdz_hasla.py leży w folderze projektu, usuń go przed pakowaniem. Na egzaminie tego pliku i tak nie będzie, ale nawyk pakowania wszystkiego jak leci kosztuje na egzaminie czas i porządek.Generator haseł — Gotowy kod
Kompletne rozwiązanie części I. Przechodzi sprawdzarkę 15/15. Kod pokazujesz dopiero po samodzielnej próbie ucznia, a nie zamiast niej.
main.py
import random
MALE_LITERY = "abcdefghijklmnopqrstuvwxyz"
WIELKIE_LITERY = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
POLSKIE_LITERY = "ąęłńóśżźĄĘŁŃÓŚŻŹ"
CYFRY = "0123456789"
ZNAKI_SPECJALNE = "!@#$%^&*()_+"
def losuj_znaki(zbior, ile):
wylosowane = ""
for i in range(ile):
indeks = random.randint(0, len(zbior) - 1)
wylosowane += zbior[indeks]
return wylosowane
def generuj_haslo():
"""
nazwa funkcji: generuj_haslo
opis funkcji: losuje 3 małe litery, 3 wielkie litery, 2 polskie litery,
2 cyfry i 2 znaki specjalne, a następnie wstawia każdy
z tych znaków w losowe miejsce hasła
parametry: brak
zwracany typ i opis: str - wygenerowane 12-znakowe hasło
autor: 00000000000
"""
znaki = (losuj_znaki(MALE_LITERY, 3)
+ losuj_znaki(WIELKIE_LITERY, 3)
+ losuj_znaki(POLSKIE_LITERY, 2)
+ losuj_znaki(CYFRY, 2)
+ losuj_znaki(ZNAKI_SPECJALNE, 2))
haslo = ""
for znak in znaki:
pozycja = random.randint(0, len(haslo))
haslo = haslo[:pozycja] + znak + haslo[pozycja:]
return haslo
print("Generator bezpiecznych haseł - 5 nowych haseł:")
for numer in range(1, 6):
print(f"Hasło {numer}: {generuj_haslo()}")
Jak to czytać w trzech zdaniach
losuj_znakilosujeileznaków z jednego zbioru i oddaje je jako napis.generuj_haslonajpierw zbiera 12 znaków w kolejności z arkusza, potem przenosi je po jednym do pustego hasła, każdy w wylosowane miejsce.- Dół pliku woła funkcję pięć razy i podpisuje każde hasło numerem.
Wariant na powtórkę w domu
To samo zadanie z innymi liczbami. Sprawdza, czy uczeń rozumie rozwiązanie, czy tylko je pamięta. Zmień funkcję tak, żeby generowała 16-znakowe hasło: 4 małe, 4 wielkie, 3 polskie, 3 cyfry, 2 znaki specjalne. Na ekranie mają być 3 hasła. Co musisz zmienić? Tylko liczby w generuj_haslo i w range. Jeśli uczeń zaczyna przepisywać pętlę wstawiania, to znaczy, że jej nie rozumie.
Generator haseł — Sprawdzian
5 pytań · 5 minut · minimum 60%, żeby zdać
CYFRY = "0123456789". Która linia poprawnie losuje indeks cyfry?"abc"[:1] + "X" + "abc"[1:]?generuj_haslo() wypisuje hasło przez print i nic nie zwraca. Na ekranie wszystko się zgadza. Co z punktacją?autor: nagłówka dokumentacji na egzaminie?Rozgrzewka — Klasa z tablicą
Pięć pytań na pierwsze minuty sesji. Każde dotyczy jednej linijki, która pojawi się dziś w programie. Kliknij pytanie, żeby zobaczyć odpowiedź.
1. Czym różni się self._tablica od self.__tablica?
Jedno podkreślenie to tylko umowa między programistami: „nie dotykaj”. Python niczego nie blokuje, a klasa potomna widzi takie pole bez przeszkód. Dwa podkreślenia uruchamiają zmianę nazwy: w klasie Tablica pole naprawdę nazywa się _Tablica__tablica, więc zapis self.__tablica w klasie potomnej szuka innego pola i kończy się błędem. Arkusz wymaga pól „niedostępnych dla klas potomnych”, czyli dwóch podkreśleń.
2. Co zwraca ta funkcja dla listy [3, 8, 5] i wartości 8?
def szukaj(lista, wartosc):
for i in range(len(lista)):
if lista[i] == wartosc:
return i
else:
return -1Zwraca -1, chociaż 8 jest w liście. Pętla sprawdza tylko pierwszy element: 3 to nie 8, więc else od razu kończy funkcję. return -1 musi stać po pętli, bez wcięcia pod if. To najczęstszy błąd w metodzie wyszukującej.
3. Ile wynosi 7 / 2, a ile 7 // 2?
3.5 i 3. Pojedynczy ukośnik daje dokładny wynik, podwójny obcina część ułamkową. Średnia liczona przez // jest po prostu zła.
4. Jak sprawdzić, że liczba x jest nieparzysta?
x % 2 != 0, czyli reszta z dzielenia przez 2 jest różna od zera. W Pythonie zadziała też x % 2 == 1. W C++ i Javie dla liczb ujemnych reszta wychodzi -1, dlatego warto mieć nawyk pisania != 0.
5. Metoda wypisuje wynik przez print i nic nie zwraca. Arkusz mówi „zwracająca średnią”. Co z punktem?
Przepada. Metoda ma zwrócić średnią (return), a wyświetlić ją ma program główny. Ten sam podział dotyczy wyszukiwania: metoda zwraca indeks, a komunikat wypisuje program główny.
Klasa z tablicą — Instrukcja
Cały egzamin trwa 180 minut: aplikacja konsolowa, aplikacja webowa (Angular albo React) i dokumentacja. Część I to klasa z tablicą liczb i czterema metodami. Część II, galeria zdjęć z kategoriami, jest w React → ćwiczenie #1. Cel: część I razem z dokumentacją w 35 minut.
Treść zadania
Za pomocą narzędzi do tworzenia aplikacji konsolowych zaimplementuj program realizujący różne operacje na tablicach.
- Język obiektowy zainstalowany na stanowisku: C++, C#, Java albo Python
- Tablica oraz operacje na niej wykonywane są implementowane z wykorzystaniem klasy
- Pola klasy:
- tablica liczb całkowitych (tradycyjna tablica, a w Python lista)
- liczba elementów tablicy jako liczba całkowita; pole przechowuje faktyczną liczbę elementów, wszystkie operacje są ograniczone wartością tego pola
- oba pola dostępne tylko w tej klasie i niedostępne dla klas potomnych
- Konstruktor przyjmuje rozmiar tablicy, ustawia na niego pole liczby elementów i wypełnia tablicę pseudolosowymi liczbami całkowitymi od 1 do 1000
- Metody, wszystkie dostępne poza klasą:
- wyświetlająca wszystkie elementy w postaci
<index_tablicy>: <wartość>, nie zwraca wartości - wyszukująca pierwsze wystąpienie wartości przekazanej jako argument; zwraca indeks albo
-1 - wyświetlająca wszystkie wartości nieparzyste i zwracająca ich liczbę
- licząca średnią arytmetyczną i zwracająca ją
- wyświetlająca wszystkie elementy w postaci
- Program główny tworzy obiekt z rozmiarem większym od 20 i sprawdza wszystkie metody: wyświetla elementy; jeśli wartość znaleziono, wypisuje komunikat z indeksem, w przeciwnym razie nic; wyświetla nieparzyste i ich liczbę z komentarzem; wyświetla średnią
- Komunikacja zrozumiała, kod czytelny, znaczące nazwy (dla klasy i pola tablicowego dopuszczalne
tab,tablica,table) - W folderze
konsolowa:konsola.zipz całym projektem, plik z kodem źródłowym i plik wykonywalny, jeżeli istnieje
Do części I arkusz nie daje żadnych plików. Archiwum pliki3.zip (hasło K@tegorie)) dotyczy części II.
Obraz 1 z arkusza pokazuje koniec wydruku tablicy z 50 elementami (indeksy do 49), od razu po nim „Liczby nieparzyste:” z listą, „Razem nieparzystych: 25” i „Średnia wszystkich elemenetów: 525”. Między tablicą a nieparzystymi nie ma żadnego komunikatu, bo szukanej wartości nie znaleziono. Program nie zadaje pytań.
Pliki do ćwiczeń: sprawdz_tablice.py, czyli sprawdzarka z kroku 7. Pełny arkusz w PDF: arkusze.pl.
Co sprawdza egzaminator i gdzie to jest w kodzie
| Wymaganie z arkusza | Gdzie w programie | Jak łatwo to stracić |
|---|---|---|
| Pola niedostępne dla klas potomnych | self.__tablica, self.__liczba_elementow | jedno podkreślenie albo pola publiczne |
| Konstruktor: rozmiar, liczba elementów, losowanie | __init__(self, rozmiar) z pętlą i random.randint(1, 1000) | randrange(1, 1000), który nigdy nie da 1000 |
| Operacje ograniczone liczbą elementów | każda pętla: range(self.__liczba_elementow) | len(self.__tablica) albo for x in self.__tablica |
Format indeks: wartość | print(f"{i}: {self.__tablica[i]}") | sama wartość, bez indeksu |
| Pierwsze wystąpienie albo -1 | return i w pętli, return -1 po pętli | else: return -1 w pętli |
| Nieparzyste: wypisz i zwróć liczbę | print w pętli, return ile na końcu | tylko jedno z dwóch |
| Średnia zwracana | return suma / self.__liczba_elementow | // albo print zamiast return |
| Program główny: rozmiar > 20, brak komunikatu przy -1 | Tablica(50) i if indeks != -1: | Tablica(20) (20 nie jest większe od 20) albo wypisane „-1” |
| Nagłówek dokumentacji metody | docstring według listingu 1 pod def szukaj | brak pola parametry albo imię zamiast numeru zdającego |
Czego się nauczysz
| Pojęcie | Do czego dziś służy |
|---|---|
klasa, konstruktor __init__ | tablica i operacje w jednym obiekcie |
pole prywatne self.__nazwa | wymaganie „niedostępne dla klas potomnych” |
metoda z return kontra metoda z print | podział pracy między klasą a programem głównym |
| wyszukiwanie liniowe | pierwsze wystąpienie, wartość -1 jako „nie ma” |
operator % i dzielenie / | nieparzystość i średnia |
Klasa z tablicą — Budowa krok po kroku
Uczeń pisze w swoim środowisku i udostępnia ekran. Ty pytasz „co teraz?” i nie dyktujesz kodu.
00000000000/ <- numer zdającego (PESEL), na ćwiczeniach dowolny
├── konsolowa/
├── webowa/
└── dokumentacja/
- Folder z numerem zdającego i trzy podfoldery, nazwy dokładnie jak w arkuszu.
- Projekt
konsola(PyCharm: New Project, VS Code: nowy folder), w nimmain.py. - Archiwum
pliki3.zipna razie zostaje zamknięte. Część I nie potrzebuje żadnych plików.
import random
class Tablica:
def __init__(self, rozmiar):
self.__liczba_elementow = rozmiar
self.__tablica = []
for i in range(rozmiar):
self.__tablica.append(random.randint(1, 1000))
| Zapis pola | Poza klasą | W klasie potomnej | Spełnia arkusz? |
|---|---|---|---|
self.tablica | dostępne | dostępne | nie |
self._tablica | dostępne (to tylko umowa) | dostępne | nie |
self.__tablica | AttributeError | AttributeError | tak |
private. protected byłoby błędem, bo klasa potomna ma do niego dostęp. W Pythonie słowa private nie ma, jego rolę pełnią dwa podkreślenia.random.randrange(1, 1000)
randrange działa jak range i kończy się na 999. randint(1, 1000) losuje z oboma końcami. Sprawdzarka losuje 20 000 liczb i sprawdza, czy wypadło 1 i 1000. def wyswietl(self):
for i in range(self.__liczba_elementow):
print(f"{i}: {self.__tablica[i]}")
Sprawdzenie od razu pod klasą (po sprawdzeniu usuń):
Tablica(5).wyswietl()
range(self.__liczba_elementow), a nie len(self.__tablica)
Arkusz mówi wprost: „wszystkie operacje są ograniczone wartością tego pola”. W C++ tablica ma stały rozmiar, a liczba faktycznie użytych elementów bywa mniejsza. W Pythonie obie liczby są dziś równe, ale egzaminator ocenia, czy pętla korzysta z pola. Ta sama zasada obowiązuje w każdej kolejnej metodzie. def szukaj(self, wartosc):
for i in range(self.__liczba_elementow):
if self.__tablica[i] == wartosc:
return i
return -1
Zapytaj ucznia, zanim cokolwiek uruchomi: „tablica to [3, 8, 5, 8], szukamy 8. Ile razy wykona się pętla i co zwróci metoda?” Odpowiedź: dwa obroty, wynik 1. return kończy metodę od razu, dlatego wychodzi pierwsze wystąpienie, a nie ostatnie.
else: return -1 w pętli
Metoda kończy się po pierwszym elemencie: znajduje tylko to, co stoi pod indeksem 0. return -1 stoi po pętli, na tym samym poziomie wcięcia co for.znaleziony = i bez przerwania pętli nadpisuje wynik przy każdym kolejnym trafieniu i metoda zwraca ostatnie wystąpienie. Sprawdzarka łapie to na tablicy, w której 7 stoi pod indeksami 0 i 2. def nieparzyste(self):
ile = 0
for i in range(self.__liczba_elementow):
if self.__tablica[i] % 2 != 0:
print(self.__tablica[i])
ile += 1
return ile
def srednia(self):
suma = 0
for i in range(self.__liczba_elementow):
suma += self.__tablica[i]
return suma / self.__liczba_elementow
| Metoda | Wypisuje | Zwraca |
|---|---|---|
wyswietl | wszystkie elementy | nic |
szukaj | nic | indeks albo -1 |
nieparzyste | liczby nieparzyste | ich liczbę |
srednia | nic | średnią |
Tę tabelę uczeń powinien umieć wyrecytować. Każdy wiersz to inne zdanie z arkusza.
//
Podwójny ukośnik obcina część ułamkową, a średnia 525,34 zamienia się w 525. Na obrazie 1 średnia akurat wyszła całkowita, co łatwo wziąć za wskazówkę. To przypadek, nie wymaganie.sum(self.__tablica)?
Liczy całą listę, nie pierwsze liczba_elementow elementów. Wersja z pętlą spełnia wymaganie o ograniczeniu i pokazuje, że uczeń umie zsumować tablicę bez gotowej funkcji.Na dole pliku, bez wcięcia:
tablica = Tablica(50)
print("Elementy tablicy:")
tablica.wyswietl()
szukana = 100
indeks = tablica.szukaj(szukana)
if indeks != -1:
print(f"Liczba {szukana} jest w tablicy pod indeksem {indeks}")
print("Liczby nieparzyste:")
ile_nieparzystych = tablica.nieparzyste()
print(f"Razem nieparzystych: {ile_nieparzystych}")
print(f"Średnia wszystkich elementów: {tablica.srednia():.2f}")
Elementy tablicy:
0: 626
1: 743
...
49: 588
Liczby nieparzyste:
743
...
Razem nieparzystych: 23
Średnia wszystkich elementów: 516.54
input()
Obraz 1 pokazuje program, który o nic nie pyta. Stała wartość daje też jeden zrzut ekranu zamiast kilku. Żeby pokazać oba przypadki wyszukiwania, uruchom program kilka razy: przy 50 losowych liczbach 100 trafia się mniej więcej co dwudzieste uruchomienie. Na zrzut wystarczy dowolny przebieg.Tablica(50), nie Tablica(20)
„Większy od 20” wyklucza samo 20.Sprawdzarka podstawia do obiektu własną tablicę [7, 4, 7, 10, 3, 8, 1000, 1, 2, 5] z liczbą elementów 10, a za nią dokłada w liście dwie liczby 999, których żadna metoda nie może zobaczyć. Tak sprawdza wymaganie „operacje ograniczone wartością pola”, którego gołym okiem nie da się zweryfikować.
- Pobierz
sprawdz_tablice.pydo folderu zmain.py. - W terminalu IDE:
python sprawdz_tablice.py main.py # Windows
python3 sprawdz_tablice.py main.py # macOS
2. Klasa Tablica - pola i konstruktor
OK pole „tablica” niedostępne poza klasą i w klasach potomnych
...
3. Metody na podstawionej tablicy [7, 4, 7, 10, 3, 8, 1000, 1, 2, 5], liczba elementów 10
OK szukaj(7) zwraca 0
OK szukaj(999) zwraca -1
...
Wynik: 29/29 wymagań spełnionych
Po 29/29 uczeń celowo psuje jedną rzecz: zamienia self.__tablica na self._tablica w całym pliku. Sprawdzarka zgłasza dwa błędy i wyjaśnia, dlaczego jedno podkreślenie nie wystarcza.
a) Nagłówek metody według listingu 1. Arkusz wymaga nagłówka „dowolnej metody”. Wybieramy szukaj, bo ma parametr i coś zwraca, więc każde pole wzoru ma treść:
def szukaj(self, wartosc):
"""
nazwa metody: szukaj
opis metody: przegląda tablicę od początku i zwraca indeks
pierwszego elementu równego szukanej wartości
parametry: wartosc - liczba całkowita, której szukamy w tablicy
zwracany typ i opis: int - indeks pierwszego wystąpienia wartości
albo -1, gdy wartości nie ma w tablicy
autor: 00000000000
"""
self nie jest opisywany jako parametr: to obiekt, na którym wywołujemy metodę, a nie argument przekazywany przez program główny. W polu autor numer zdającego, czyli PESEL.
b) Zrzut ekranu konsola1.png w dokumentacja: cały ekran z paskiem zadań, okno z wynikiem programu i środowisko z projektem w jednym kadrze. Program nie prowadzi interakcji, więc jeden zrzut wystarcza.
c) Plik egzamin w edytorze pakietu biurowego (Word albo LibreOffice Writer, nie Notatnik):
System operacyjny: Windows 11 Pro
Środowiska programistyczne: PyCharm Community, Visual Studio Code
Języki programowania: Python, JavaScript (React)
d) Folder konsolowa: konsola.zip z całym folderem projektu i skopiowany main.py. Python nie tworzy pliku wykonywalnego, więc go nie dorabiasz. sprawdz_tablice.py usuń z projektu przed pakowaniem.
Klasa z tablicą — Gotowy kod
Kompletne rozwiązanie części I. Przechodzi sprawdzarkę 29/29. Kod pokazujesz po samodzielnej próbie ucznia.
main.py
import random
class Tablica:
def __init__(self, rozmiar):
self.__liczba_elementow = rozmiar
self.__tablica = []
for i in range(rozmiar):
self.__tablica.append(random.randint(1, 1000))
def wyswietl(self):
for i in range(self.__liczba_elementow):
print(f"{i}: {self.__tablica[i]}")
def szukaj(self, wartosc):
"""
nazwa metody: szukaj
opis metody: przegląda tablicę od początku i zwraca indeks
pierwszego elementu równego szukanej wartości
parametry: wartosc - liczba całkowita, której szukamy w tablicy
zwracany typ i opis: int - indeks pierwszego wystąpienia wartości
albo -1, gdy wartości nie ma w tablicy
autor: 00000000000
"""
for i in range(self.__liczba_elementow):
if self.__tablica[i] == wartosc:
return i
return -1
def nieparzyste(self):
ile = 0
for i in range(self.__liczba_elementow):
if self.__tablica[i] % 2 != 0:
print(self.__tablica[i])
ile += 1
return ile
def srednia(self):
suma = 0
for i in range(self.__liczba_elementow):
suma += self.__tablica[i]
return suma / self.__liczba_elementow
tablica = Tablica(50)
print("Elementy tablicy:")
tablica.wyswietl()
szukana = 100
indeks = tablica.szukaj(szukana)
if indeks != -1:
print(f"Liczba {szukana} jest w tablicy pod indeksem {indeks}")
print("Liczby nieparzyste:")
ile_nieparzystych = tablica.nieparzyste()
print(f"Razem nieparzystych: {ile_nieparzystych}")
print(f"Średnia wszystkich elementów: {tablica.srednia():.2f}")
Jak to czytać
- Konstruktor zapamiętuje rozmiar w prywatnym polu i wypełnia prywatną listę losowymi liczbami.
- Każda metoda chodzi pętlą od 0 do
liczba_elementow - 1. Wyświetlające używająprint, liczące kończą sięreturn. - Program główny tylko woła metody i podpisuje ich wyniki.
Wariant na powtórkę w domu
Dopisz metodę parzyste_wieksze_od(prog), która wyświetla parzyste elementy większe od progu i zwraca ich sumę, oraz jej nagłówek według listingu 1. W programie głównym wywołaj ją dla progu 500. Jeśli uczeń pisze w nowej metodzie for x in self.__tablica, wróć do kroku 3.
Klasa z tablicą — Sprawdzian
5 pytań · 5 minut · minimum 60%, żeby zdać
szukaj ma w pętli if ...: return i oraz else: return -1. Co się stanie?Rozgrzewka — Loteria
Pięć pytań na start. Każde dotyczy jednej linijki dzisiejszego programu. Kliknij pytanie, żeby zobaczyć odpowiedź.
1. Co zwraca 5 in [3, 5, 8], a co 4 in [3, 5, 8]?
True i False. Operator in sprawdza, czy wartość jest w liście. Dziś pilnuje, żeby liczba nie trafiła do zestawu drugi raz.
2. Ile razy wykona się pętla while len(zestaw) < 6, jeśli losowanie dwa razy trafi liczbę, która już jest w zestawie?
8 razy. for i in range(6) wykonałby się zawsze 6 razy i przy powtórce zestaw miałby 5 liczb. while kręci się, dopóki zestaw nie jest pełny, niezależnie od liczby powtórek.
3. Co zawiera [0] * 50?
Listę 50 zer, z indeksami od 0 do 49. Dziś to licznik: wystapienia[7] mówi, ile razy padła siódemka. Indeks 0 zostaje nieużywany, za to numer liczby jest od razu jej indeksem.
4. Jak wygląda lista dwuwymiarowa 2 × 3 i jak odczytać jej prawy dolny element?
t = [[1, 2, 3], [4, 5, 6]], a prawy dolny element to t[1][2], czyli 6. Pierwszy indeks to wiersz, drugi kolumna. U nas wiersz to jedno losowanie, kolumna to jedna z sześciu liczb.
5. Dlaczego kod programu głównego chowamy w def main():?
Zmienna utworzona na poziomie pliku jest globalna, a arkusz w podejściu strukturalnym zabrania zmiennych globalnych. W main() zmienne są lokalne, a funkcje dostają dane wyłącznie przez parametry i oddają je przez return.
Loteria — Instrukcja
Cały egzamin to aplikacja konsolowa, aplikacja desktopowa (wzornik kolorów RGB) i dokumentacja w 180 minut. Do tego arkusza nie ma archiwum z materiałami: obie części robi się od zera. Cel: część I razem z dokumentacją w 30 minut.
Treść zadania
Program loterii liczbowej polegającej na losowaniu zestawów składających się z sześciu liczb.
- Język obiektowy zainstalowany na stanowisku: C++, C#, Java albo Python
- Program wczytuje z klawiatury, ile zestawów należy wylosować
- Generuje tyle zestawów po sześć liczb całkowitych z zakresu 1–49; w jednym zestawie liczby nie mogą się powtarzać; zestawy są wyświetlane
- Zlicza we wszystkich zestawach, ile jest wystąpień każdej liczby od 1 do 49, i wyświetla wynik
- Użyta funkcja do generowania liczb pseudolosowych
- Wyniki w tablicy dwuwymiarowej n × 6 (n wczytane z klawiatury) albo innej kolekcji pasującej do problemu
- Co najmniej dwie funkcje: wypełniająca kolekcję danymi losowań i wyświetlająca wszystkie losowania
- W podejściu strukturalnym nie można stosować zmiennych globalnych
- Zrozumiała komunikacja, znaczące nazwy, czytelny kod
- W folderze
konsolowa:konsola.zip, plik z kodem źródłowym i plik wykonywalny, jeżeli istnieje
Obraz 1 z arkusza: „Ile wygenerować losowań?”, odpowiedź 11, „Zestawy wylosowanych liczb:”, wiersze „Losowanie 1: 18 31 49 4 47 11” aż do „Losowanie 11”, potem „Wystąpienia liczby 1: 1” i dalej do 49.
Pliki: sprawdz_loteria.py, czyli sprawdzarka z kroku 7. Arkusz w PDF: arkusze.pl, zasady oceniania: arkusze.pl.
Kryteria CKE i gdzie są w kodzie
| Kryterium z zasad oceniania | Gdzie w programie | Jak łatwo to stracić |
|---|---|---|
| 2.1 Program główny i co najmniej dwie funkcje; dane nie giną po wyjściu z funkcji losującej, bez zmiennych globalnych | losuj_zestawy kończy się return zestawy, program główny w main() | funkcja wypełnia listę zadeklarowaną na górze pliku |
| 2.2 Tablica dwuwymiarowa albo kolekcja o dynamicznym rozmiarze | lista list zestawy | stała tablica na 100 losowań |
| 2.3 Generator pseudolosowy, zakres 1–49 | random.randint(1, 49) | — (CKE toleruje ±1) |
| 2.4 Pętla obsługuje rzeczywistą liczbę losowań | for i in range(ile_zestawow) | pętla po stałej liczbie |
| 2.5 Wyświetlanie pętlą, wszystkie losowania | wyswietl_zestawy | wypisanie całej listy jednym print(zestawy) |
| 2.6 Losowanie zestawu bez powtórzeń | while + if liczba not in zestaw | for sześć razy bez sprawdzania |
| 2.7 Zliczone i wyświetlone wystąpienia 1–49 | zlicz_wystapienia, wyswietl_wystapienia | licznik od 0 do 48 i przesunięte wyniki |
| 4.1–4.5 Nagłówek dokumentacji | docstring pod def losuj_zestawy | brak opisu parametru albo imię zamiast numeru zdającego |
Loteria — Budowa krok po kroku
Uczeń pisze u siebie i udostępnia ekran. Ty pytasz „co teraz?” i nie dyktujesz.
00000000000/ <- numer zdającego (PESEL), na ćwiczeniach dowolny
├── konsolowa/
├── desktopowa/
└── dokumentacja/
Tym razem druga część to aplikacja desktopowa, więc folder nazywa się desktopowa, nie webowa. Projekt konsola, w nim main.py.
import random
def main():
ile_zestawow = int(input("Ile wygenerować losowań? "))
print(ile_zestawow)
main()
main()
Wszystko, co napiszesz na poziomie pliku (bez wcięcia), jest zmienną globalną. Arkusz ich zabrania, a kryterium 2.1 sprawdza, czy dane z funkcji losującej przetrwały dzięki return, a nie dzięki globalnej liście. Zaczynając od main(), nie trzeba potem niczego przenosić.Najpierw jeden zestaw, sprawdzony ręcznie, potem dopiero ich lista:
zestaw = []
while len(zestaw) < 6:
liczba = random.randint(1, 49)
if liczba not in zestaw:
zestaw.append(liczba)
Zapytaj ucznia: „wylosowało się 7, 12, 7. Co jest w zestawie po trzech obrotach i ile obrotów jeszcze zostało?” Odpowiedź: [7, 12], druga siódemka odpadła, pętla kręci się dalej, aż będzie 6 liczb.
for i in range(6) bez sprawdzania
Daje 6 liczb, ale czasem z powtórką, mniej więcej w co czwartym zestawie. Na ekranie trudno to zauważyć, a kryterium 2.6 przepada.random.sample(range(1, 50), 6)?
Robi dokładnie to w jednej linii, ale kryterium 2.6 brzmi „program implementuje losowanie bez powtórzeń”. Wersja z pętlą nie zostawia egzaminatorowi pola do interpretacji. Sprawdzarka zgłasza sample jako błąd.Zestaw z kroku 3 trafia do pętli, a gotowe zestawy do listy, którą funkcja zwraca. Funkcja stoi nad main():
def losuj_zestawy(ile_zestawow):
zestawy = []
for i in range(ile_zestawow):
zestaw = []
while len(zestaw) < 6:
liczba = random.randint(1, 49)
if liczba not in zestaw:
zestaw.append(liczba)
zestawy.append(zestaw)
return zestawy
W main(): zestawy = losuj_zestawy(ile_zestawow). Lista list to tablica dwuwymiarowa n × 6: zestawy[0] to pierwsze losowanie, zestawy[0][5] jego szósta liczba.
zestaw = [] przed pętlą for
Wtedy pierwszy zestaw się zapełnia, a w każdym kolejnym obrocie while od razu kończy się, bo lista ma już 6 elementów. Do zestawy trafia n razy ta sama lista. Nowy zestaw zaczyna się w każdym obrocie pętli zewnętrznej.def wyswietl_zestawy(zestawy):
print("Zestawy wylosowanych liczb:")
for numer in range(len(zestawy)):
wiersz = f"Losowanie {numer + 1}:"
for liczba in zestawy[numer]:
wiersz += f" {liczba}"
print(wiersz)
numer + 1, bo na obrazie 1 losowania są numerowane od 1, a indeksy listy od 0. Kryterium 2.5 wymaga wyświetlania pętlą, więc print(zestawy), które wypisze całą listę z nawiasami, go nie spełnia.
def zlicz_wystapienia(zestawy):
wystapienia = [0] * 50
for zestaw in zestawy:
for liczba in zestaw:
wystapienia[liczba] += 1
return wystapienia
def wyswietl_wystapienia(wystapienia):
for liczba in range(1, 50):
print(f"Wystąpienia liczby {liczba}: {wystapienia[liczba]}")
def main():
ile_zestawow = int(input("Ile wygenerować losowań? "))
zestawy = losuj_zestawy(ile_zestawow)
wyswietl_zestawy(zestawy)
wystapienia = zlicz_wystapienia(zestawy)
wyswietl_wystapienia(wystapienia)
[0] * 50, a nie [0] * 49
Lista 49 zer ma indeksy 0–48, więc wystapienia[49] kończy się IndexError, ale dopiero wtedy, gdy padnie 49. Program działa raz tak, raz nie. Z 50 pozycjami numer liczby jest od razu indeksem, a pozycja 0 po prostu zostaje pusta.Ile wygenerować losowań? 11
Zestawy wylosowanych liczb:
Losowanie 1: 19 33 43 39 21 14
...
Losowanie 11: 33 37 2 24 12 44
Wystąpienia liczby 1: 2
Wystąpienia liczby 2: 3
...
Sprawdzarka losuje funkcją ucznia 2000 zestawów i sprawdza każdy: 6 liczb, bez powtórzeń, zakres 1–49. Potem uruchamia program dla 11 i dla 1 i porównuje wyświetlone wystąpienia z wyświetlonymi zestawami.
python sprawdz_loteria.py main.py # Windows
python3 sprawdz_loteria.py main.py # macOS
2. Funkcje
OK co najmniej dwie funkcje poza programem głównym (...)
OK brak zmiennych globalnych
OK liczby w zestawie się nie powtarzają
...
Wynik: 16/16 wymagań spełnionych
Po 16/16 uczeń zamienia while na for i in range(6) bez if i patrzy, jak sprawdzarka znajduje zestaw z powtórką.
a) Nagłówek pod def losuj_zestawy, bo ma parametr i zwraca wartość:
def losuj_zestawy(ile_zestawow):
"""
nazwa funkcji: losuj_zestawy
opis funkcji: losuje podaną liczbę zestawów; każdy zestaw to 6 różnych
liczb całkowitych z zakresu od 1 do 49
parametry: ile_zestawow - liczba zestawów do wylosowania
zwracany typ i opis: list - lista zestawów (n wierszy po 6 liczb)
autor: 00000000000
"""
b) Zrzut konsola1.png: cały ekran z paskiem zadań, okno z wynikiem programu i środowisko z projektem.
c) Plik egzamin w edytorze pakietu biurowego: system operacyjny, środowiska programistyczne, języki programowania.
d) Folder konsolowa: konsola.zip z całym projektem i skopiowany main.py, bez sprawdzarki.
Loteria — Gotowy kod
Kompletne rozwiązanie części I. Przechodzi sprawdzarkę 16/16.
main.py
import random
def losuj_zestawy(ile_zestawow):
"""
nazwa funkcji: losuj_zestawy
opis funkcji: losuje podaną liczbę zestawów; każdy zestaw to 6 różnych
liczb całkowitych z zakresu od 1 do 49
parametry: ile_zestawow - liczba zestawów do wylosowania
zwracany typ i opis: list - lista zestawów (n wierszy po 6 liczb)
autor: 00000000000
"""
zestawy = []
for i in range(ile_zestawow):
zestaw = []
while len(zestaw) < 6:
liczba = random.randint(1, 49)
if liczba not in zestaw:
zestaw.append(liczba)
zestawy.append(zestaw)
return zestawy
def wyswietl_zestawy(zestawy):
print("Zestawy wylosowanych liczb:")
for numer in range(len(zestawy)):
wiersz = f"Losowanie {numer + 1}:"
for liczba in zestawy[numer]:
wiersz += f" {liczba}"
print(wiersz)
def zlicz_wystapienia(zestawy):
wystapienia = [0] * 50
for zestaw in zestawy:
for liczba in zestaw:
wystapienia[liczba] += 1
return wystapienia
def wyswietl_wystapienia(wystapienia):
for liczba in range(1, 50):
print(f"Wystąpienia liczby {liczba}: {wystapienia[liczba]}")
def main():
ile_zestawow = int(input("Ile wygenerować losowań? "))
zestawy = losuj_zestawy(ile_zestawow)
wyswietl_zestawy(zestawy)
wystapienia = zlicz_wystapienia(zestawy)
wyswietl_wystapienia(wystapienia)
main()
Wariant na powtórkę w domu
Dopisz funkcję, która zwraca liczbę, która padła najczęściej (przy remisie najmniejszą), i wyświetl ją w main(). Bez max() na liście: pętla po indeksach 1–49.
Loteria — Sprawdzian
5 pytań · 5 minut · minimum 60%, żeby zdać
[0] * 49. Co się stanie?zestawy = [[5, 9, 1, 30, 22, 41], [7, 3, 48, 11, 2, 19]]. Ile wynosi zestawy[1][2]?📝 Ściąga Python
Zmienne & typy
x = 10 # int
y = 3.14 # float
s = "tekst" # str
b = True # bool
l = [1, 2, 3] # list
d = {"k": "v"} # dictf-string
f"Cześć {imie}"
f"{cena:.2f} zł"
f"{nr:05d}"if / elif / else
if x > 0:
print("dodatnia")
elif x == 0:
print("zero")
else:
print("ujemna")for + range
for i in range(5):
print(i)
for el in lista:
print(el)
for i, el in enumerate(l):
print(i, el)Funkcja
def suma(a, b=0):
return a + bLista — metody
l.append(x)
l.insert(0, x)
l.remove(x)
l.pop()
l.sort()
sorted(l, key=...)Słownik
d = {"imie": "Jan"}
d["imie"]
d.get("x", "?")
d.keys()
d.values()
d.items()Plik — czytanie
with open("f.txt", "r",
encoding="utf-8") as f:
for linia in f:
linia = linia.strip()
parts = linia.split(";")Plik — zapis
with open("f.txt", "w",
encoding="utf-8") as f:
f.write("tekst\n")SQLite — tworzenie
import sqlite3
conn = sqlite3.connect("db.db")
c = conn.cursor()
c.execute("""CREATE TABLE IF
NOT EXISTS t (id INTEGER PRIMARY KEY,
nazwa TEXT, cena REAL)""")
conn.commit()SQLite — INSERT
c.execute(
"INSERT INTO t VALUES (?,?,?)",
(1, "Laptop", 3500.0)
)
conn.commit()SQLite — SELECT
c.execute("SELECT * FROM t
WHERE kategoria = ?
ORDER BY cena DESC",
("elektronika",))
rows = c.fetchall()PyQt6 — Qt Creator
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())PyQt6 — self.ui. widżety
# pyuic6 mainwindow.ui -o mainwindow.py
self.ui.label.setText("X")
self.ui.lineEdit.text()
self.ui.pushButton.clicked.connect(self.fn)
self.ui.listWidget.addItem("el")
self.ui.comboBox.currentText()List comprehension
[x**2 for x in range(5)]
[x for x in l if x > 0]Konwersja typów
int("42")
float("3.14")
str(100)
bool(0) # False🐛 Typowe błędy Python
# ❌ Mieszanie tab i spacji
if True:
print("tab") # ← TAB
print("space") # ← 4 spacje → BŁĄD
# ✅ Zawsze 4 spacje
if True:
print("ok")# ❌
print(imie) # imie nie istnieje
# ✅
imie = "Maks"
print(imie)# ❌ input() zwraca string!
wiek = input("Wiek: ")
print(wiek + 1) # "18" + 1 → TypeError
# ✅
wiek = int(input("Wiek: "))
print(wiek + 1) # 19# ❌ Zły folder lub nazwa pliku
open("Dane.txt") # a plik to dane.txt (małe d!)
# ✅ Sprawdź: plik w tym samym folderze co .py
# ✅ Nazwy case-sensitive na Mac/Linux!# ❌
l = [1, 2, 3]
print(l[3]) # IndexError! (indeksy 0, 1, 2)
# ✅
print(l[2]) # 3
print(l[-1]) # 3 (ostatni)# ❌ Brak przecinka — to nie tuple!
cursor.execute("SELECT * WHERE k=?", ("elektronika")) # Error
# ✅ Przecinek tworzy tuple
cursor.execute("SELECT * WHERE k=?", ("elektronika",))# ❌ INSERT działa, ale dane nie zapisane na dysk
cursor.execute("INSERT INTO t VALUES (?,?)", (1, "X"))
# brak commit → dane znikną po zamknięciu
# ✅
cursor.execute("INSERT INTO t VALUES (?,?)", (1, "X"))
conn.commit() # ← ZAPISZ!# ❌ ImportError: No module named 'mainwindow'
from mainwindow import Ui_MainWindow # plik nie istnieje!
# ✅ Wygeneruj plik z formularza .ui:
# pyuic6 mainwindow.ui -o mainwindow.py
# Upewnij się, że mainwindow.ui jest zapisany w Qt Creator# ❌ app.exec() PRZED konfiguracją
window.show()
app.exec()
ui.btnFiltruj.clicked.connect(fn) # za późno!
# ✅ app.exec() na SAMYM KOŃCU
ui.btnFiltruj.clicked.connect(fn)
window.show()
app.exec()# ❌ .sort() zwraca None!
wynik = lista.sort() # wynik = None
# ✅ sorted() zwraca NOWĄ listę
wynik = sorted(lista) # wynik = posortowana kopia
# ✅ .sort() mutuje oryginalną
lista.sort() # lista jest teraz posortowana🎓 Matura z informatyki rozszerzonej — jak to działa
Zanim wejdziemy w zadania — zrozum egzamin jako całość. Matura rozszerzona z informatyki to przepustka na kierunki IT (informatyka, automatyka, AI) na uczelniach technicznych. Liczy się jako przedmiot dodatkowy — im wyższy wynik, tym więcej punktów rekrutacyjnych.
Format egzaminu (Formuła 2023)
| Parametr | Wartość |
|---|---|
| Czas | 210 minut (3,5 godziny) |
| Punkty | 50 |
| Liczba zadań | ~8 zadań głównych, podzielonych na podpunkty |
| Forma | Jeden arkusz: część „bez komputera" (papier) + część „przy komputerze" |
| Języki programowania | C++, Java, Python (wybierasz jeden) |
| Oprogramowanie na sali | Środowisko do programowania, arkusz kalkulacyjny (Excel/Calc), baza danych (SQL) |
| Internet | Brak — komputer odcięty od sieci |
Pięć działów — i ile są warte
Rozkład punktów jest dość stały rok do roku. Z arkusza maj 2026:
| Dział | Waga | Charakter |
|---|---|---|
| Programowanie (napisy, struktury, pliki) | ~34% | Kod czytający pliki z danymi (setki–tysiące wierszy) |
| Algorytmika i rekurencja | ~24% | Analiza algorytmu na papierze + pisanie |
| Bazy danych (SQL) | ~18% | Zapytania na 3 powiązanych tabelach |
| Arkusz kalkulacyjny + modelowanie | ~18% | Formuły, wykresy, symulacje |
| Teoria (systemy, sieci, logika) | ~6% | Krótkie zadania na wiedzę |
Jak wygląda dzień egzaminu
- Dostajesz arkusz papierowy + folder z plikami danych na komputerze (np.
pary.txt,klienci.txt) - Część zadań rozwiązujesz na papierze (algorytm, teoria, analiza), część przy komputerze (kod, SQL, arkusz)
- Pliki z rozwiązaniami (kod źródłowy, plik arkusza, zapytania SQL) zapisujesz w wskazanym folderze pod określoną nazwą
- Oceniany jest zapisany plik — pamiętaj zapisywać często (brak autozapisu = stracone punkty przy zawieszeniu)
Strategia na sali (210 minut)
- Najpierw przejrzyj cały arkusz (5 min) — zaznacz zadania które od razu umiesz.
- Zbierz pewne punkty — teoria (systemy, sieci) i proste podpunkty SQL/Excel. Szybkie, gwarantowane.
- Potem zadania programistyczne — najwięcej punktów, ale czasochłonne. Zrób podpunkty od najłatwiejszego (zwykle .1 przed .3).
- Podpunkty są niezależne — jeśli nie umiesz 3.3, zrób 3.1 i 3.2. Punkty się sumują.
- Zapisuj co 10 minut i nazywaj pliki dokładnie tak jak każe polecenie.
- Pilnuj czasu — orientacyjnie ~4 min na punkt. Nie utknij 40 min na jednym podpunkcie za 2p.
Co oznacza wynik (orientacyjnie)
| Wynik | Interpretacja |
|---|---|
| 30–40% | Próg na mniej oblężone kierunki |
| 50–60% | Solidny — większość kierunków IT |
| 70%+ | Mocny — czołowe uczelnie (PW, AGH, UJ) |
| 85%+ | Bardzo wysoki — pewne wejście wszędzie |
Następny krok
Przejdź do Arkusz CKE → „Maj 2026 — omówienie" — tam masz realny arkusz rozłożony zadanie po zadaniu, z pełnymi rozwiązaniami w Pythonie, SQL i arkuszu kalkulacyjnym. To pokazuje czego konkretnie się uczysz. Potem teoria działami (Systemy liczbowe → … → Teoria informacji) i ćwiczenia.
📄 Arkusz CKE — Maj 2026 (rozszerzona)
Pełne omówienie arkusza, z którego zdawali maturzyści w maju 2026 (Formuła 2023). Jeden arkusz, 210 minut, 50 punktów. Część zadań „bez komputera" (algorytm na papierze, teoria), część „przy komputerze" (kod, SQL, arkusz kalkulacyjny). To twoja mapa — z czego dokładnie będziesz zdawał.
Rozkład punktów — na czym się skupić
| Dział | Punkty | Waga |
|---|---|---|
| Programowanie (napisy, struktury, pliki) | 17 | 34% |
| Algorytmika i rekurencja | 12 | 24% |
| Bazy danych (SQL) | 9 | 18% |
| Arkusz kalkulacyjny + modelowanie | 9 | 18% |
| Teoria (systemy, sieci) | 3 | 6% |
Załączniki — pliki z danymi (folder Dane-NF-2605)
Na egzaminie dostajesz folder z plikami wejściowymi do zadań. Oto co realnie w nich jest:
| Plik | Wierszy | Struktura | Zadanie |
|---|---|---|---|
pary.txt | 500 | dwa słowa w wierszu (spacja), litery a–d, max 50 | 3 |
korpo.txt | 50 000 | liczba/wiersz = nr przełożonego pracownika i; 0 = prezes | 4 |
staw.txt | 365 dni | TAB: Data, Temp, Opady | 7 |
klienci.txt | 2045 | TAB: IdKlienta, Imie, Nazwisko, Plec (K/M) | 8 |
transakcje.txt | 1000 | TAB: IdTransakcji, DataTransakcji, IdKlienta, IdSklepu, IdSprzedawcy | 8 |
opis_transakcji.txt | 1466 | TAB: IdTransakcji, IdProduktu, Cena, Liczba | 8 |
- pary.txt — litery to a, b, c i d. Nie zakładaj alfabetu z głowy.
- staw.txt — temperatura z polskim przecinkiem (
9,8). W Pythoniefloat("9,8")wybucha — trzebafloat(x.replace(",", ".")). - transakcje.txt — 163 transakcje mają puste IdSprzedawcy = kasy samoobsługowe. To klucz do zadania 8.3! Po
split("\t")to"", nieNone. - opis_transakcji.txt — wiele wierszy na jedną transakcję (pozycje koszyka). Kwota = suma
Cena × Liczba. - korpo.txt — drzewo zapisane jako tablica rodziców. Pracownik
ijest w liniii(od 1). - Wszędzie separator TAB (
\t) pozapary.txt(spacja). Pliki SQL-owe mają nagłówek do pominięcia.
Zadanie 1 — Rekurencja [7 pkt, papier]
Dana funkcja: A(m,n) = m gdy n=1; A(2m, n/2) gdy n parzyste; 2·A(m, (n−1)/2) + m gdy n nieparzyste.
A(m, n) = m · n — ta funkcja to po prostu mnożenie metodą binarną. Dowód indukcyjny: baza A(m,1)=m=m·1; parzyste A(2m,n/2)=(2m)(n/2)=mn; nieparzyste 2·m·(n−1)/2 + m = m(n−1)+m = mn. Liczba wywołań = liczba kroków sprowadzających n do 1 ≈ ⌊log₂n⌋.
1.1 — tabela wywołań (krok po kroku)
„Wywołanie rekurencyjne" = każde wejście do funkcji A z wnętrza A. Wywołanie startowe (np. A(3,9)) nie liczy się — liczymy dopiero te, które ono uruchamia. Śledzimy drugi argument n i stosujemy reguły: parzyste → n/2, nieparzyste → (n−1)/2, aż dojdziemy do n=1 (baza).
m=2⁵=32, n=2⁵=32 — n parzyste cały czas, więc tylko dzielenie przez 2 (a m się podwaja):
A(32,32) → A(64,16) → A(128,8) → A(256,4) → A(512,2) → A(1024,1)
(1) (2) (3) (4) (5) = baza
Drugi argument: 16, 8, 4, 2, 1 → 5 wywołań.
m=10, n=15 — n nieparzyste, więc (n−1)/2; m się nie zmienia (w regule nieparzystej pierwszy argument zostaje):
15 → (15−1)/2 = 7 → (7−1)/2 = 3 → (3−1)/2 = 1
(1) (2) (3) = baza
Wywołania: A(10,7), A(10,3), A(10,1) → 3 wywołania.
m=1, n=2¹⁰⁰+1 — najpierw jeden krok nieparzysty: (2¹⁰⁰+1−1)/2 = 2¹⁰⁰/2 = 2⁹⁹. Potem 2⁹⁹ to czyste potęgi dwójki — dzielimy przez 2 aż do 2⁰=1. Drugie argumenty kolejnych wywołań: 2⁹⁹, 2⁹⁸, …, 2¹, 2⁰. Wykładniki od 99 do 0 to 100 liczb → 100 wywołań.
1.2 — wartości (dlaczego tak)
Tu nie liczymy wywołań ręcznie — używamy wzoru A(m,n)=m·n (udowodnionego wyżej). To po prostu mnożenie:
A(1, 777) = 1 · 777 = 777A(2·10⁶, 256·10⁶) = 2·10⁶ · 256·10⁶ = (2·256) · 10⁶⁺⁶ = 512·10¹²
Gdybyś NIE zauważył wzoru, musiałbyś dla 256·10⁶ wykonać ~28 wywołań ręcznie (256·10⁶ ≈ 2²⁸) — dlatego rozpoznanie „to jest mnożenie" jest tu kluczowe i oszczędza mnóstwo czasu.
1.3 — liczba wywołań i i-ty argument (uogólnienie 1.1)
To samo co w 1.1, tylko zapisane wzorem dla dowolnego k. Wzorzec z przykładu n=8: drugie argumenty to 4, 2, 1 = 8/2¹, 8/2², 8/2³, czyli i-ty argument = 8/2ⁱ = 2³⁻ⁱ, a wywołań jest 3.
n = 2ᵏ — same potęgi dwójki, dzielimy przez 2: drugie argumenty to 2ᵏ⁻¹, 2ᵏ⁻², …, 2⁰. To k wywołań, a i-ty argument = 2ᵏ⁻ⁱ (= 2ᵏ/2ⁱ). Sprawdź dla k=3 (n=8): 3 wywołania, 2³⁻ⁱ ✓.
n = 2ᵏ−1 — liczba nieparzysta, więc reguła (n−1)/2. Ciekawostka: (2ᵏ−1−1)/2 = (2ᵏ−2)/2 = 2ᵏ⁻¹−1 — znów liczba tej samej postaci, o jeden wykładnik mniej! Sekwencja: 2ᵏ⁻¹−1, 2ᵏ⁻²−1, …, 2¹−1=1 (baza). Wykładniki od k−1 do 1 → k−1 wywołań, i-ty argument = 2ᵏ⁻ⁱ−1. Sprawdź dla k=3 (n=7): 7→3→1 = 2 wywołania = k−1 ✓.
| n | liczba wywołań | i-ty drugi argument |
|---|---|---|
| 8 | 3 | 2³⁻ⁱ |
| 2ᵏ | k | 2ᵏ⁻ⁱ |
| 2ᵏ−1 | k−1 | 2ᵏ⁻ⁱ−1 |
Zadanie 2 — Liczenie przeniesień [5 pkt]
O co chodzi: gdy dodajesz dwie liczby „pisemnie" (jak w szkole — kolumna po kolumnie od prawej), czasem suma w kolumnie wynosi 10 lub więcej. Wtedy piszesz ostatnią cyfrę, a 1 „przenosisz" do następnej kolumny. Zadanie: policzyć ile razy nastąpi takie przeniesienie.
Najpierw zrozum na papierze (przykład z arkusza: 27732 + 72619)
Dodajemy kolumnami od prawej. „p" = powstaje przeniesienie:
pozycja: 5 4 3 2 1 (od prawej)
a: 2 7 7 3 2
b: 7 2 6 1 9
─────────────────────────────
jedności: 2+9 = 11 → piszę 1, przenoszę 1 ← przeniesienie #1
dziesiątki:3+1 +1(p)= 5 → bez przeniesienia
setki: 7+6 = 13 → przeniesienie ← #2
tysiące: 7+2 +1(p)= 10 → przeniesienie ← #3
dzies.tys.:2+7 +1(p)= 10 → przeniesienie ← #4
─────────────────────────────
RAZEM: 4 przeniesienia ✓
Zwróć uwagę: przeniesienie z poprzedniej kolumny dolicza się do następnej (dlatego 3+1+1=5, a 7+2+1=10).
2.1 — odpowiedzi (rozpisane kolumnami)
88765 + 11111: każda kolumna: 5+1=6, 6+1=7, 7+1=8, 8+1=9, 8+1=9 — żadna suma nie sięga 10. → 0 przeniesień (wynik 99876).
456789 + 222222:
9+2=11 → przeniesienie #1
8+2+1=11 → #2
7+2+1=10 → #3
6+2+1=9 → bez
5+2=7 → bez
4+2=6 → bez
→ 3 przeniesienia (wynik 679011)
2.2 — algorytm (bez tablic i stringów, tylko arytmetyka)
Klucz: jak „wyciągnąć" cyfry liczby bez zamiany na tekst? Dwoma operatorami:
n % 10→ ostatnia cyfra (reszta z dzielenia przez 10). Np.27732 % 10 = 2.n // 10→ liczba bez ostatniej cyfry (dzielenie całkowite). Np.27732 // 10 = 2773.
Powtarzając te dwie operacje, „zjadamy" liczbę cyfra po cyfrze od prawej — dokładnie tak jak dodawanie pisemne.
def przeniesienia(a, b):
carry = 0 # czy z poprzedniej kolumny przyszło przeniesienie (0/1)
count = 0 # licznik przeniesień
while a > 0 or b > 0: # dopóki w którejkolwiek liczbie zostały cyfry
s = a % 10 + b % 10 + carry # suma cyfr w tej kolumnie + ew. przeniesienie
if s >= 10:
carry = 1 # jest przeniesienie do następnej kolumny
count += 1 # zliczamy je
else:
carry = 0 # nie ma przeniesienia
a //= 10 # obcinamy ostatnią cyfrę a
b //= 10 # obcinamy ostatnią cyfrę b
return count
Linia po linii: w każdym obrocie pętli bierzemy ostatnią cyfrę a (a%10) i b (b%10), dodajemy do nich przeniesienie z poprzedniej kolumny. Jeśli suma ≥10 — ustawiamy carry=1 i zliczamy. Potem //10 przesuwa nas o kolumnę w lewo. Pętla działa while a>0 OR b>0 (nie AND!), bo liczby mogą mieć różną długość — kończymy gdy obie się „wyczerpią".
% i //), a nie „obejść" zadanie zamieniając liczbę na napis i czytając znaki.Zadanie 3 — Pary słów [9 pkt, pary.txt]
O co chodzi: w pliku pary.txt jest 500 wierszy, w każdym dwa słowa (litery a–z, oddzielone spacją). Dla każdego podpunktu przechodzimy wszystkie pary i czegoś szukamy.
Wczytanie pliku — czytamy linia po linii, dzielimy po spacji na dwa słowa:
pary = []
with open("pary.txt") as f:
for linia in f:
a, b = linia.split() # "bcba babb" → a="bcba", b="babb"
pary.append((a, b))
3.1 — największa różnica sum kodów ASCII
Każda litera ma swój kod ASCII (numer w tablicy znaków): a=97, b=98, c=99, …, z=122. W Pythonie ord('a') daje 97. f(s) = suma kodów wszystkich liter słowa.
Przykład z arkusza: dla słów oko i pies:
f("oko") = ord('o')+ord('k')+ord('o') = 111+107+111 = 329
f("pies") = ord('p')+ord('i')+ord('e')+ord('s') = 112+105+101+115 = 433
|329 − 433| = 104
Szukamy pary, dla której ta różnica (wartość bezwzględna) jest największa. Lecimy przez wszystkie 500 par i zapamiętujemy maksimum:
def suma_ascii(s):
return sum(ord(z) for z in s) # suma kodów liter słowa
maks = 0
wynik = None
for a, b in pary:
roznica = abs(suma_ascii(a) - suma_ascii(b))
if roznica > maks:
maks = roznica
wynik = (a, b) # zapamiętujemy też PARĘ, nie tylko liczbę
print(wynik, maks)
3.2 — suma wspólnych wystąpień liter
Dla każdej litery liczymy ile razy występuje w obu słowach i bierzemy mniejszą z tych dwóch liczb (to „wspólna" liczba wystąpień). Sumujemy po wszystkich literach.
Przykład z arkusza: para adabbcdde / aadabbbccdc:
litera a: w 1. słowie 2 razy, w 2. słowie 3 razy → min(2,3)=2
litera b: 2 i 3 → min=2
litera c: 1 i 3 → min=1
litera d: 3 i 2 → min=2
litera e: 1 i 0 → min=0
suma wspólnych = 2+2+1+2+0 = 7
Counter(s) z biblioteki collections automatycznie zlicza litery (Counter("aab") → {'a':2,'b':1}). set(a) & set(b) to litery wspólne dla obu słów:
from collections import Counter
maks = 0
wynik = None
for a, b in pary:
ca, cb = Counter(a), Counter(b)
wspolne = sum(min(ca[z], cb[z]) for z in set(a) & set(b))
if wspolne > maks:
maks = wspolne
wynik = (a, b)
print(wynik, maks)
3.3 — najdłuższy prefiksosufiks ≥ 5
Prefiksosufiks pary to słowo, które jest początkiem (prefiksem) jednego słowa i jednocześnie końcem (sufiksem) drugiego — i to w obie strony.
Przykład z arkusza: para abbaabaa / baabaabba:
"abba" — początek 1. słowa (abbaabaa) i koniec 2. słowa (...aabba) → długość 4
"baabaa" — początek 2. słowa (baabaabba) i koniec 1. słowa (...baabaa) → długość 6
najdłuższy ma długość 6 → para spełnia warunek (≥5)
Sprawdzamy oba kierunki (prefiks a = sufiks b ORAZ prefiks b = sufiks a), dla każdej długości k. a[:k] = pierwsze k liter, b[-k:] = ostatnie k liter:
def pref_suf(a, b):
best = 0
for k in range(1, min(len(a), len(b)) + 1):
if a[:k] == b[-k:]: # początek a = koniec b
best = max(best, k)
if b[:k] == a[-k:]: # początek b = koniec a
best = max(best, k)
return best
for a, b in pary:
if pref_suf(a, b) >= 5:
print(a, b, pref_suf(a, b))
- 3.1 → para
gpeeazeugmvsbzwsrxfplqdbakoxxe/lhpbmoirdm, różnica 2206 - 3.2 → para
aacbcccaacacbcabac/cccccaaaacaccbabcba, suma 18 - 3.3 → 7 par z prefiksosufiksem ≥5 (m.in.
aababbbababbbbbbaab / bbbbaabbababababa → 7,aaaababaaaabbbb / aabbbbbabbbaaaa → 6,caabbccabccc / cabccccabbaac → 6; reszta po 5)
Zadanie 4 — Korporacja / drzewo [8 pkt, korpo.txt]
O co chodzi: plik korpo.txt ma 50 000 wierszy. Wiersz numer i zawiera numer przełożonego pracownika i. Wartość 0 oznacza prezesa (nie ma nad sobą nikogo). To jest drzewo — tylko zapisane nietypowo: każdy „wskazuje w górę" na swojego szefa.
Przykład (mała firma, 6 osób):
wiersz 1: 0 ← pracownik 1 to prezes (szef = 0)
wiersz 2: 1 ← pracownik 2 podlega pod 1
wiersz 3: 1 ← pracownik 3 podlega pod 1
wiersz 4: 3 ← pracownik 4 podlega pod 3
wiersz 5: 3 ← pracownik 5 podlega pod 3
wiersz 6: 3 ← pracownik 6 podlega pod 3
1 (prezes)
/ \
2 3
/|\
4 5 6
Wczytanie — chcemy by szef[i] = przełożony pracownika i. Pracownicy numerowani od 1, więc na indeks 0 wkładamy „zaślepkę":
szef = [0] # indeks 0 nieużywany (pracownicy od 1)
with open("korpo.txt") as f:
for linia in f:
szef.append(int(linia))
n = len(szef) - 1 # liczba pracowników
4.2 — liczba „liści" (szeregowych)
Liść = pracownik, który nie jest niczyim szefem — czyli jego numer nigdy nie pojawia się jako wartość w pliku. W przykładzie: 2, 4, 5, 6 (nikt pod nimi nie podlega) = 4 liście.
Counter(szef[1:]) zlicza ile razy każdy numer występuje jako szef = ilu ma bezpośrednich podwładnych. Kto ma 0 → jest liściem:
from collections import Counter
ile_podwladnych = Counter(szef[1:]) # ile razy każdy jest czyimś szefem
liscie = sum(1 for i in range(1, n+1) if ile_podwladnych[i] == 0)
print(liscie)
4.3 — kto ma najwięcej bezpośrednich podwładnych
To po prostu numer, który najczęściej pojawia się jako wartość w pliku (najczęstszy szef). Korzystamy z tego samego Counter — szukamy klucza o największej wartości (pomijając 0, bo to „brak przełożonego", nie pracownik):
del ile_podwladnych[0] # 0 to nie pracownik, tylko znacznik prezesa
najlepszy = max(ile_podwladnych, key=ile_podwladnych.get)
print(najlepszy, ile_podwladnych[najlepszy])
4.4 — maksymalna liczba przełożonych (głębokość drzewa)
Dla pracownika liczymy, ilu ma nad sobą przełożonych: jego szef, szef jego szefa, … aż do prezesa. To „głębokość" w drzewie. Szukamy największej takiej liczby w całej firmie.
Naturalnie robi to rekurencja „w górę": głębokość(i) = 1 + głębokość(szefa i), a prezes (szef=0) ma głębokość 0. ALE przy 50 000 pracowników liczenie tego osobno dla każdego byłoby wolne (te same ścieżki liczone wielokrotnie). Dlatego memoizacja — raz policzoną głębokość zapamiętujemy w tablicy glebokosc i nie liczymy drugi raz:
glebokosc = [0] * (n + 1) # pamięć policzonych wyników
def policz(i):
if szef[i] == 0: # prezes — nie ma przełożonych
return 0
if glebokosc[i] == 0: # jeszcze nie liczone → policz i zapamiętaj
glebokosc[i] = 1 + policz(szef[i])
return glebokosc[i]
print(max(policz(i) for i in range(1, n+1)))
Zadanie 5 — Systemy liczbowe [2 pkt, papier]
Równanie z arkusza: 1440₅ + …₅ = 427₁₀ = …₃ − 110002₃ (dolny indeks = podstawa systemu). Trzeba wpisać dwie liczby w miejsca kropek.
1440₅ czytamy jako1·5³ + 4·5² + 4·5¹ + 0·5⁰ = 1·125 + 4·25 + 4·5 + 0 = 245 (w dziesiętnym).To jak w dziesiętnym, gdzie 1440 = 1·1000+4·100+4·10+0 — tylko zamiast dziesiątek mamy piątki.
Strategia: wszystko przeliczamy na zwykły system dziesiętny, rozwiązujemy jak równanie, a wynik konwertujemy z powrotem na żądany system.
Lewa równość 1440₅ + X₅ = 427₁₀ — szukamy X w systemie piątkowym:
1) zamień 1440₅ na dziesiętny: 1·125 + 4·25 + 4·5 + 0 = 245
2) z równania: 245 + X = 427 → X = 182 (dziesiętnie)
3) zamień 182 z powrotem na piątkowy (dziel przez 5, spisuj reszty):
182 : 5 = 36 r 2
36 : 5 = 7 r 1
7 : 5 = 1 r 2
1 : 5 = 0 r 1
reszty od dołu: 1 2 1 2 → X = 1212₅
sprawdzenie: 1·125+2·25+1·5+2 = 182 ✓
Prawa równość 427₁₀ = Y₃ − 110002₃ — szukamy Y w systemie trójkowym:
1) zamień 110002₃ na dziesiętny: 1·243 + 1·81 + 0 + 0 + 0 + 2 = 326
2) z równania: 427 = Y − 326 → Y = 753 (dziesiętnie)
3) zamień 753 na trójkowy (dziel przez 3, spisuj reszty):
753 : 3 = 251 r 0
251 : 3 = 83 r 2
83 : 3 = 27 r 2
27 : 3 = 9 r 0
9 : 3 = 3 r 0
3 : 3 = 1 r 0
1 : 3 = 0 r 1
reszty od dołu: 1 0 0 0 2 2 0 → Y = 1000220₃
sprawdzenie: 1·729 + 2·9 + 2·3 = 753 ✓
Zadanie 6 — Sieci [1 pkt, papier]
Adres IP to „numer" urządzenia w sieci. Są dwie wersje:
- IPv4 = 32 bity (4 bajty, np.
192.168.0.1— 4 liczby po 8 bitów). Daje ~4 mld adresów — to się skończyło. - IPv6 = 128 bitów (16 bajtów, np.
2001:0db8::1). Stworzony bo IPv4 się wyczerpał — daje praktycznie nieskończenie wiele adresów.
To czysta wiedza do zapamiętania — 32 i 128 bitów. Pewny punkt za 0 wysiłku obliczeniowego.
Zadanie 7 — Staw / rzęsa wodna [9 pkt, staw.txt, arkusz kalkulacyjny]
Staw 10 000 m². Plik staw.txt = dane pogodowe całego 2022 (Data, Temp z przecinkiem, Opady).
- 7.1 — średnie temperatury miesięczne + wykres kolumnowy. Kolumna pomocnicza z miesiącem (
=MIESIĄC(A2)), potemŚREDNIA.JEŻELIlub tabela przestawna. - 7.2 — najdłuższy ciąg kolejnych dni bez opadów w każdym miesiącu. Kolumna licznika:
=JEŻELI(Opady=0; poprzedni+1; 0), potemMAXper miesiąc.
Model do 7.3–7.4 (od 1 marca 2023, 184 dni, stałe warunki): wzrost 1,75%/noc, start 2000 m² (20%), 80 amurów × 0,25 m²/dzień = 20 m²/dzień zjadane, dodatkowo co piątek −60 m². Pomiar rano, wzrost w nocy. Rekurencja na każdy kolejny ranek:
P_jutro = (P_dziś − amury·0,25 − (piątek? 60 : 0)) × 1,0175
- 7.3 — pierwszy dzień (licząc od 1 marca), gdy rzęsa > 75% (7500 m²).
- 7.4 — najmniejsza liczba amurów, by w całym okresie rzęsa zajmowała maks. 50% (5000 m²).
ZASTĄP.Zadanie 8 — Sieć sklepów [9 pkt, SQL]
O co chodzi: trzy powiązane tabele. klienci (kto), transakcje (kto, gdzie, kiedy, który sprzedawca), opis_transakcji (co kupiono w danej transakcji — produkt, cena, ilość). Łączą się przez wspólne Id (IdKlienta, IdTransakcji).
SELECT … FROM tabela— wybierz kolumny z tabeliWHERE warunek— tylko wiersze spełniające warunekGROUP BY X— zgrupuj wiersze o tym samym X i licz na grupachCOUNT(*)= ile wierszy,SUM(…)= suma,COUNT(DISTINCT X)= ile różnych XORDER BY … DESC+LIMIT 1— posortuj malejąco i weź pierwszy (czyli „największy")JOIN B ON …— dołącz dane z drugiej tabeli po wspólnym kluczu
8.1 — klient z największą liczbą transakcji (pyta o imię i nazwisko, więc JOIN do klienci):
SELECT k.Imie, k.Nazwisko, COUNT(*) AS ile
FROM transakcje t
JOIN klienci k ON k.IdKlienta = t.IdKlienta
GROUP BY t.IdKlienta
ORDER BY ile DESC
LIMIT 1;
Czytanie: grupujemy transakcje po kliencie → COUNT(*) liczy ile transakcji ma każdy → sortujemy malejąco → bierzemy pierwszego. JOIN dokleja imię i nazwisko z tabeli klienci.
8.2 — ile kobiet i mężczyzn NIC nie kupiło:
SELECT Plec, COUNT(*) AS ilu
FROM klienci
WHERE IdKlienta NOT IN (SELECT DISTINCT IdKlienta FROM transakcje)
GROUP BY Plec;
Czytanie: wewnętrzne SELECT (podzapytanie) daje listę klientów którzy COŚ kupili. NOT IN wybiera tych, których na tej liście NIE ma (nic nie kupili). GROUP BY Plec rozbija wynik na K i M.
8.3 — ile RÓŻNYCH sklepów z kasą samoobsługową + łączna kwota (kasa samoobsługowa = puste IdSprzedawcy):
-- liczba różnych sklepów
SELECT COUNT(DISTINCT IdSklepu)
FROM transakcje WHERE IdSprzedawcy = '';
-- łączna kwota = suma (cena × ilość) z pozycji tych transakcji
SELECT SUM(o.Cena * o.Liczba)
FROM transakcje t
JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
WHERE t.IdSprzedawcy = '';
Czytanie: kwoty są w opis_transakcji (cena i ilość każdej pozycji), a info „która kasa" w transakcje — dlatego JOIN. COUNT(DISTINCT IdSklepu) liczy różne sklepy (jeden sklep może mieć wiele transakcji, ale liczymy go raz).
8.4 — sprzedawca w największej liczbie różnych sklepów W JEDNYM MIESIĄCU + ten miesiąc. Data ma format dd.mm.rrrr, więc miesiąc to znaki 4–5 (SUBSTR(data,4,2)):
SELECT IdSprzedawcy, SUBSTR(DataTransakcji, 4, 2) AS miesiac,
COUNT(DISTINCT IdSklepu) AS sklepy
FROM transakcje
WHERE IdSprzedawcy <> ''
GROUP BY IdSprzedawcy, miesiac
ORDER BY sklepy DESC
LIMIT 1;
Czytanie: grupujemy po dwóch rzeczach naraz (sprzedawca + miesiąc), bo pytanie brzmi „w jednym miesiącu". Dla każdej takiej pary liczymy ile różnych sklepów obsłużył, sortujemy i bierzemy najlepszego. <> '' odrzuca kasy samoobsługowe (brak sprzedawcy).
8.5 — produkty z kategorii „spożywcze" z opisem „do ekspresu kolbowego", które ktoś kupił (JOIN trzech tabel):
SELECT DISTINCT p.IdProduktu, p.Nazwa
FROM Produkty p
JOIN Kategorie k ON k.IdKategorii = p.IdKategorii
JOIN opis_transakcji o ON o.IdProduktu = p.IdProduktu
WHERE k.NazwaKategorii = 'spozywcze'
AND p.Opis LIKE '%do ekspresu kolbowego%';
Czytanie: łączymy produkt z jego kategorią (żeby filtrować „spożywcze") i z opisem zakupów (żeby wziąć tylko kupione). LIKE '%…%' szuka fragmentu w tekście (procenty = „cokolwiek przed i po"). DISTINCT usuwa duplikaty (produkt mógł być kupiony wiele razy).
- 8.1 → Marcelino Kruk (8 transakcji)
- 8.2 → 689 kobiet, 651 mężczyzn bez zakupów
- 8.3 → 28 różnych sklepów, łącznie 19 443,29 zł
- 8.4 → sprzedawca 14, miesiąc styczeń (01) — 3 różne sklepy
NOT IN (8.2), COUNT(DISTINCT) + SUM (8.3), grupowanie po dwóch kolumnach (8.4), JOIN 3 tabel + LIKE (8.5).Szablony wczytywania danych (Python)
# Plik TAB z nagłówkiem (klienci, transakcje, opis_transakcji, staw)
with open("klienci.txt", encoding="utf-8") as f:
naglowek = f.readline() # pomiń nagłówek
for linia in f:
pola = linia.rstrip("\n").split("\t")
# Temp z przecinkiem dziesiętnym (staw)
temp = float(pola[1].replace(",", "."))
# Puste pole (transakcje — IdSprzedawcy = kasa samoobsługowa)
if pola[4] == "":
...
# Plik spacjowy (pary)
a, b = linia.split()
# Plik jednokolumnowy (korpo)
szef.append(int(linia))
🐍 Maj 2026 — gotowy kod Python
Kompletne, uruchamialne rozwiązania zadań programistycznych z arkusza maj 2026. Każdy program czyta odpowiedni plik z folderu z danymi (Dane-NF-2605) i wypisuje odpowiedź. Wszystkie wyniki są zweryfikowane na realnych załącznikach.
.py w tym samym folderze, w którym leżą pliki danych (np. pary.txt), i odpal w terminalu: python zad3_pary.py. Zadania teoretyczne (1, 5, 6) i SQL (8) mają rozwiązania w zakładce „Maj 2026 — omówienie".
Zadanie 2 — przeniesienia (zad2.py)
Operuje wyłącznie na liczbach całkowitych (%, //), bez tablic i napisów — zgodnie z zakazem w treści.
# Zadanie 2 — liczba przeniesień przy dodawaniu pisemnym a + b
def przeniesienia(a, b):
carry = 0 # przeniesienie z poprzedniej kolumny (0 lub 1)
count = 0 # licznik przeniesień
while a > 0 or b > 0:
s = a % 10 + b % 10 + carry # suma cyfr w kolumnie + przeniesienie
if s >= 10:
carry = 1
count += 1
else:
carry = 0
a //= 10 # obetnij ostatnią cyfrę
b //= 10
return count
# przykład z arkusza
print("27732 + 72619 =>", przeniesienia(27732, 72619)) # 4
# Zadanie 2.1
print("37932 + 12528 =>", przeniesienia(37932, 12528)) # 3
print("88765 + 11111 =>", przeniesienia(88765, 11111)) # 0
print("456789 + 222222 =>", przeniesienia(456789, 222222)) # 3
Zadanie 3 — pary słów (zad3.py)
Czyta pary.txt, zapisuje odpowiedzi do wyniki3.txt (tak jak wymaga arkusz) i pokazuje je na ekranie.
# Zadanie 3 — pary słów (pary.txt)
from collections import Counter
pary = []
with open("pary.txt", encoding="utf-8") as f:
for linia in f:
a, b = linia.split()
pary.append((a, b))
# 3.1 — maks. |suma_ASCII(s1) - suma_ASCII(s2)|
def suma_ascii(s):
return sum(ord(z) for z in s)
best31 = (-1, None)
for a, b in pary:
roznica = abs(suma_ascii(a) - suma_ascii(b))
if roznica > best31[0]:
best31 = (roznica, (a, b))
# 3.2 — maks. suma wspólnych wystąpień liter: sum( min(d(x,s1), d(x,s2)) )
best32 = (-1, None)
for a, b in pary:
ca, cb = Counter(a), Counter(b)
wspolne = sum(min(ca[x], cb[x]) for x in set(a) | set(b))
if wspolne > best32[0]:
best32 = (wspolne, (a, b))
# 3.3 — pary, których najdłuższy prefiksosufiks ma >= 5 liter (obie strony)
def pref_suf(a, b):
best = 0
for k in range(1, min(len(a), len(b)) + 1):
if a[:k] == b[-k:]: # prefiks a = sufiks b
best = max(best, k)
if b[:k] == a[-k:]: # prefiks b = sufiks a
best = max(best, k)
return best
wyniki33 = [(a, b, pref_suf(a, b)) for a, b in pary if pref_suf(a, b) >= 5]
# zapis do wyniki3.txt
with open("wyniki3.txt", "w", encoding="utf-8") as out:
out.write("3.1 {} {} {}\n".format(best31[1][0], best31[1][1], best31[0]))
out.write("3.2 {} {} {}\n".format(best32[1][0], best32[1][1], best32[0]))
out.write("3.3\n")
for a, b, d in wyniki33:
out.write(" {} {} {}\n".format(a, b, d))
print("3.1:", best31[1], "->", best31[0])
print("3.2:", best32[1], "->", best32[0])
print("3.3:", len(wyniki33), "par")
Zadanie 4 — korporacja / drzewo (zad4.py)
Czyta korpo.txt (drzewo jako tablica rodziców). Memoizacja, bo 50 000 pracowników.
# Zadanie 4 — korporacja (korpo.txt)
import sys
from collections import Counter
szef = [0] # szef[i] = przełożony pracownika i (indeks 0 nieużywany)
with open("korpo.txt", encoding="utf-8") as f:
for linia in f:
szef.append(int(linia))
n = len(szef) - 1
ile_podwladnych = Counter(szef[1:]) # ile razy każdy jest czyimś szefem
# 4.2 — liście (nikt ich nie wskazuje jako szefa)
liscie = sum(1 for i in range(1, n + 1) if ile_podwladnych[i] == 0)
# 4.3 — najwięcej bezpośrednich podwładnych
ile = dict(ile_podwladnych)
ile.pop(0, None) # 0 = znacznik prezesa, pomijamy
najlepszy = max(ile, key=ile.get)
# 4.4 — maksymalna liczba przełożonych (głębokość) z memoizacją
sys.setrecursionlimit(100000)
glebokosc = [0] * (n + 1)
def policz(i):
if szef[i] == 0:
return 0
if glebokosc[i] == 0:
glebokosc[i] = 1 + policz(szef[i])
return glebokosc[i]
print("4.2 liście:", liscie)
print("4.3 najwięcej podwładnych: pracownik", najlepszy, "(", ile[najlepszy], ")")
print("4.4 maks. głębokość:", max(policz(i) for i in range(1, n + 1)))
Zadanie 7.3 / 7.4 — staw (zad7.py)
To zadanie z arkusza kalkulacyjnego, ale ten sam model w Pythonie jest świetny do weryfikacji. Skrypt sam sprawdza checkpoint 30 kwietnia (powinno wyjść 25,79% — wtedy model jest poprawny).
# Zadanie 7.3 i 7.4 — symulacja stawu
from datetime import date, timedelta
TOTAL, START, GROWTH, EAT, FRIDAY, DAYS = 10000, 2000.0, 0.0175, 0.25, 60, 184
def symulacja(amury):
P = START
d = date(2023, 3, 1)
log = []
for dzien in range(1, DAYS + 1):
log.append((dzien, d, P)) # pomiar rano
rem = amury * EAT + (FRIDAY if d.weekday() == 4 else 0) # 4 = piątek
P = (P - rem) * (1 + GROWTH) # wzrost w nocy
if P < 0:
P = 0
d += timedelta(days=1)
return log
log = symulacja(80)
po_30iv = next(P for (_, dt, P) in log if dt == date(2023, 4, 30))
print("Walidacja 30 IV:", round(po_30iv / TOTAL * 100, 2), "% (arkusz: 25,79%)")
# 7.3 — pierwszy dzień > 75%
for dzien, dt, P in log:
if P > 0.75 * TOTAL:
print("7.3: dzień", dzien, "(", dt, ",", round(P / TOTAL * 100, 2), "% )")
break
# 7.4 — najmniejsza liczba amurów, by zarost <= 50% przez cały okres
def max_zarost(amury):
return max(P for (_, _, P) in symulacja(amury))
a = 80
while max_zarost(a) > 0.50 * TOTAL:
a += 1
print("7.4: min amurów =", a, "( max", round(max_zarost(a) / TOTAL * 100, 2), "% )")
Zadanie 8 — SQL (gotowe zapytania)
Zadanie 8 robi się w bazie (SQLite/MySQL), nie w Pythonie. Gotowe zapytania:
-- 8.1: klient z największą liczbą transakcji (imię i nazwisko)
SELECT k.Imie, k.Nazwisko, COUNT(*) AS ile
FROM transakcje t JOIN klienci k ON k.IdKlienta = t.IdKlienta
GROUP BY t.IdKlienta ORDER BY ile DESC LIMIT 1;
-- 8.2: ile K i M nic nie kupiło
SELECT Plec, COUNT(*) FROM klienci
WHERE IdKlienta NOT IN (SELECT DISTINCT IdKlienta FROM transakcje)
GROUP BY Plec;
-- 8.3: ile różnych sklepów z kasą samoobsługową + łączna kwota
SELECT COUNT(DISTINCT IdSklepu) FROM transakcje WHERE IdSprzedawcy = '';
SELECT SUM(o.Cena * o.Liczba)
FROM transakcje t JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
WHERE t.IdSprzedawcy = '';
-- 8.4: sprzedawca w najwięcej różnych sklepach w jednym miesiącu
SELECT IdSprzedawcy, SUBSTR(DataTransakcji,4,2) AS miesiac,
COUNT(DISTINCT IdSklepu) AS sklepy
FROM transakcje WHERE IdSprzedawcy <> ''
GROUP BY IdSprzedawcy, miesiac ORDER BY sklepy DESC LIMIT 1;
-- 8.5: produkty spożywcze "do ekspresu kolbowego", które kupiono
SELECT DISTINCT p.IdProduktu, p.Nazwa
FROM Produkty p
JOIN Kategorie k ON k.IdKategorii = p.IdKategorii
JOIN opis_transakcji o ON o.IdProduktu = p.IdProduktu
WHERE k.NazwaKategorii = 'spozywcze'
AND p.Opis LIKE '%do ekspresu kolbowego%';
📘 Maj 2026 — Zadanie 2 (dodawanie): pełny przewodnik
Wyczerpujący przewodnik do Zadania 2 z arkusza maj 2026 — od intuicji „co to jest przeniesienie" po wyprowadzenie algorytmu CKE i ćwiczenia z odpowiedziami. Materiał do przejścia z uczniem od deski do deski.
Wprowadzenie
Zadanie 2 z arkusza maja 2026 jest "papierowe" tylko z pozoru. CKE testuje, czy potrafisz rozłożyć szkolną mechanikę dodawania na cyfry i zapisać ją jako pętlę. Pod spodem to ten sam mechanizm, który procesor wykonuje miliardy razy na sekundę w pełnym sumatorze bitowym. Jeśli zrozumiesz przeniesienie w systemie dziesiętnym, to samo zrozumiesz w binarnym, szesnastkowym i każdym innym.
Dodawanie pisemne w trzech zdaniach
Ustawiasz liczby jedna pod drugą tak, żeby cyfry jedności były w tej samej kolumnie. Dodajesz kolumna po kolumnie od prawej do lewej. Jeżeli suma cyfr w danej kolumnie wynosi 10 lub więcej, zapisujesz pod kreską tylko jej ostatnią cyfrę (czyli suma mod 10), a 1 przekazujesz w lewo do następnej kolumny. Ta przekazana jedynka to właśnie przeniesienie (ang. carry).
Mikroprzykład 1: 47 + 8
Przeniesienie: 1
Liczba a: 4 7
Liczba b: + 8
-----
Wynik: 5 5
Kolumna jedności: 7 + 8 = 15. Pod kreską piszemy 5, a 1 wędruje w lewo. Kolumna dziesiątek: 4 + 0 + 1 (przeniesienie) = 5. Mamy 1 przeniesienie.
Mikroprzykład 2: 56 + 44
Przeniesienie: 1 1
Liczba a: 5 6
Liczba b: + 4 4
-----
Wynik: 1 0 0
Jedności: 6 + 4 = 10, piszemy 0, przeniesienie 1. Dziesiątki: 5 + 4 + 1 = 10, piszemy 0, przeniesienie 1. Trafia w pustą setkę, gdzie nie ma już żadnych cyfr do dodania, więc 1 ląduje jako nowa cyfra wyniku. Mamy 2 przeniesienia.
Dlaczego CKE w ogóle to liczy
Dodawanie pisemne to najprostsza wersja pełnego sumatora, który na poziomie sprzętu składa się z bramek logicznych. Przeniesienie w dziesiętnym odpowiada carry bit w binarnym i to ono jest źródłem integer overflow, gdy suma dwóch int32 przekracza pojemność rejestru. Każdy, kto pisał kiedyś kryptografię, arytmetykę dużych liczb albo debugował overflow w C, robił to samo co Ty w tym zadaniu, tylko na bitach.
123 + 456), może być równa liczbie cyfr (gdy każda kolumna przekracza, np. 999 + 999), a w skrajnym przypadku może być o jeden większa niż liczba cyfr każdego ze składników, jeśli ostatnie przeniesienie wytworzy nową, dodatkową cyfrę wyniku (jak 56 + 44 = 100). Dla pary a, b o tej samej liczbie cyfr n, liczba przeniesień zawsze mieści się w przedziale 0..n.
1 do przekazania w lewo, a nie ile cyfr ma końcowy wynik, nie ile jedynek widać na samej górze ilustracji, nie suma wartości przeniesień. Liczymy zdarzenia, nie cyfry.
Przyklad krok po kroku: 27732 + 72619
Dodawanie pisemne wykonujemy kolumna po kolumnie, idąc od prawej do lewej. W każdej kolumnie liczymy sumę: cyfra z liczby a, cyfra z liczby b oraz przeniesienie wniesione z kolumny poprzedniej (carry in). Jeśli suma w kolumnie jest większa lub równa 10, generujemy przeniesienie do kolumny następnej (carry out = 1) i zapisujemy cyfrę wynikową suma mod 10. W przeciwnym wypadku carry out = 0, a cyfrą wynikową jest cała suma.
Numerujemy kolumny od prawej: kolumna 1 to jedności, kolumna 2 to dziesiątki, i tak dalej. Litera P w ostatniej kolumnie tabeli oznacza, że w tej kolumnie powstało przeniesienie do kolumny następnej.
kol | a | b | c_in | suma | cyfra | c_out | P?
-----+-----+-----+------+------+-------+-------+----
1 | 2 | 9 | 0 | 11 | 1 | 1 | P
2 | 3 | 1 | 1 | 5 | 5 | 0 | -
3 | 7 | 6 | 0 | 13 | 3 | 1 | P
4 | 7 | 2 | 1 | 10 | 0 | 1 | P
5 | 2 | 7 | 1 | 10 | 0 | 1 | P
-----+-----+-----+------+------+-------+-------+----
razem P = 4
Sklejając cyfry wynikowe od lewej (z dopisanym ostatnim carry out jako najstarszą cyfrą wyniku) dostajemy: 1 0 0 3 5 1, czyli 100351. Zgadza się z arytmetyką: 27732 + 72619 = 100351.
Liczymy wiersze oznaczone literą P: kolumna 1, 3, 4 oraz 5. Daje to dokładnie 4 przeniesienia, zgodnie z odpowiedzią z arkusza.
i > 1 zachodzi tożsamość:
carry_in(i) = carry_out(i - 1)
Przeniesienie wyjściowe z kolumny poprzedniej staje się przeniesieniem wejściowym do kolumny bieżącej. Dla kolumny pierwszej (jedności) zawsze carry_in(1) = 0. Ta zależność jest fundamentem algorytmu z zadania 2.2: w pętli wystarczy pamiętać jedną zmienną c (bieżące przeniesienie), nadpisywać ją w każdym kroku wartością carry_out i zliczać, ile razy przyjmie wartość 1 jako wyjście z kolumny.
Zadanie 2.1 - pelne rozwiazania
Przeniesienie powstaje w kolumnie wtedy, gdy suma cyfr tej kolumny powiększona o przeniesienie z kolumny poprzedniej jest większa lub równa 10. Sprawdzamy każdą kolumnę osobno, idąc od prawej do lewej.
s = a[i] + b[i] + carry_in. Jeśli s >= 10, to carry_out = 1 (i zwiększamy licznik przeniesień), w przeciwnym razie carry_out = 0.
Wiersz 1: 37932 + 12528 (przykład z arkusza)
Kolumna: 5 4 3 2 1
Przeniesienie: 1 1 1
a: 3 7 9 3 2
b: + 1 2 5 2 8
-----------------
Wynik: 5 0 4 6 0
Kolumna 1 (jednosci): 2 + 8 + 0 = 10 -> wynik 0, carry 1 [PRZENIESIENIE]
Kolumna 2 (dziesiatki): 3 + 2 + 1 = 6 -> wynik 6, carry 0
Kolumna 3 (setki): 9 + 5 + 0 = 14 -> wynik 4, carry 1 [PRZENIESIENIE]
Kolumna 4 (tysiace): 7 + 2 + 1 = 10 -> wynik 0, carry 1 [PRZENIESIENIE]
Kolumna 5 (dz. tys.): 3 + 1 + 1 = 5 -> wynik 5, carry 0
Wiersz 2: 88765 + 11111
Kolumna: 5 4 3 2 1
Przeniesienie: - - - -
a: 8 8 7 6 5
b: + 1 1 1 1 1
-----------------
Wynik: 9 9 8 7 6
Kolumna 1 (jednosci): 5 + 1 + 0 = 6 -> wynik 6, carry 0
Kolumna 2 (dziesiatki): 6 + 1 + 0 = 7 -> wynik 7, carry 0
Kolumna 3 (setki): 7 + 1 + 0 = 8 -> wynik 8, carry 0
Kolumna 4 (tysiace): 8 + 1 + 0 = 9 -> wynik 9, carry 0
Kolumna 5 (dz. tys.): 8 + 1 + 0 = 9 -> wynik 9, carry 0
Cyfry liczby b to same jedynki, a cyfry liczby a w odpowiadających kolumnach wynoszą maksymalnie 8, więc żadna suma kolumnowa nie osiąga 10.
Wiersz 3: 456789 + 222222
Kolumna: 6 5 4 3 2 1
Przeniesienie: 1 1 1
a: 4 5 6 7 8 9
b: + 2 2 2 2 2 2
---------------------
Wynik: 6 7 9 0 1 1
Kolumna 1 (jednosci): 9 + 2 + 0 = 11 -> wynik 1, carry 1 [PRZENIESIENIE]
Kolumna 2 (dziesiatki): 8 + 2 + 1 = 11 -> wynik 1, carry 1 [PRZENIESIENIE]
Kolumna 3 (setki): 7 + 2 + 1 = 10 -> wynik 0, carry 1 [PRZENIESIENIE]
Kolumna 4 (tysiace): 6 + 2 + 1 = 9 -> wynik 9, carry 0
Kolumna 5 (dz. tys.): 5 + 2 + 0 = 7 -> wynik 7, carry 0
Kolumna 6 (setki tys.): 4 + 2 + 0 = 6 -> wynik 6, carry 0
Trzy najmłodsze kolumny generują przeniesienie kaskadowo: jednostki dają 11, dziesiątki z carry dają 11, setki z carry dają 10. Kolumna tysięcy (6 + 2 + 1 = 9) już nie przekracza progu, więc kaskada się urywa.
Tabela zbiorcza odpowiedzi
| a | b | a + b | Liczba przeniesień |
|---|---|---|---|
| 37932 | 12528 | 50460 | 3 |
| 88765 | 11111 | 99876 | 0 |
| 456789 | 222222 | 679011 | 3 |
7 + 2 = 9) nie dałaby przeniesienia, daje je dopiero carry wpadające z dziesiątek (7 + 2 + 1 = 10). Zawsze licz od prawej do lewej i przekazuj carry w górę.
a wynosi 8, a wszystkie cyfry liczby b to 1, to maksymalna suma kolumnowa wynosi 8 + 1 = 9. Bez wejściowego carry nigdy nie powstanie nowe carry, stąd 0 przeniesień bez konieczności rozpisywania każdej kolumny. Tę intuicję warto mieć w głowie jako szybką weryfikację wyniku.
Zadanie 2.2 - wyprowadzenie algorytmu
Zadanie wymaga napisania algorytmu, który dla dwóch dodatnich liczb całkowitych a i b o tej samej liczbie cyfr policzy, ile razy przy dodawaniu pisemnym pojawi się przeniesienie. Wynik dla 27732 + 72619 to 4. Zanim napiszemy choćby linię kodu, musimy precyzyjnie wiedzieć, czego nam wolno użyć, bo CKE w 2026 mocno zacieśnił dozwolony zestaw operacji.
1. Zakazy CKE
Wolno użyć wyłącznie: operatorów arytmetycznych +, -, *, /, div, mod, porównań, operatorów logicznych, instrukcji sterujących (if, while, for), przypisania oraz własnych funkcji pomocniczych.
Nie wolno użyć: tablic, list, słowników, łańcuchów znaków, funkcji konwersji str() / int(), indeksowania n[i], bibliotek, operatorów bitowych. Każda próba "zamienię liczbę na string i polecę po znakach" daje 0 punktów.
2. Dozwolony zestaw - szybka sciaga
| Kategoria | Co wolno |
|---|---|
| Arytmetyka | + - * /, div (czyli //), mod (czyli %) |
| Porównania | = < > <= >= != |
| Logiczne | and, or, not |
| Sterujące | if, else, while, for |
| Inne | przypisanie <- / =, własne funkcje pomocnicze |
3. Klucz calego zadania - jak wyciagnac cyfre arytmetycznie
Skoro nie wolno traktować liczby jako napisu, jedyna droga do pojedynczej cyfry prowadzi przez dwie operacje:
n mod 10- zwraca ostatnią cyfrę liczby (np.27732 mod 10 = 2)n div 10- obcina ostatnią cyfrę (np.27732 div 10 = 2773)
Łącząc te dwa kroki w pętli, "zjadamy" liczbę od prawej do lewej, dokładnie tak, jak przebiega dodawanie pisemne. Demo na n = 27732:
krok | n | n mod 10 | n div 10
-----+-------+----------+---------
1 | 27732 | 2 | 2773
2 | 2773 | 3 | 277
3 | 277 | 7 | 27
4 | 27 | 7 | 2
5 | 2 | 2 | 0 <- koniec, n stalo sie 0
Pięć kroków, pięć cyfr. Zgadza się. Warunek stopu: pętla działa, dopóki n > 0.
4. Pseudokod naturalny - szkic logiki
Inwariant: w każdej iteracji bierzemy ostatnią cyfrę a, ostatnią cyfrę b, dodajemy do nich aktualne przeniesienie c (0 albo 1). Jeśli suma >= 10, generujemy nowe przeniesienie i zwiększamy licznik. Następnie obcinamy obie liczby przez div 10 i lecimy z następną kolumną.
zaczynamy z carry = 0 i count = 0
dopoki sa jeszcze cyfry do przerobienia:
suma = ostatnia cyfra a + ostatnia cyfra b + carry
jezeli suma >= 10:
nowy carry = 1
count = count + 1
w przeciwnym razie:
nowy carry = 0
obetnij ostatnia cyfre a
obetnij ostatnia cyfre b
zwroc count
5. Pelna implementacja w Pythonie (wersja while)
def przeniesienia(a, b):
carry = 0 # aktualne przeniesienie (0 albo 1)
count = 0 # licznik przeniesien
while a > 0 or b > 0: # patrz nizej: dlaczego "or"
s = a % 10 + b % 10 + carry # suma w aktualnej kolumnie
if s >= 10:
carry = 1 # generujemy przeniesienie
count = count + 1
else:
carry = 0
a = a // 10 # obcinamy ostatnia cyfre a
b = b // 10 # obcinamy ostatnia cyfre b
return count
# Testy
print(przeniesienia(27732, 72619)) # 4
print(przeniesienia(88765, 11111)) # 0
print(przeniesienia(456789, 222222)) # 3
Dlaczego a > 0 or b > 0, a nie and? Zadanie 2.2 gwarantuje równą liczbę cyfr, więc or i and dadzą tu ten sam efekt. Ale or jest bezpieczniejsze i uniwersalniejsze, działa też wtedy, gdy z powodu przeniesień jedna liczba "skończy się" wcześniej niż druga (czego de facto nie ma w 2.2, ale jest np. gdy a = 5, b = 95). and zatrzymałoby pętlę zbyt wcześnie. Egzaminator za or nie odejmie punktu, za and przy nierównych długościach odejmie.
6. Alternatywa - wersja for wykorzystujaca zalozenie "ta sama liczba cyfr"
Skoro arkusz gwarantuje równą długość, możemy najpierw policzyć cyfry i przelecieć pętlę for ustaloną liczbę razy. Wygląda elegancko i jasno zdradza egzaminatorowi, że rozumiemy specyfikację.
def liczba_cyfr(n):
k = 0
while n > 0:
k = k + 1
n = n // 10
return k
def przeniesienia_for(a, b):
k = liczba_cyfr(a) # = liczba_cyfr(b) z zalozenia zadania
carry = 0
count = 0
for _ in range(k):
s = a % 10 + b % 10 + carry
if s >= 10:
carry = 1
count = count + 1
else:
carry = 0
a = a // 10
b = b // 10
return count
print(przeniesienia_for(27732, 72619)) # 4
Obie wersje są poprawne i zdobywają komplet 4 punktów. Wersja while jest krótsza i bardziej uniwersalna. Polecam ją na maturze, jeśli nie czujesz się pewnie w pisaniu funkcji pomocniczej liczba_cyfr.
7. Pseudokod CKE - wersja jezyk-niezalezna
Jeśli wolisz oddać pseudokod (CKE akceptuje), zapisz tak:
funkcja przeniesienia(a, b):
c <- 0
p <- 0
dopoki a > 0:
s <- a mod 10 + b mod 10 + c
jezeli s >= 10:
c <- 1
p <- p + 1
w przeciwnym razie:
c <- 0
a <- a div 10
b <- b div 10
zwroc p
Warunek dopoki a > 0 wystarcza, bo z założenia a i b mają tę samą liczbę cyfr. Gdy a stanie się zerem, b też. Na egzaminie z 2.2 to jest formalnie poprawne.
Zlozonosc i kontrola
| Parametr | Wartość |
|---|---|
| Liczba iteracji | k - liczba cyfr (np. dla 5-cyfrowej liczby: 5 obrotów pętli) |
| Złożoność czasowa | O(k) = O(log10 a) |
| Złożoność pamięciowa | O(1) - tylko trzy zmienne: carry, count, s |
| Brak struktur | żadnej tablicy, żadnego napisu - zgodne z zakazem CKE |
Punktacja CKE (4 pkt): 1 pkt za poprawne ekstraktowanie cyfry przez mod 10 i div 10, 1 pkt za prawidłową obsługę przeniesienia carry, 1 pkt za warunek s >= 10 i inkrementację licznika, 1 pkt za poprawny warunek stopu pętli. Jakakolwiek tablica, str() albo indeksowanie - minus wszystkie 4 punkty.
Pulapki i blednowanie CKE
Zadanie 2.2 wyglada na trywialne: pobierasz cyfry, sumujesz, sledzisz carry. W praktyce CKE rok w rok punktuje ten typ zadania ponizej oczekiwan, bo zdajacy potykaja sie o szczegoly mechaniki dodawania pisemnego, a nie o sam algorytm. Ponizej siedem konkretnych pulapek z kodem ZLY/POPRAWNY i komentarzem do czego prowadza.
27732+72619=4, a polegnie na 99999+11111, tracisz 2-3 z 4 punktow.
1. Carry niezerowane w iteracji bez przeniesienia
Klasyk. Zdajacy ustawia carry := 1 przy przeniesieniu, ale zapomina ustawic carry := 0, gdy go nie ma. W efekcie raz wlaczony carry zostaje na zawsze i licznik p rosnie w kazdej kolejnej iteracji.
# ZLY - carry nie wraca do 0
carry = 0
while a > 0 or b > 0:
s = a % 10 + b % 10 + carry
if s >= 10:
carry = 1
p = p + 1
# brak else: carry = 0 -- BLAD
a = a // 10
b = b // 10
# POPRAWNY
carry = 0
while a > 0 or b > 0:
s = a % 10 + b % 10 + carry
if s >= 10:
carry = 1
p = p + 1
else:
carry = 0
a = a // 10
b = b // 10
Koszt: -2 pkt (algorytm dziala dla losowych liczb, ale dla 12+10 zwroci wiecej przeniesien niz jest).
2. Warunek petli and zamiast or
Specyfikacja mowi "o tej samej liczbie cyfr", wiec teoretycznie and wystarczy. Ale wystarczy, ze zdajacy testuje 99+1 (rozna liczba cyfr) i widzi, ze algorytm sie urywa zanim policzy ostatnie przeniesienie. or jest nawykiem bezpieczniejszym i CKE w kluczu odpowiedzi zwykle akceptuje oba, jednak preferuje or.
# Ryzykowne (formalnie OK dla zalozenia, ale lamie sie poza nim)
while a > 0 and b > 0:
...
# Bezpieczne
while a > 0 or b > 0:
...
3. if s > 10 zamiast if s >= 10
Najczestszy off-by-one w tym zadaniu. Suma 5+5=10 to JUZ przeniesienie w dodawaniu pisemnym: zapisujesz 0, w pamieci masz 1. Jezeli warunkiem jest s > 10, wszystkie pary cyfr sumujace sie dokladnie do 10 sa pomijane.
Test: 55 + 55
1 1 <- powinno byc carry z obu kolumn
5 5
+ 5 5
-----
1 1 0
przeniesienia = 2
Algorytm z s > 10 zwroci 0, a poprawnie 2. Koszt: -2 pkt.
4. Niedoliczenie carry IN do sumy biezacej kolumny
Klasyczny przypadek 9+0=9, ale jezeli z poprzedniej kolumny przyszedl carry=1, to faktyczna suma to 10 i jest kolejne przeniesienie. Zdajacy, ktory liczy tylko a%10 + b%10 bez doliczania carry, gubi calego lancucha przeniesien.
# ZLY - carry ignorowany w warunku
s = a % 10 + b % 10
if s >= 10:
p = p + 1
carry = 1
else:
carry = 0
# POPRAWNY
s = a % 10 + b % 10 + carry
if s >= 10:
p = p + 1
carry = 1
else:
carry = 0
Przyklad demaskujacy blad: 199+101. Bez carry IN algorytm zwroci 1, poprawnie 2.
5. Zla kolejnosc operacji na cyfrze
Jezeli najpierw a := a div 10, a dopiero potem a mod 10, czytasz nie te cyfre. Klasyczny blad copy-paste.
# ZLY
a = a // 10 # ucinasz najmlodsza cyfre
cyfra_a = a % 10 # czytasz JUZ druga od konca, nie ostatnia
# POPRAWNY
cyfra_a = a % 10 # najpierw odczytaj
a = a // 10 # potem skroc
Efekt: algorytm w ogole nie patrzy na ostatnia kolumne, zaczyna od przedostatniej. Koszt: typowo -3 pkt, bo wynik jest losowo zly.
6. Zakazane konstrukcje: str, len, listy
Specyfikacja explicit zabrania konwersji napis-liczba, tablic, list i funkcji wlasnych poza dozwolonymi operatorami. Rozwiazanie typu:
# ZAKAZANE - 0 pkt mimo poprawnego wyniku
sa = str(a)
sb = str(b)
n = len(sa)
for i in range(n-1, -1, -1):
...
CKE traktuje to jako niespelnienie warunkow zadania i zeruje 2.2 nawet jezeli wynik dla przykladu jest poprawny. To NIE jest negocjowalne, egzaminator nie ma marginesu interpretacyjnego.
7. Carry na koncu, gdy nie ma juz cyfr do dodania
Przyklad: 9+1. Ostatnia (i jedyna) kolumna: 9+1=10, p=1. Po tej iteracji a=0, b=0, carry=1. Niektorzy zdajacy mysla "to przeniesienie wystaje poza liczbe, nie liczy sie" - to blad interpretacyjny. CKE jasno definiuje: kazde wystapienie sytuacji "suma >= 10 w danej kolumnie" to jedno przeniesienie, niezaleznie czy ostatecznie wystaje poza liczbe wynikowa.
1
9
+ 1
---
1 0 <- carry koncowy istnieje, ale p liczy SIE +1 za 9+1
Algorytm nie musi specjalnie obslugiwac "carry koncowego", wystarczy ze warunek s >= 10 zliczy go w iteracji ostatniej cyfry. Pulapka pojawia sie wtedy, gdy zdajacy dorzuca dodatkowy if carry == 1 then p := p - 1 "zeby skompensowac wystajacy carry". To czysty blad logiczny i -1 pkt.
Jak CKE punktuje 2.2 (4 pkt)
Klucz odpowiedzi CKE rozbija 4 punkty zwykle nastepujaco: 1 pkt za poprawna inicjalizacje (p=0, carry=0) i zgodnosc ze specyfikacja Dane/Wynik; 1 pkt za poprawna petle iterujaca po cyfrach z uzyciem div i mod; 1 pkt za poprawne wyliczenie sumy z uwzglednieniem carry IN i warunek >= 10; 1 pkt za poprawna aktualizacje carry (zarowno ustawienie na 1, jak i powrot do 0) oraz dzialanie algorytmu dla przykladu 27732+72619=4. Uzycie zakazanej konstrukcji (str, len, lista, tablica, wlasna funkcja, operator inny niz dozwolone) zeruje cale zadanie 2.2 niezaleznie od poprawnosci logiki - egzaminator nie zaglada w wynik, tylko skresla. Drobny blad logiczny (np. > zamiast >=) kosztuje 1-2 pkt zaleznie od tego, czy algorytm dziala dla podanego przykladu z trescia zadania.
Zadania własne — ćwiczenia z odpowiedziami
Sekcja praktyczna. Kazde zadanie ma rozwiazanie ukryte w <details>: najpierw policz sam, dopiero potem rozwin. W zadaniach konstrukcyjnych (3, 4, 5) wlasna odpowiedz moze byc inna niz wzorcowa i nadal poprawna - wazne, zeby spelniala warunki zadania.
cyfra_a + cyfra_b + przeniesienie_z_poprzedniej_kolumny >= 10. Liczymy od najmlodszej cyfry (prawej) do najstarszej (lewej).
5749 + 3451. Narysuj slup z przeniesieniami.
Pokaz odpowiedz
Odpowiedz: 3 przeniesienia.
Przeniesienie: 1 1 1
Liczba a: 5 7 4 9
Liczba b: + 3 4 5 1
---------
Wynik: 9 2 0 0
Kolumna jednosci: 9 + 1 + 0 = 10 -> wpis 0, carry 1 [PRZENIESIENIE]
Kolumna dziesiatek: 4 + 5 + 1 = 10 -> wpis 0, carry 1 [PRZENIESIENIE]
Kolumna setek: 7 + 4 + 1 = 12 -> wpis 2, carry 1 [PRZENIESIENIE]
Kolumna tysiecy: 5 + 3 + 1 = 9 -> wpis 9, carry 0
Razem przeniesien: 3
Wynik koncowy: 9200
Uwaga: w arkuszu maturalnym istotne sa przeniesienia ktore powstaja podczas dodawania, nie to czy na koniec dostaniesz dodatkowa cyfre z lewej. Tu carry z tysiecy wynosi 0, wiec wynik ma 4 cyfry - liczymy 3 przeniesienia w trzech najmlodszych kolumnach.
19999 + 80001. To klasyczna "lawina carry": zwroc uwage na to, jak jedno przeniesienie z najmlodszej kolumny propaguje sie przez cala liczbe.
Pokaz odpowiedz
Odpowiedz: 5 przeniesien.
Przeniesienie: 1 1 1 1 1
Liczba a: 1 9 9 9 9
Liczba b: + 8 0 0 0 1
-----------
Wynik: 1 0 0 0 0 0
Kolumna 1 (jednosci): 9 + 1 + 0 = 10 -> carry 1
Kolumna 2 (dziesiatki): 9 + 0 + 1 = 10 -> carry 1
Kolumna 3 (setki): 9 + 0 + 1 = 10 -> carry 1
Kolumna 4 (tysiace): 9 + 0 + 1 = 10 -> carry 1
Kolumna 5 (dziesiatki tys): 1 + 8 + 1 = 10 -> carry 1
Razem: 5 przeniesien
Wynik: 100000 (szesciocyfrowy, bo ostatnie carry "wyszlo" w nowa kolumne)
9999) i dodaj 1: przeniesienie idzie przez wszystkie pozycje. Tutaj 19999 + 80001 robi to samo zjawisko, tylko z innymi cyframi startowymi.
Pokaz odpowiedz
Regula jest prosta: w kazdej kolumnie suma cyfr musi byc <= 9. Wtedy zaden carry nie powstaje na zadnej pozycji.
Przyklad poprawny: 1234 + 5432.
Przeniesienie: 0 0 0 0
Liczba a: 1 2 3 4
Liczba b: + 5 4 3 2
---------
Wynik: 6 6 6 6
Kolumna jednosci: 4 + 2 = 6 (<= 9, brak carry)
Kolumna dziesiatek: 3 + 3 = 6 (<= 9, brak carry)
Kolumna setek: 2 + 4 = 6 (<= 9, brak carry)
Kolumna tysiecy: 1 + 5 = 6 (<= 9, brak carry)
Przeniesien: 0
Inne poprawne odpowiedzi: 1111 + 1111 = 2222, 2222 + 3333 = 5555, 4321 + 5432 = 9753 (kolumna jednosci 1+2=3, dziesiatek 2+3=5, setek 3+4=7, tysiecy 4+5=9 - wszystko <= 9).
Anty-przyklad: 1234 + 5678 - kolumna jednosci 4+8=12, juz carry. NIE spelnia.
Pokaz odpowiedz
Trzy przeniesienia w liczbach 3-cyfrowych oznaczaja, ze carry powstaje na kazdej z trzech pozycji (jednosci, dziesiatki, setki). To wymaga zeby suma w kazdej kolumnie (z uwzglednieniem przychodzacego carry) byla >= 10.
Najprostsza droga: zbudowac "pelna lawine" jak 555 + 555 lub 999 + 111.
Przyklad A: 555 + 555
Przeniesienie: 1 1 1
Liczba a: 5 5 5
Liczba b: + 5 5 5
---------
Wynik: 1 1 1 0
Kolumna jednosci: 5 + 5 + 0 = 10 -> carry 1
Kolumna dziesiatek: 5 + 5 + 1 = 11 -> carry 1
Kolumna setek: 5 + 5 + 1 = 11 -> carry 1
Razem: 3 przeniesienia, wynik 1110
Przyklad B: 999 + 111
Przeniesienie: 1 1 1
Liczba a: 9 9 9
Liczba b: + 1 1 1
---------
Wynik: 1 1 1 0
Kolumna jednosci: 9 + 1 + 0 = 10 -> carry 1
Kolumna dziesiatek: 9 + 1 + 1 = 11 -> carry 1
Kolumna setek: 9 + 1 + 1 = 11 -> carry 1
Razem: 3 przeniesienia, wynik 1110
Zwroc uwage: po 3 przeniesieniach w liczbach 3-cyfrowych wynik ZAWSZE ma 4 cyfry (bo carry z najstarszej kolumny tworzy nowa pozycje z lewej).
a + b wiekszej niz w zadaniu 4 (gdzie bylo 555+555 = 1110, suma cyfr 5+5+5+5+5+5 = 30, albo 999+111 = 1110, suma cyfr 9+9+9+1+1+1 = 30). Cel: liczba bedzie wieksza od 1110, ale ZADNEJ kolumny nie przepelnia.
Pokaz odpowiedz
Pomysl: chcemy uniknac carry (kazda kolumna <= 9), ale chcemy uzyc cyfr ktore w sumie daja duzo. Maksymalna suma cyfr w jednej kolumnie bez carry to 9 (np. 4+5, 9+0, 3+6). Wiec dla 4-cyfrowych liczb maksymalna suma cyfr a+b bez zadnego carry wynosi 9 * 4 = 36.
Przyklad ktory tego dosiega:
Liczba a: 9 0 9 0
Liczba b: 0 9 0 9
---------
Wynik: 9 9 9 9
Kolumna jednosci: 0 + 9 = 9 (brak carry)
Kolumna dziesiatek: 9 + 0 = 9 (brak carry)
Kolumna setek: 0 + 9 = 9 (brak carry)
Kolumna tysiecy: 9 + 0 = 9 (brak carry)
Przeniesien: 0
Suma cyfr a + b = 9+0+9+0 + 0+9+0+9 = 36
Wynik liczbowy: 9999 (znacznie wiecej niz 1110 z zadania 4)
9999 (wynik 9090+0909) jest 9x wieksze od 1110 (555+555), a powstalo bez ani jednego carry. Przeniesienia opisuja strukture cyfr, nie wielkosc.
Pokaz odpowiedz
Idea: w systemie dziesietnym carry powstaje, gdy suma w kolumnie >= 10. W systemie binarnym (podstawa 2) carry powstaje, gdy suma w kolumnie >= 2. Wynik kolumny to suma mod 2, a carry to suma div 2 (zawsze 0 albo 1, bo max suma to 1 + 1 + 1 = 3).
Wystarczy w algorytmie zamienic kazde wystapienie 10 na 2:
def liczba_przeniesien_bin(a, b):
p = 0 # licznik przeniesien
c = 0 # carry z poprzedniej pozycji
while a > 0 or b > 0 or c > 0:
bit_a = a % 2 # cyfra binarna a
bit_b = b % 2 # cyfra binarna b
s = bit_a + bit_b + c # suma w kolumnie
if s >= 2:
c = 1
p = p + 1
else:
c = 0
a = a // 2
b = b // 2
return p
# Test: 0b1011 + 0b0111 = 11 + 7 = 18 = 0b10010
# Kolumny od prawej:
# 1 + 1 + 0 = 2 -> bit 0, carry 1 (1. przeniesienie)
# 1 + 1 + 1 = 3 -> bit 1, carry 1 (2. przeniesienie)
# 0 + 1 + 1 = 2 -> bit 0, carry 1 (3. przeniesienie)
# 1 + 0 + 1 = 2 -> bit 0, carry 1 (4. przeniesienie)
# 0 + 0 + 1 = 1 -> bit 1, carry 0
# Razem: 4 przeniesienia
print(liczba_przeniesien_bin(0b1011, 0b0111)) # -> 4
W pseudokodzie maturalnym (z dozwolonym mod i div):
funkcja LiczbaPrzeniesienBin(a, b):
p <- 0
c <- 0
dopoki a > 0 lub b > 0 lub c > 0:
bit_a <- a mod 2
bit_b <- b mod 2
s <- bit_a + bit_b + c
jezeli s >= 2:
c <- 1
p <- p + 1
w przeciwnym razie:
c <- 0
a <- a div 2
b <- b div 2
zwroc p
B >= 2: wystarczy zamienic kazde 10 (w wersji dziesietnej) lub 2 (w wersji binarnej) na B, a mod 10 / div 10 na mod B / div B. To pokazuje, ze "przeniesienie" jest cecha pozycyjnego systemu liczbowego, nie konkretnej podstawy.
a > 0 lub b > 0 lub c > 0 automatycznie obsluguje rozne dlugosci). To rozszerzenie poza wymagania arkusza, pokazuje, ze rozumiesz problem glebiej niz tylko skopiowanie wzorca.
Podsumowanie
Z tego zadania uczen wynosi trzy rzeczy operacyjne: ekstrakcja cyfr przez mod 10 i div 10 (jedyna dozwolona droga przy zakazie str()), nawyk przekazywania carry jako jednej zmiennej skalarnejnadpisywanej w petli oraz dyscypline warunku s >= 10 z obowiazkowym else: carry = 0. Te trzy elementy ukladaja sie w wzorzec "per-pozycja w systemie pozycyjnym", ktory na maturze wraca w kazdym zadaniu o liczbach: konwersjach miedzy systemami (Z2/Z10/Z16), liczeniu sumy cyfr, palindromach liczbowych, kontrolnym NIP/PESEL, mnozeniu pisemnym. Umiejetnosc "iteruj cyfra po cyfrze bez zamiany na napis" jest transferowalna 1:1 i odroznia 4 punkty od 0 punktow nie tylko w zadaniu 2.
📘 Maj 2026 — Zadanie 3 (Pary słów): pełny przewodnik
Wyczerpujący przewodnik do Zadania 3 z arkusza maj 2026 — od intuicji po pełne rozwiązanie z ćwiczeniami.
Wprowadzenie
Zadanie 3 z arkusza maturalnego maj 2026 to klasyczne dla CKE polowanie na napisy — trzy podpunkty, jedna struktura danych wejsciowych, trzy zupelnie rozne sposoby patrzenia na te same slowa. Plik pary.txt zawiera 500 par slow, kazda para w osobnym wierszu, slowa rozdzielone pojedyncza spacja. Slowa skladaja sie wylacznie z malych liter a-z i maja maksymalnie 50 znakow. Do testowania CKE dostarcza pary_przyklad.txt z gotowymi odpowiedziami w tresci zadania — to poligon do weryfikacji, czy algorytm w ogole liczy to, co trzeba.
Intuicja trzech podpunktow w jednym akapicie
Podpunkt 3.1 to porownanie wagi liter — kazdej parze przypisujesz dwie liczby f(s1) i f(s2) bedace suma kodow ASCII, i szukasz pary o najwiekszej roznicy bezwzglednej. Litera a wazy 97, litera z wazy 122 — zatem para z dlugim slowem pelnym z kontra krotkim slowem pelnym a bedzie kandydatem na maksimum. Podpunkt 3.2 to wspolny multizbior liter — dla kazdej litery a-z liczysz, ile razy wystepuje w s1, ile w s2, i bierzesz minimum (czyli ile razy litera pasuje w obie strony). Sumujesz po wszystkich 26 literach. To dokladnie operacja Counter(s1) & Counter(s2) z Pythona — przeciecie multizbiorow. Podpunkt 3.3 to dopasowanie konca jednego slowa do poczatku drugiego — szukasz najdluzszego napisu, ktory jest prefiksem s1 i sufiksem s2 albo na odwrot. To prosta wersja zagadnienia, ktore w wersji doroslej rozwiazuje algorytm KMP (funkcja prefiksowa) i wykorzystuja je wszystkie powazne implementacje wyszukiwania wzorca w tekscie.
Struktura pliku — co konkretnie wczytujesz
| Wlasnosc | Wartosc |
|---|---|
| Liczba wierszy | 500 |
| Format wiersza | slowo1 slowo2\n |
| Alfabet | tylko a-z (male litery) |
| Dlugosc pojedynczego slowa | od 1 do 50 znakow |
| Separator wewnatrz wiersza | pojedyncza spacja |
| Plik testowy | pary_przyklad.txt (odpowiedzi w tresci zadania) |
| Plik wynikowy | wyniki3.txt z prefiksem numeru podpunktu |
Wczytanie sprowadza sie do jednej petli:
with open("pary.txt", encoding="utf-8") as f:
pary = [linia.strip().split() for linia in f if linia.strip()]
# pary[i] = [slowo1, slowo2]
pary_przyklad.txt i porownaj z odpowiedziami podanymi w tresci. Jesli sie zgadza, masz pewnosc, ze logika dziala. Dopiero potem podstawiasz pary.txt i przepisujesz wynik do wyniki3.txt. Ten dwuetapowy workflow eliminuje 90% bledow, ktore na maturze kosztuja punkty.
Format pliku wynikowego
Plik wyniki3.txt ma byc czytelny dla egzaminatora — uzyj prefiksu numeru podpunktu w osobnej linii, zeby od razu bylo wiadomo, co skad pochodzi. Format identyczny z tym, ktory generuje funkcja main() w skrypcie ponizej:
3.1.
gpeeazeugmvsbzwsrxfplqdbakoxxe lhpbmoirdm 2206
3.2.
aacbcccaacacbcabac cccccaaaacaccbabcba 18
3.3.
bbbbaabbababbaaaa baaaaabaaabbbabab 5
aababbbababbbbbbaab bbbbaabbababababa 7
...
pary.txt), a nie jeden. Jesli wypiszesz tylko pierwsza znaleziona pare o dlugosci ≥5, stracisz punkty za niekompletnosc. Tresc zadania mowi wyraznie wszystkie pary — czytaj polecenie do konca, nie tylko pierwsza linijke. Trzymaj jeden format zapisu (etykieta podzadania w osobnej linii, ponizej dane) konsekwentnie w calym pliku — mieszanie konwencji w obrebie jednego wyniki3.txt sygnalizuje egzaminatorowi niechlujstwo.
Zanim napiszesz pierwsza linijke, powinienes umiec odpowiedziec na trzy pytania bez zagladania do edytora: (1) jakiego typu jest wynik kazdego podpunktu — liczba, para, lista par; (2) jaka operacja na pojedynczej parze rozwiazuje problem — suma ASCII, minimum z dwoch Counterow, przeszukanie prefiksow; (3) jak laczysz wyniki pojedynczych par w wynik koncowy — argmax, argmax, filtr po progu ≥5. Te trzy pytania pokrywaja 100% logiki zadania. Reszta to kwestia czystego zapisu w Pythonie i ostroznosci przy I/O.
Przyklady i trace na malych danych
Trzy podpunkty operuja na tym samym zbiorze 500 par, ale kazdy mierzy zupelnie inna wlasciwosc. Zanim uderzysz w kod na pelnym zbiorze, przesledz logike na parach z arkusza — wtedy wynik z pary_przyklad.txt dziala jak test jednostkowy: gdy nie zgadza sie ani na bit, masz blad w funkcji, nie w danych.
3.1 — najwieksza roznica sum kodow ASCII
Funkcja f(s) to po prostu suma kodow ASCII wszystkich znakow slowa. Szukamy pary, ktora maksymalizuje |f(s1) - f(s2)|. Zadne sortowanie, zadne triki — jeden przebieg, jedna zmienna na maksimum, jedna na zwyciezka pare.
Trace na parze z tresci: oko / pies
| Slowo | Znak | ord() | Suma czastkowa |
|---|---|---|---|
oko | o | 111 | 111 |
| k | 107 | 218 | |
| o | 111 | 329 | |
pies | p | 112 | 112 |
| i | 105 | 217 | |
| e | 101 | 318 | |
| s | 115 | 433 |
|f(oko) - f(pies)| = |329 - 433| = 104. Gdyby cala lista miala tylko ta pare, ona bylaby zwyciezka.
3.2 — suma wspolnych wystapien wszystkich liter
Dla kazdej z liter a-z liczymy ile razy wystepuje w s1 i ile w s2, bierzemy minimum (czyli ile par tej litery da sie zlozyc z obu slow), sumujemy te minima dla calego alfabetu. Pythonowy collections.Counter ma to wbudowane jako przeciecie c1 & c2, ale dla zrozumienia warto przesledzic recznie.
Trace na parze z tresci: adabbcdde / aadabbbccdc
| Litera | Wystapienia w adabbcdde | Wystapienia w aadabbbccdc | min(.,.) |
|---|---|---|---|
| a | 2 | 3 | 2 |
| b | 2 | 3 | 2 |
| c | 1 | 3 | 1 |
| d | 3 | 2 | 2 |
| e | 1 | 0 | 0 |
| Suma | 7 | ||
Litery, ktorych nie ma w zadnym ze slow, nie wnosza nic — pominiete. Litery wystepujace tylko po jednej stronie daja min(n, 0) = 0, czyli rowniez nic.
Counter a nie petla po alfabecie? Bo Counter(s1) & Counter(s2) zwraca multiset intersection — dokladnie minimum wystapien dla kazdego klucza. Mniej kodu, mniej miejsc na off-by-one.
3.3 — prefiksosufiks o dlugosci co najmniej 5
Definicja jest dwukierunkowa: prefiksosufiks pary (s1, s2) to najdluzszy ciag znakow, ktory jest jednoczesnie poczatkiem jednego slowa i koncem drugiego — w dowolna strone. Sprawdzasz dwa warianty i bierzesz wieksze maksimum:
- Wariant A: prefiks
s1== sufikss2. Maks dlugoscktaka, zes1[:k] == s2[-k:]. - Wariant B: prefiks
s2== sufikss1. Maks dlugoscktaka, zes2[:k] == s1[-k:].
Wynik dla pary = max(A, B). Para kwalifikuje sie gdy ten wynik >= 5.
Trace 1: aabbbca / caacaab — maks = 3, NIE spelnia
Wariant A — prefiks aabbbca vs sufiks caacaab:
k=1: "a" vs "b" -> NIE
k=2: "aa" vs "ab" -> NIE
k=3: "aab" vs "aab" -> TAK (kandydat: 3)
k=4: "aabb" vs "caab" -> NIE
k=5: "aabbb" vs "acaab" -> NIE
k=6: "aabbbc" vs "aacaab" -> NIE
k=7: "aabbbca" vs "caacaab" -> NIE
Wariant A max = 3 (pasuje "aab"). Wariant B — prefiks caacaab vs sufiks aabbbca:
k=1: "c" vs "a" -> NIE
k=2: "ca" vs "ca" -> TAK (kandydat: 2)
k=3: "caa" vs "bca" -> NIE
k=4: "caac" vs "bbca" -> NIE
k=5: "caaca" vs "bbbca" -> NIE
k=6: "caacaa" vs "abbbca" -> NIE
k=7: "caacaab" vs "aabbbca" -> NIE
Wariant B max = 2. max(3, 2) = 3. Para NIE trafia do wyniku (prog to 5).
Trace 2: abbaabaa / baabaabba — maks = 6, SPELNIA
Wariant A — prefiks abbaabaa vs sufiks baabaabba:
k=1: "a" vs "a" -> TAK
k=2: "ab" vs "ba" -> NIE
k=3: "abb" vs "bba" -> NIE
k=4: "abba" vs "abba" -> TAK
k=5: "abbaa" vs "aabba" -> NIE
k=6: "abbaab" vs "baabba" -> NIE
k=7: "abbaaba" vs "abaabba" -> NIE
k=8: "abbaabaa" vs "aabaabba" -> NIE
Wariant A max = 4. Wariant B — prefiks baabaabba vs sufiks abbaabaa:
k=1: "b" vs "a" -> NIE
k=2: "ba" vs "aa" -> NIE
k=3: "baa" vs "baa" -> TAK
k=4: "baab" vs "abaa" -> NIE
k=5: "baaba" vs "aabaa" -> NIE
k=6: "baabaa" vs "baabaa" -> TAK (kandydat: 6)
k=7: "baabaab" vs "bbaabaa" -> NIE
k=8: "baabaabb" vs "abbaabaa" -> NIE
Wariant B max = 6 (pasuje "baabaa"). max(4, 6) = 6 >= 5. Para SPELNIA warunek — trafia do wyniku z dlugoscia 6.
Pelne rozwiazanie
Wspolny szkielet dla wszystkich trzech podpunktow jest identyczny — wczytaj 500 par, dla kazdej policz metryke, trzymaj rekord. Rozni sie tylko funkcja liczaca. Tu wyprowadzam kazda z nich, a na koncu sklejam je w jeden skrypt zad3.py, ktory zapisuje wszystko do wyniki3.txt z prefiksem numeru podpunktu.
Wspolny szkielet — wczytanie par
def wczytaj_pary(sciezka):
pary = []
with open(sciezka, encoding='utf-8') as f:
for linia in f:
linia = linia.strip()
if not linia:
continue
s1, s2 = linia.split(' ', 1)
pary.append((s1, s2))
return pary
Zamiast split() bez argumentu uzywam split(' ', 1) — jawnie spacja, jawnie maksymalnie jeden podzial. Tresc gwarantuje, ze separator to dokladnie spacja.
3.1 — implementacja
Pseudokod:
najlepsza_para := None
najlepsza_roznica := -1
dla kazdej pary (s1, s2):
r := |suma_ascii(s1) - suma_ascii(s2)|
jezeli r > najlepsza_roznica:
najlepsza_roznica := r
najlepsza_para := (s1, s2)
zwroc (najlepsza_para, najlepsza_roznica)
def suma_ascii(s):
return sum(ord(z) for z in s)
def zad_3_1(pary):
najlepsza_para = None
najlepsza_roznica = -1
for s1, s2 in pary:
r = abs(suma_ascii(s1) - suma_ascii(s2))
if r > najlepsza_roznica:
najlepsza_roznica = r
najlepsza_para = (s1, s2)
return najlepsza_para, najlepsza_roznica
suma_ascii(s)— generatorord(z) for z in sdaje kody znakow,sumagreguje.najlepsza_roznica = -1— startujemy ponizej zera, zeby pierwsza para z roznica 0 tez zostala zapamietana.if r > najlepsza_roznica— ostry znak nierownosci oznacza, ze przy remisie zachowujemy pare napotkana wczesniej.
3.2 — implementacja
Dla kazdej litery x liczymy W(x, s1, s2) = min(d(x, s1), d(x, s2)). Suma po wszystkich literach to dokladnie suma min(Counter(s1)[x], Counter(s2)[x]). collections.Counter zwraca 0 dla nieobecnego klucza, wiec iteracja po sumie zbiorow set(s1) | set(s2) jest bezpieczna.
from collections import Counter
def wspolne_wystapienia(s1, s2):
c1 = Counter(s1)
c2 = Counter(s2)
suma = 0
for litera in set(s1) | set(s2):
suma += min(c1[litera], c2[litera])
return suma
def zad_3_2(pary):
najlepsza_para = None
najlepsza_suma = -1
for s1, s2 in pary:
s = wspolne_wystapienia(s1, s2)
if s > najlepsza_suma:
najlepsza_suma = s
najlepsza_para = (s1, s2)
return najlepsza_para, najlepsza_suma
Wariant jednolinijkowy (krocej, ale w arkuszu CKE lepiej zostac przy rozbitej wersji):
def wspolne_wystapienia(s1, s2):
return sum((Counter(s1) & Counter(s2)).values())
3.3 — implementacja
Pseudokod:
funkcja najdluzszy_prefiksosufiks(s1, s2):
najlepsze := 0
dla k od 1 do min(|s1|, |s2|):
jezeli s1[:k] == s2[-k:] LUB s2[:k] == s1[-k:]:
najlepsze := k
zwroc najlepsze
def najdluzszy_prefiksosufiks(s1, s2):
najlepsze = 0
max_k = min(len(s1), len(s2))
for k in range(1, max_k + 1):
if s1[:k] == s2[-k:] or s2[:k] == s1[-k:]:
najlepsze = k
return najlepsze
def zad_3_3(pary, prog=5):
wyniki = []
for s1, s2 in pary:
d = najdluzszy_prefiksosufiks(s1, s2)
if d >= prog:
wyniki.append((s1, s2, d))
return wyniki
max_k = min(len(s1), len(s2))— dluzszy niz krotsze slowo prefiksosufiks byc nie moze.s1[:k]— pierwszekznakows1;s2[-k:]— ostatniekznakows2.or— krotkie spiecie. Jesli pierwsze porownanie wyjdzie True, drugie nie jest liczone.- W
zad_3_3nie szukamy maksimum — zbieramy wszystkie pary z prefiksosufiksem ≥ 5.
Kompletny skrypt zad3.py
from collections import Counter
def wczytaj_pary(sciezka):
pary = []
with open(sciezka, encoding='utf-8') as f:
for linia in f:
linia = linia.strip()
if not linia:
continue
s1, s2 = linia.split(' ', 1)
pary.append((s1, s2))
return pary
def suma_ascii(s):
return sum(ord(z) for z in s)
def zad_3_1(pary):
najlepsza_para = None
najlepsza_roznica = -1
for s1, s2 in pary:
r = abs(suma_ascii(s1) - suma_ascii(s2))
if r > najlepsza_roznica:
najlepsza_roznica = r
najlepsza_para = (s1, s2)
return najlepsza_para, najlepsza_roznica
def wspolne_wystapienia(s1, s2):
c1 = Counter(s1)
c2 = Counter(s2)
suma = 0
for litera in set(s1) | set(s2):
suma += min(c1[litera], c2[litera])
return suma
def zad_3_2(pary):
najlepsza_para = None
najlepsza_suma = -1
for s1, s2 in pary:
s = wspolne_wystapienia(s1, s2)
if s > najlepsza_suma:
najlepsza_suma = s
najlepsza_para = (s1, s2)
return najlepsza_para, najlepsza_suma
def najdluzszy_prefiksosufiks(s1, s2):
najlepsze = 0
max_k = min(len(s1), len(s2))
for k in range(1, max_k + 1):
if s1[:k] == s2[-k:] or s2[:k] == s1[-k:]:
najlepsze = k
return najlepsze
def zad_3_3(pary, prog=5):
wyniki = []
for s1, s2 in pary:
d = najdluzszy_prefiksosufiks(s1, s2)
if d >= prog:
wyniki.append((s1, s2, d))
return wyniki
def main():
pary = wczytaj_pary('pary.txt')
(s1, s2), r = zad_3_1(pary)
(a1, a2), suma = zad_3_2(pary)
wyniki33 = zad_3_3(pary, prog=5)
with open('wyniki3.txt', 'w', encoding='utf-8') as f:
f.write('3.1.\n')
f.write(f'{s1} {s2} {r}\n\n')
f.write('3.2.\n')
f.write(f'{a1} {a2} {suma}\n\n')
f.write('3.3.\n')
for w1, w2, d in wyniki33:
f.write(f'{w1} {w2} {d}\n')
if __name__ == '__main__':
main()
Zweryfikowane odpowiedzi na realnym pary.txt
| Podpunkt | Para / lista par | Wartosc |
|---|---|---|
| 3.1 | gpeeazeugmvsbzwsrxfplqdbakoxxe lhpbmoirdm | 2206 |
| 3.2 | aacbcccaacacbcabac cccccaaaacaccbabcba | 18 |
| 3.3 | 7 par (ponizej) | dlugosci 5-7 |
| # | s1 | s2 | Dlugosc |
|---|---|---|---|
| 1 | bbbbaabbababbaaaa | baaaaabaaabbbabab | 5 |
| 2 | aababbbababbbbbbaab | bbbbaabbababababa | 7 |
| 3 | aaaababaaaabbbb | aabbbbbabbbaaaa | 6 |
| 4 | bbbbabaaabbbabb | aaababaabbbbbbba | 5 |
| 5 | ccccabacbba | acbbabcbcbcbaa | 5 |
| 6 | caabbccabccc | cabccccabbaac | 6 |
| 7 | abaacabcccccabbbc | abbbcbbbbbcabaca | 5 |
pary_przyklad.txt (odpowiedzi z tresci zadania):
- 3.1 →
eddcd eededcddceeecededcc 1403 - 3.2 →
ccccdcedddddecde ccdcdddecc 10 - 3.3 →
ececdddeed dddeedd 6
pary.txt.
Pulapki
Zadanie 3 wyglada przyjaznie, ale pulapki nie tkwia w algorytmice, tylko w drobiazgach interpretacyjnych i implementacyjnych. Komisja na maturze odejmuje punkty za poprawny algorytm z blednym indeksem tak samo jak za calkowite niezrozumienie.
1. Definicja prefiksosufiksu — OBA kierunki, nie jeden
(s1, s2) to slowo, ktore jest poczatkiem s1 i koncem s2, ALBO poczatkiem s2 i koncem s1. To dwa osobne testy. Wielu maturzystow sprawdza tylko jeden kierunek (zwykle pierwszy) i traci pary, gdzie pasuje tylko drugi. Przyklad z tresci: abbaabaa / baabaabba ma maksymalny prefiksosufiks dlugosci 6, ale wynika to z kierunku drugiego.
2. Slicing — a[:k] vs a[-k:]
| Wyrazenie | Znaczenie | Przyklad dla "abcdef", k=3 |
|---|---|---|
a[:k] | pierwsze k znakow | "abc" |
a[-k:] | ostatnie k znakow | "def" |
a[k:] | od pozycji k do konca (BLAD jesli chcecie sufiksu) | "def" (przypadkowo dobrze dla k=3 i dlugosci 6) |
a[:-k] | wszystko POZA ostatnimi k | "abc" (przypadkowo dobrze dla tego samego przypadku) |
Te przypadkowe zbieznosci dla rownej dlugosci sa szczegolnie zdradzieckie — kod dziala na krotkim tescie i sypie sie na produkcyjnym pliku, w ktorym dlugosci sa rozne.
a[k:] i a[-k:] daja ten sam wynik tylko gdy len(a) == 2*k. Dla dowolnych innych dlugosci sa to dwa rozne kawalki napisu. W zadaniu 3.3 slowa s1 i s2 moga miec rozne dlugosci (do 50 znakow), wiec uzycie s2[k:] zamiast s2[-k:] da bledne porownania na wiekszosci par.
| Napis | k | a[k:] | a[-k:] | Zgodne? |
|---|---|---|---|---|
"abcdef" (dl. 6) | 3 | "def" | "def" | tak (len = 2k) |
"abcdefgh" (dl. 8) | 3 | "defgh" | "fgh" | nie |
"abcde" (dl. 5) | 3 | "de" | "cde" | nie |
"abcdefghij" (dl. 10) | 4 | "efghij" | "ghij" | nie |
a[-k:] z minusem. a[k:] to "od pozycji k do konca" i to zupelnie inna semantyka — daje sufiks o dlugosci len(a) - k, a nie k.
3. Zakres petli w 3.3
Zakres range(1, min(len(s1), len(s2)) + 1) jest jedynym poprawnym. Czeste bledy:
range(min(len(s1), len(s2)))— pomija ostatnia mozliwa dlugoscrange(5, min(...) + 1)— preemptywne filtrowanie; gubicie pelne maksimum, ktore moze byc potrzebne do dalszej decyzjirange(1, max(len(s1), len(s2)) + 1)— slice'y poza zakresem krotszego slowa
>= 5 zostawcie na koniec, po znalezieniu maksimum dla kazdej pary. Tresc wymaga par z prefiksosufiksem dlugosci najwiekszej, ale tylko gdy ta najwieksza wynosi co najmniej 5. Para z maksimum 3 odpada w calosci.
4. Counter, union i przeciecie zbiorow w 3.2
Iteracja po sumie unii set(s1) | set(s2) i po przecieciu set(s1) & set(s2) daje w tym zadaniu identyczny wynik (bo dla litery obecnej tylko w jednym slowie minimum wynosi 0). Przeciecie jest logicznie zgodne z definicja wspolnych wystapien i czytelniejsze. Wybierzcie swiadomie, nie przez przypadek.
5. Format pliku wyniki3.txt
3.1. / 3.2. / 3.3. w osobnej linii, dane w nastepnej). Bez tego oceniajacy moze uznac, ze oddaliscie tylko jedno rozwiazanie z trzech. W 3.3 wypiszcie wszystkie siedem par, kazda w osobnej linii. Trzymajcie jeden format konsekwentnie — przykladowo taki, jaki generuje funkcja main() w skrypcie powyzej.
6. Encoding i separator
Plik pary.txt zawiera tylko ASCII, ale jawne encoding="utf-8" przy open() jest dobrym nawykiem — na komputerze szkolnym z Windowsem default to cp1250. Drobiazg: .split() bez argumentu dzieli po dowolnych bialych znakach i ignoruje wielokrotne spacje, .split(" ") dzieli dokladnie po pojedynczej spacji. Dla pary.txt obie wersje dzialaja.
7. Jeden traversal vs trzy osobne
Plik pary.txt ma 500 wierszy. Caly zestaw policzycie w jednej petli i to jest preferowane — ulatwia debug:
best_diff = (0, "", "")
best_common = (0, "", "")
ps_pairs = []
for s1, s2 in pary:
d = abs(sum(map(ord, s1)) - sum(map(ord, s2)))
if d > best_diff[0]:
best_diff = (d, s1, s2)
w = wspolne_wystapienia(s1, s2)
if w > best_common[0]:
best_common = (w, s1, s2)
k = najdluzszy_prefiksosufiks(s1, s2)
if k >= 5:
ps_pairs.append((s1, s2, k))
pary.txt to 7 par o dlugosci ≥ 5.
Zadania własne — ćwiczenia
Piec zadan treningowych pokrywajacych trzy mechaniki Zadania 3: sume kodow ASCII, wspolne wystapienia liter oraz prefiksosufiks pary. Najpierw policz samodzielnie, dopiero potem zweryfikuj odpowiedz.
f("matura"), czyli sume kodow ASCII wszystkich liter. Pokaz obliczenia litera po literze, podaj wynik.
Pokaz odpowiedz
Kody ASCII malych liter zaczynaja sie od a=97 (rosna kolejno).
| Litera | ASCII |
|---|---|
| m | 109 |
| a | 97 |
| t | 116 |
| u | 117 |
| r | 114 |
| a | 97 |
f("matura") = 109 + 97 + 116 + 117 + 114 + 97 = 650
sum(ord(c) for c in "matura") # 650
s1="kotek", s2="piesek" policz sume wspolnych wystapien wszystkich liter alfabetu (definicja W z 3.2).
Pokaz odpowiedz
| Litera | d(x, "kotek") | d(x, "piesek") | min |
|---|---|---|---|
| e | 1 | 2 | 1 |
| i | 0 | 1 | 0 |
| k | 2 | 1 | 1 |
| o | 1 | 0 | 0 |
| p | 0 | 1 | 0 |
| s | 0 | 1 | 0 |
| t | 1 | 0 | 0 |
Suma minimow: 1 + 1 = 2. Wspolne litery to jedno "e" i jedno "k".
from collections import Counter
c1, c2 = Counter("kotek"), Counter("piesek")
sum((c1 & c2).values()) # 2
s1="matka", s2="kura" ma prefiksosufiks o dlugosci co najmniej 2? Sprawdz oba kierunki dla dlugosci 2, 3 i 4.
Pokaz odpowiedz
| Kierunek | Dl. | Prefiks | Sufiks | Rowne? |
|---|---|---|---|---|
| A: s1[:k] vs s2[-k:] | 2 | "ma" | "ra" | nie |
| A: s1[:k] vs s2[-k:] | 3 | "mat" | "ura" | nie |
| A: s1[:k] vs s2[-k:] | 4 | "matk" | "kura" | nie |
| B: s2[:k] vs s1[-k:] | 2 | "ku" | "ka" | nie |
| B: s2[:k] vs s1[-k:] | 3 | "kur" | "tka" | nie |
| B: s2[:k] vs s1[-k:] | 4 | "kura" | "atka" | nie |
Odpowiedz: NIE. Zadna kombinacja nawet dla k=2 nie pasuje, wiec maksymalny prefiksosufiks tej pary wynosi 0. Para nie kwalifikuje sie do wyniku 3.3 (prog ≥ 5).
Pokaz odpowiedz
Najprostsza konstrukcja: pieciolitrowy rdzen otoczony znakami gwarantujacymi brak rozszerzenia do 6.
| s1 | s2 | Wspolny fragment | Kierunek |
|---|---|---|---|
abcdex | yabcde | abcde | A: s1[:5]="abcde" = s2[-5:]="abcde" |
Dlaczego dokladnie 5, a nie 6:
- s1[:6] = "abcdex", s2[-6:] = "yabcde" — rozne.
- Kierunek B: s2[:5] = "yabcd", s1[-5:] = "bcdex" — rozne.
def max_prefsuf(s1, s2):
n = min(len(s1), len(s2))
best = 0
for k in range(1, n + 1):
if s1[:k] == s2[-k:] or s2[:k] == s1[-k:]:
best = k
return best
max_prefsuf("abcdex", "yabcde") # 5
123 4567 w kazdej linii). Opisz, jakie zmiany trzeba wprowadzic w kodzie z 3.1-3.3.
Pokaz odpowiedz
(a) Parsowanie linii
a, b = map(int, linia.split())
(b) Odpowiednik f(s) dla liczby
Najbardziej naturalna jest suma cyfr sum(int(c) for c in str(n)). Przenosi idee agregatu po znakach, zachowujac prostote.
(c) Counter na cyfrach
from collections import Counter
sum((Counter(str(a)) & Counter(str(b))).values())
Logika 3.2 bez zmian — zmienia sie tylko alfabet (z 26 liter na 10 cyfr).
Podsumowanie zmian
| Miejsce w kodzie | Wersja slowna | Wersja liczbowa |
|---|---|---|
| Parsowanie | s1, s2 = linia.split() | a, b = map(int, linia.split()) |
| f(s) | sum(ord(c) for c in s) | sum(int(c) for c in str(n)) |
| Counter | Counter(s) | Counter(str(n)) |
| Prefiksosufiks | s1[:k], s2[-k:] | str(a)[:k], str(b)[-k:] |
Podsumowanie
Zadanie 3 sprowadza sie do trzech niezaleznych metryk na tych samych 500 parach: roznicy sum ASCII (3.1), przeciecia multizbiorow liter (3.2) i dwukierunkowego dopasowania prefiks-sufiks (3.3). Cala logika miesci sie w trzech funkcjach po kilka linii kazda, a poprawnosc weryfikujesz w 30 sekund na pary_przyklad.txt przed puszczeniem na pelnym pliku. Najwiekszym zagrozeniem dla 9/9 nie jest algorytmika, tylko trzy konkretne bledy: sprawdzanie tylko jednego kierunku prefiksosufiksu (utrata 2-4 pkt w 3.3), wypisanie jednej pary zamiast wszystkich siedmiu z progiem ≥ 5 (utrata punktow za niekompletnosc), i brak prefiksu numeru podpunktu w pliku wynikowym (ryzyko, ze egzaminator nie znajdzie ktorejs odpowiedzi). Trzymaj sie pojedynczego traversalu po liscie par, jawnie testuj OBA warianty s1[:k]==s2[-k:] or s2[:k]==s1[-k:], zapisuj wyniki z czytelnymi etykietami 3.1./3.2./3.3. w osobnych liniach i 9 punktow lezy w zasiegu.
📘 Maj 2026 — Zadanie 4 (Korporacja (drzewo hierarchii)): pełny przewodnik
Wyczerpujący przewodnik do Zadania 4 z arkusza maj 2026 — od intuicji po pełne rozwiązanie z ćwiczeniami.
Wprowadzenie — drzewo hierarchii ukryte w tablicy rodzicow
Zadanie 4 z maja 2026 to klasyczny CKE-owy test rozpoznawania struktury. Plik korpo.txt wyglada banalnie: 50 000 liczb, jedna na wiersz. Komisja nie mowi ani slowa o "drzewie", "grafie" ani "hierarchii". Caly trick polega na zauwazeniu, ze ta jedna kolumna liczb jest pelnym, kompletnym zapisem drzewa organizacyjnego firmy — i ze kazde z czterech podpunktow sprowadza sie do jednej operacji na tej strukturze.
Reprezentacja parent-array — co to jest
Drzewo mozna zapisac na kilka sposobow. CKE wybrala najbardziej oszczedna: tablica rodzicow (ang. parent array). Regula jest jedna: pod indeksem i trzymamy numer bezposredniego przelozonego pracownika i. Wartosc 0 oznacza, ze ten pracownik nie ma nad soba nikogo — to prezes, korzen drzewa.
Wyobraz sobie mala firme z 8 osob:
- pracownik 1 jest prezesem (nikt nad nim),
- pracownicy 2 i 3 podlegaja bezposrednio prezesowi,
- pracownicy 4 i 5 podlegaja pracownikowi 2,
- pracownicy 6 i 7 podlegaja pracownikowi 4,
- pracownik 8 podlega pracownikowi 5.
Narysowane jako drzewko wyglada tak:
[1] prezes
/ \
[2] [3]
/ \
[4] [5]
/ \ \
[6] [7] [8]
A zapisane jako tablica rodzicow (indeksowanie od 1, tak jak w pliku CKE):
pracownik i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| szef[i] | 0 | 1 | 1 | 2 | 2 | 4 | 4 | 5 |
To wszystko. Osiem liczb. Drzewo zostalo skompresowane do listy o dlugosci rownej liczbie wierzcholkow. Plik korpo.txt to dokladnie taka sama tablica, tylko 50 000 razy dluzsza.
Slownik pojec, ktorych komisja unika nazywania
Korzen (prezes) — pracownik, ktory ma szef[i] = 0. W realnym korpo.txt bedzie dokladnie jeden taki wiersz z zerem.
Lisc — pracownik, ktorego numer nigdy nie wystepuje jako wartosc w tablicy szef. Inaczej: nikt go nie wskazuje jako swojego przelozonego. W przykladzie powyzej liscmi sa: 3, 6, 7, 8 (czterech ludzi).
Stopien wierzcholka (liczba bezposrednich podwladnych) — ile razy numer k wystepuje jako wartosc w tablicy szef. W przykladzie pracownik 2 wystepuje dwa razy (szef[4]=2, szef[5]=2), wiec ma 2 bezposrednich podwladnych.
Glebokosc (liczba przelozonych nad pracownikiem) — dlugosc lancucha szef[i] -> szef[szef[i]] -> ... az do 0. Dla pracownika 6: szef[6]=4, szef[4]=2, szef[2]=1, szef[1]=0. Trzy skoki, trzech przelozonych nad nim.
Cztery podpunkty — cztery operacje na tej samej strukturze
| Podpunkt | Pytanie | Operacja na tablicy szef |
|---|---|---|
| 4.1 | analiza malej hierarchii (papier) | narysuj drzewko, policz reka |
| 4.2 | ilu jest lisci | policz numery, ktore nie wystepuja jako wartosc |
| 4.3 | kto ma najwiecej bezposrednich podwladnych | policz wystapienia kazdej wartosci, znajdz maksimum |
| 4.4 | maksymalna glebokosc hierarchii | dla kazdego i idz po szef[] az do 0, znajdz najdluzszy lancuch |
Trzy wlasciwosci parent-array, ktore warto wbic do glowy
- Pamiec liniowa — N wierzcholkow zajmuje N liczb, niezaleznie od ksztaltu drzewa.
- Wedrowka w gore jest natychmiastowa — z dowolnego wierzcholka do korzenia idziesz w O(glebokosc), bez rekursywnego DFS-a.
- Wedrowka w dol jest droga — zeby znalezc dzieci wierzcholka
k, musisz przeskanowac cala tablice. Dlatego 4.3 wymaga policzenia wszystkich wystapien w jednym przebiegu.
Zweryfikowane odpowiedzi na realnym korpo.txt: 4.2 = 25113 lisci, 4.3 = pracownik 2 z 19 bezposrednimi podwladnymi, 4.4 = 22 i 2 (maksymalna glebokosc 22 poziomow, ma ja 2 pracownikow). Te liczby beda wyrocznia — jezeli twoj skrypt zwroci cokolwiek innego, masz bug. Dla korpo_przyklad.txt odpowiedz CKE: 2 i 49997.
Maly przyklad (4.1) — hierarchia 8 pracownikow z rysunku w arkuszu
Zadanie 4.1 (1 pkt, na papierze) sprawdza, czy potrafisz odczytac hierarchie z rysunku i wypelnic tabelke dla kazdego z 8 pracownikow: ile ma bezposrednich podwladnych oraz ile wszystkich (tez posrednich). Dwie pozycje w tabelce — pracownik 2 (3 / 4) oraz pracownik 8 (0 / 0) — sa juz wpisane jako wzor. Twoja praca: uzupelnic pozostale 6 wierszy. Wszystko trzeba zaczytac z rysunku; nie ma tu kodu.
Hierarchia z arkusza (Rysunek 1)
Strzalki na rysunku wskazuja na bezposrednich przelozonych. Czyli pracownik 7 ma strzalke do pracownika 3 — znaczy 3 jest szefem 7. Z opisu pod rysunkiem wiemy, ze pracownik 7 ma ponadto przelozonych 2 i 1 (laricuch w gore: 7 -> 3 -> 2 -> 1), a podwladnymi pracownika 2 sa 3, 5, 6 i 7 (3 bezposrednich + 1 posredni = 4 wszystkich, dlatego w arkuszu wpisano dla niego 3 / 4).
Po przepisaniu rysunku do tablicy rodzicow parent[i] (gdzie 0 oznacza brak przelozonego):
| Pracownik i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| parent[i] | 0 | 1 | 2 | 1 | 2 | 2 | 3 | 4 |
Czytanie tabelki: pracownik 1 jest prezesem (parent = 0); 2 i 4 raportuja do 1; 3, 5, 6 do 2; 7 do 3; 8 do 4.
Rysunek drzewa (to samo, ulozone od gory w dol)
[1] prezes (parent = 0)
/ [2] [4]
/ | \ [3][5][6] [8]
|
[7]
Strzalka w drzewie idzie od przelozonego do podwladnego (zgodnie z rysunkiem z arkusza, gdzie strzalki wskazuja na przelozonych, my je tu odwrocilismy dla czytelnosci hierarchii).
Jak liczyc dwie kolumny tabeli 4.1
Liczba bezposrednich podwladnych pracownika X = ile razy X wystepuje w wierszu parent[i]. Wystarczy policzyc.
Liczba wszystkich podwladnych pracownika X = liczba pracownikow w calym poddrzewie ukorzenionym w X (sam X sie nie liczy). Wystarczy odczytac z rysunku: ile jest "nizej" od X. Alternatywnie: dla kazdego bezposredniego dziecka dodaj 1 (jego samego) plus jego "wszystkich" — i tak rekurencyjnie.
Pelna tabela odpowiedzi (wszystkie 8 wierszy)
| Numer pracownika | Liczba bezposrednich podwladnych | Liczba wszystkich podwladnych |
|---|---|---|
| 1 | 2 | 7 |
| 2 | 3 (z arkusza) | 4 (z arkusza) |
| 3 | 1 | 1 |
| 4 | 1 | 1 |
| 5 | 0 | 0 |
| 6 | 0 | 0 |
| 7 | 0 | 0 |
| 8 | 0 (z arkusza) | 0 (z arkusza) |
Uzasadnienie kazdego wiersza
- 1 (prezes): bezposrednich = 2 (pracownicy 2 i 4). Wszystkich = 7 (wszyscy oprocz niego: 2, 3, 4, 5, 6, 7, 8).
- 2: bezposrednich = 3 (pracownicy 3, 5, 6). Wszystkich = 4 (3, 5, 6 i 7 jako podwladny posredni przez 3).
- 3: bezposrednich = 1 (pracownik 7). Wszystkich = 1 (tylko 7, bo 7 nie ma juz dalej).
- 4: bezposrednich = 1 (pracownik 8). Wszystkich = 1 (tylko 8).
- 5, 6, 7, 8: bezposrednich = 0, wszystkich = 0 — to liscie drzewa, nikt do nich nie raportuje.
Co z tej analizy przenosisz na 4.2, 4.3, 4.4
Pytania 4.2-4.4 to dokladnie te same operacje, tylko na 50 000 pracownikow z pliku korpo.txt zamiast 8 z rysunku. Algorytm sie nie zmienia, zmienia sie tylko skala — i dlatego musi byc efektywny (memoizacja w 4.4).
| Podpunkt | Pytanie | Metoda | Wynik na korpo.txt |
|---|---|---|---|
| 4.2 | Ile lisci | Numery NIEobecne w kolumnie parent[] | 25113 |
| 4.3 | Kto ma najwiecej bezposrednich podwladnych | Zliczanie wystapien w kolumnie parent[] | pracownik 2 (19 podwladnych) |
| 4.4 | Maksymalna glebokosc (liczba przelozonych nad jednym pracownikiem) | Dla kazdego i ida w gore po parent[], licz kroki, bierz max (z memoizacja) | 22 i 2 |
Pelne rozwiazanie 4.2, 4.3, 4.4 — kod Python z komentarzem
Wszystkie trzy podpunkty operuja na tej samej strukturze: drzewie hierarchii z 50000 wierzcholkami. Trzy pytania to trzy rozne odczyty z tej samej tablicy — nie wymagaja budowy grafu, wystarczy jedno wczytanie pliku do listy.
| Podpunkt | Pytanie | Wynik |
|---|---|---|
| 4.2 | Liczba lisci (pracownicy bez podwladnych) | 25113 |
| 4.3 | Pracownik z najwieksza liczba bezposrednich podwladnych | nr 2, 19 podwladnych |
| 4.4 | Maksymalna glebokosc (liczba przelozonych nad jednym pracownikiem) | 22 i 2 |
Wczytanie pliku — fundament dla 4.2, 4.3, 4.4
szef indeksowanej od 1 do 50000. szef[i] = numer bezposredniego przelozonego pracownika i. Pozycje szef[0] wypelniamy wartownikiem None, zeby indeksowanie zgadzalo sie z numeracja z arkusza. None wybieramy swiadomie zamiast 0 — operacja Counter(szef[1:]) i tak omija pozycje 0, a None wprost sygnalizuje "to nie jest pracownik, tylko padding".
szef = [None] # pozycja 0 — wartownik, zeby szef[i] dla i = 1..50000
with open("korpo.txt") as f: # otwiera plik w trybie tekstowym
for linia in f: # iteracja po wierszach (jeden wiersz = jeden pracownik)
szef.append(int(linia.strip()))# strip usuwa biale znaki, int zamienia tekst na liczbe
Po tej petli len(szef) == 50001, a szef[i] to numer szefa pracownika i. Dla prezesa szef[i] == 0.
Zadanie 4.2 — liczba lisci
Definicja problemu. Lisc drzewa hierarchii to pracownik, ktory nie jest niczyim bezposrednim przelozonym. Jego numer nie pojawia sie ani razu jako wartosc w tablicy szef.
Pomysl. Liczymy przez Counter z collections — od razu zyskujemy strukture przydatna w 4.3.
from collections import Counter
# szef[1:] pomija wartownika z pozycji 0
# Counter zlicza ile razy kazdy numer wystepuje jako szef
ile_podwladnych = Counter(szef[1:]) # klucz = numer pracownika, wartosc = liczba podwladnych
# usuwamy 0 — to nie pracownik, tylko znacznik "brak przelozonego"
del ile_podwladnych[0]
# liscie = pracownicy, ktorzy NIE wystepuja jako klucz w Counterze
liscie = 50000 - len(ile_podwladnych)
print("4.2:", liscie)
Co dzieje sie linia po linii:
Counter(szef[1:])— przechodzi raz po wszystkich 50000 wartosciach i tworzy slownik: dla kazdej unikalnej wartosci zapamietuje, ile razy wystapila. Zlozonosc O(n).del ile_podwladnych[0]— wartosc0wszefoznacza prezesa, nie istnieje pracownik o numerze 0. Bez tego usuniecia policzylibysmy "0" jako szefa.len(ile_podwladnych)— liczba unikalnych pracownikow, ktorzy sa czyims szefem. Reszta z 50000 to liscie.
25113 lisci. Polowa korporacji to wykonawcy bez podwladnych — typowy ksztalt drzewa hierarchii. Na 50000 etatow daje 50,2% calej organizacji.
Zadanie 4.3 — pracownik z najwieksza liczba bezposrednich podwladnych
Cala robota zostala juz wykonana w 4.2. Counter trzyma odwzorowanie numer szefa → ile bezposrednich podwladnych. Wystarczy wybrac klucz o najwiekszej wartosci.
# max po kluczach, klucz porownania = wartosc z Countera
najwiekszy_szef = max(ile_podwladnych, key=ile_podwladnych.get)
print("4.3:", najwiekszy_szef, ile_podwladnych[najwiekszy_szef])
Jak dziala max(..., key=...):
max(ile_podwladnych, ...)— iteruje po kluczach slownika (numerach pracownikow).key=ile_podwladnych.get— dla kazdego klucza k pobieraile_podwladnych[k]i to jest wartosc porownywana.- Zwraca klucz, ktory dal najwieksza wartosc. W zadaniu zagwarantowano, ze taki pracownik jest jeden.
2, ma 19 bezposrednich podwladnych. To dyrektor pionu raportujacy bezposrednio do prezesa — 19 bezposrednich raportow to liczba na granicy zarzadzalnosci.
Zadanie 4.4 — maksymalna glebokosc hierarchii
Definicja. Dla pracownika i liczba przelozonych = dlugosc sciezki od i do prezesa. Dla samego prezesa wynosi 0. Szukamy maksimum po wszystkich 50000 pracownikach.
Pomysl naiwny. Dla kazdego pracownika idz w gore przez szef[i], licz kroki az trafisz na 0. W najgorszym razie O(n) na pracownika, O(n²) lacznie. Dla 50000 to 2,5 mld operacji — za wolno przy patologicznym ksztalcie drzewa.
Pomysl wlasciwy — memoizacja. Glebokosc pracownika i = glebokosc jego szefa + 1. Wynik dla kazdego pracownika liczymy raz i zapamietujemy w tablicy glebokosc. Kazda krawedz drzewa jest pokonywana co najwyzej raz, lacznie O(n).
import sys
sys.setrecursionlimit(100000) # zabezpieczenie — domyslny limit (~1000) moze byc za niski
glebokosc = [None] * 50001 # tablica wynikow, None = jeszcze nie policzone
def glab(i):
if szef[i] == 0: # prezes — zero przelozonych
return 0
if glebokosc[i] is not None: # juz policzone — zwracamy z cache
return glebokosc[i]
glebokosc[i] = glab(szef[i]) + 1 # rekurencja w gore + 1 za biezacego szefa
return glebokosc[i]
# liczymy glebokosc dla kazdego pracownika
glebokosci = [glab(i) for i in range(1, 50001)]
# CKE wymaga DWOCH liczb: najwieksza glebokosc oraz ilu pracownikow ma taka glebokosc
maks = max(glebokosci)
ilu_ma_maks = sum(1 for g in glebokosci if g == maks)
print("4.4:", maks, "i", ilu_ma_maks)
Linia po linii:
sys.setrecursionlimit(100000)— Python domyslnie pada przy ~1000 zagniezdzonych wywolan. Ustawiamy z duzym zapasem.glebokosc = [None] * 50001— cache wynikow indeksowany jakszef, pozycja 0 nieuzywana.if szef[i] == 0: return 0— warunek bazowy rekurencji.if glebokosc[i] is not None: return glebokosc[i]— serce memoizacji.glebokosc[i] = glab(szef[i]) + 1— rekurencja w gore plus 1 za krawedz i → szef[i].max(glab(i) for i in range(1, 50001))— generator wyrazen, nie buduje listy w pamieci.
Dlaczego rekurencja dziala szybko mimo 50000 wywolan? Po pierwszym pelnym przejsciu przez dowolna sciezke wszystkie wezly na niej maja juz wynik w cache. Kolejne wywolania krotko schodza do najblizszego policzonego wezla i wracaja. Laczna liczba "prawdziwych" obliczen = liczba krawedzi drzewa = n − 1.
22 i 2 — najwieksza liczba przelozonych nad jednym pracownikiem wynosi 22, a taka liczbe przelozonych ma 2 pracownikow. Dla korpo_przyklad.txt odpowiedz arkusza to 2 i 49997 (drzewo jest tam ekstremalnie plaskie: prezes -> pracownik 3 -> reszta).
glab(i) uzyc petli while szef[j] != 0 liczacej kroki, z memoizacja przez tablice. Bezpieczniejsze przy ekstremalnie glebokich drzewach (przekraczajacych setrecursionlimit), dla 22 poziomow rekurencja jest czytelniejsza.
Trace memoizacji na mini-przykladzie
Zalozmy mikroskopijna hierarchie (8 pracownikow, prezes = 1):
| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| szef[i] | 0 | 1 | 1 | 2 | 2 | 4 | 4 | 6 |
Wywolanie glab(8) schodzi 8 → 6 → 4 → 2 → 1 → bazowy 0. Wypelnia cache: glebokosc[1]=0, [2]=1, [4]=2, [6]=3, [8]=4. Pozniejsze glab(7) robi tylko jeden krok (7 → 4), bo glebokosc[4] juz jest, zwraca 2 + 1 = 3. Bez memoizacji glab(7) przeszedlby cala sciezke 7 → 4 → 2 → 1.
Kompletny skrypt zad4.py
import sys
from collections import Counter
sys.setrecursionlimit(100000)
# --- wczytanie ---
szef = [None]
with open("korpo.txt") as f:
for linia in f:
szef.append(int(linia.strip()))
# --- 4.2: liscie ---
ile_podwladnych = Counter(szef[1:])
del ile_podwladnych[0] # 0 to znacznik prezesa, nie pracownik
liscie = 50000 - len(ile_podwladnych)
print("4.2:", liscie)
# --- 4.3: najwiecej bezposrednich podwladnych ---
najwiekszy_szef = max(ile_podwladnych, key=ile_podwladnych.get)
print("4.3:", najwiekszy_szef, ile_podwladnych[najwiekszy_szef])
# --- 4.4: maksymalna glebokosc ---
glebokosc = [None] * 50001
def glab(i):
if szef[i] == 0:
return 0
if glebokosc[i] is not None:
return glebokosc[i]
glebokosc[i] = glab(szef[i]) + 1
return glebokosc[i]
maks = max(glab(i) for i in range(1, 50001))
print("4.4:", maks)
Oczekiwany output na realnym korpo.txt:
4.2: 25113
4.3: 2 19
4.4: 22
Zlozonosc
| Podpunkt | Operacja | Zlozonosc | Czas dla n = 50000 |
|---|---|---|---|
| 4.2 | Counter na liscie | O(n) | ponizej 100 ms |
| 4.3 | max po slowniku | O(k) gdzie k = liczba szefow | ponizej 50 ms |
| 4.4 | DFS z memoizacja | O(n) | ponizej 200 ms |
Caly skrypt wykonuje sie ponizej sekundy.
Pulapki
Zadanie 4 wyglada na proste — wczytaj plik, policz liscie, znajdz max, zmierz glebokosc. W praktyce kazdy z tych krokow ma swoja minowke. Ponizej siedem realnych pulapek, ktore na egzaminie kosztuja punkty albo daja poprawny wynik dla korpo_przyklad.txt i bledny dla korpo.txt.
1. Indeksowanie od 1 vs od 0
Plik ma 50000 wierszy, ale numeracja zaczyna sie od 1. Naturalna struktura to lista szef dlugosci 50001 z elementem sentynelnym na indeksie 0. Wtedy szef[i] dziala dla pelnego zakresu i = 1..50000 bez przesuniec.
szef = [None] # sentinel pod indeksem 0 — None sygnalizuje "to nie pracownik"
with open("korpo.txt", encoding="utf-8") as f:
for linia in f:
szef.append(int(linia))
# teraz szef[1] to przelozony pracownika 1
Alternatywa szef = [int(x) for x in f] daje liste 0-indeksowana i wymaga ciaglego pisania szef[i-1]. Dziala, ale jeden zapomniany -1 w 4.4 zamienia poprawna 22 na blad indeksu lub wynik o jeden mniejszy.
2. Zero nie jest pracownikiem (najczestszy blad w 4.2/4.3)
Bez del ile[0] w Counterze wygrywa "pracownik 0" z 1 podwladnym (prezesem) — bezsensowny wynik. Z poprawnym filtrem dostajesz pracownika 2 z 19 podwladnymi. Roznica: 8 pkt vs 0 pkt na tym podpunkcie. To samo w 4.2 — set(szef[1:]) bez usuniecia zera daje 25112 zamiast 25113 (wynik o 1 za maly).
3. Limit rekurencji w Pythonie
Domyslny limit to 1000 ramek. Glebokosc drzewa korporacji to 22, wiec mieszczisz sie z zapasem. Ale dla pewnosci (np. gdyby arkusz dal patologiczny test):
import sys
sys.setrecursionlimit(100000)
Bardziej eleganckie wyjscie: iteracyjna petla while szef[i] != 0. Bez rekurencji, bez limitu, bez stack overflow.
4. Memoizacja — bez niej za wolno na patologicznych danych
Naiwne liczenie glebokosci dla kazdego pracownika niezaleznie to O(n · depth). Dla realnego korpo.txt z maksymalna glebokoscia 22 srednia jest jeszcze nizsza, wiec Python ogarnie nawet bez memoizacji. Ale gdy drzewo zdegeneruje sie do lancucha dlugosci n (skrajny test od egzaminatora), masz O(n²/2) = 1,25 mld operacji — czas idzie w dziesiatki sekund. Memoizacja kosztuje 3 linie kodu i sciaga zlozonosc do O(n) niezaleznie od ksztaltu.
5. Liscie topologiczne, nie biznesowe
W 4.2 szukamy pracownikow, ktorych numer nigdy nie pojawia sie jako wartosc szef[j]. To definicja czysto strukturalna — wierzcholek bez dzieci. Nie myl tego z "pracownik szeregowy biznesowo". Prezes (z szef[i]=0) wystepuje jako wartosc szef[j] dla swoich bezposrednich podwladnych, wiec automatycznie wpada do zbioru szefow i nie jest liczony jako lisc.
6. Wczytanie pliku — trailing whitespace
Plik moze miec \r\n na Windowsie, koncowy pusty wiersz, ewentualnie BOM. int(linia) radzi sobie z \n i \r\n. Pusty wiersz na koncu da ValueError — zabezpiecz sie filtrem:
szef = [None]
with open("korpo.txt", encoding="utf-8") as f:
for linia in f:
linia = linia.strip()
if linia:
szef.append(int(linia))
Dla pewnosci sprawdz len(szef) == 50001 przed dalsza praca. Brak jednego wiersza przesuwa wszystkie odpowiedzi.
7. Counter na slice zamiast na calej liscie
Klasyczny blad: Counter(szef) zamiast Counter(szef[1:]). Pierwsza wersja policzy rowniez sentinel z pozycji 0 i doda go jako osobny klucz do Countera. Przy sentinelu None dostaniesz dodatkowy klucz None: 1 — del ile[0] go nie usunie, wiec len(ile_podwladnych) bedzie o 1 wieksze i wynik 4.2 spadnie do 25112. Przy sentinelu 0 Counter doda 1 do klucza 0 (laczna wartosc ile_podwladnych[0] wzrosnie z 1 do 2), ale skoro i tak usuwasz ten klucz przez del, wynik 4.2 wyjdzie poprawnie — za to most_common() w 4.3 pokaze 0 jako lidera. Zawsze pisz Counter(szef[1:]) i niezaleznie wykonaj del ile[0] — dwa zabezpieczenia, jedno przed sentinelem, drugie przed wartoscia 0 reprezentujaca prezesa.
korpo.txt: 4.2 = 25113, 4.3 = 2 z 19 podwladnymi, 4.4 = 22. Jesli kod daje inne liczby, sprawdz po kolei: czy len(szef) to 50001, czy w 4.3 usuniety klucz 0, czy w 4.4 mierzysz lancuch do szef[i]==0 a nie do szef[i]==1.
None na indeksie 0, jeden del ile[0], jedna memoizowana funkcja glebokosci. Trzy linie obrony przed siedmioma pulapkami.
Zadania własne — ćwiczenia
Piec cwiczen w rosnacej trudnosci. Pierwsze trzy sprawdzaja podstawy reprezentacji drzewa przez tablice szefow, dwa ostatnie wymagaja modyfikacji algorytmu z zadan 4.2 i 4.4.
Pokaz odpowiedz
Z tabeli zliczen: pracownicy 3 i 5 maja po 1 podwladnym (odpowiednio 7 i 8). Odpowiedz: 2 pracownikow.
Pokaz odpowiedz
Suma glebokosci dla pracownikow 2..8: 1+1+2+2+2+2+3 = 13. Liczba pracownikow: 7. Srednia: 13/7 ≈ 1,86.
Pokaz odpowiedz
1
/ \
2 3
/|\ /|\
4 5 6 7 8 9
| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| szef[i] | 0 | 1 | 1 | 2 | 2 | 2 | 3 | 3 | 3 |
Czyli szef = [0, 0, 1, 1, 2, 2, 2, 3, 3, 3] (pierwszy 0 to padding indeksu, drugi 0 to szef prezesa).
setrecursionlimit). Wskazowka: petla while w gore drzewa, zliczanie krokow, memoizacja w slowniku.
Pokaz odpowiedz
glebokosc = {}
for start in range(1, 50001):
sciezka = []
j = start
while j != 0 and j not in glebokosc:
sciezka.append(j)
j = szef[j]
baza = glebokosc.get(j, 0) # glebokosc szefa na ktorym sie zatrzymalismy
for k, w in enumerate(reversed(sciezka), start=1):
glebokosc[w] = baza + k
print("4.4:", max(glebokosc.values()))
Pokaz odpowiedz
print(ile_podwladnych.most_common(3))
# Counter ma wbudowana metode most_common(k) — zwraca liste par (klucz, liczba)
Pokaz odpowiedz
from collections import deque, defaultdict
def wczytaj_szefow(plik):
szef = [None]
with open(plik) as f:
for linia in f:
szef.append(int(linia.strip()))
return szef
def zbuduj_dzieci(szef):
dzieci = defaultdict(list)
for i in range(1, len(szef)):
if szef[i] != 0:
dzieci[szef[i]].append(i)
return dzieci
def wszyscy_podwladni(start, dzieci):
wynik = []
kolejka = deque([start])
while kolejka:
biezacy = kolejka.popleft()
for dziecko in dzieci[biezacy]:
wynik.append(dziecko)
kolejka.append(dziecko)
return wynik
szef = wczytaj_szefow("korpo.txt")
dzieci = zbuduj_dzieci(szef)
# znajdz numer prezesa (jedyny pracownik z szef[i]=0)
prezes = next(i for i in range(1, len(szef)) if szef[i] == 0)
podwladni_prezesa = wszyscy_podwladni(prezes, dzieci)
print(f"Prezes (nr {prezes}) ma {len(podwladni_prezesa)} podwladnych lacznie")
Sanity check: dla korpo.txt wynik powinien wynosic 49999 (wszyscy oprocz samego prezesa). Wynika to z dwoch zalozen tresci CKE: drzewo ma dokladnie jeden korzen (jeden wiersz w pliku ma wartosc 0) i jest spojne — kazdy pracownik ma sciezke do prezesa przez relacje szef[]. Te dwa fakty CKE traktuje jako implicite w opisie hierarchii organizacyjnej.
Podsumowanie
Zadanie 4 to test rozpoznawania struktury danych w "ukrytej" reprezentacji. Plik korpo.txt to tablica rodzicow zapisujaca drzewo organizacyjne 50000-osobowej firmy. Trzy liczbowe podpunkty (4.2, 4.3, 4.4) operuja na tej samej tablicy i sprowadzaja sie do trzech operacji: zliczania wartosci nieobecnych (liscie = 25113), znajdowania klucza o najwiekszej liczbie wystapien (pracownik 2 z 19 podwladnymi) oraz memoizowanego liczenia dlugosci sciezek do korzenia (glebokosc 22).
Caly arsenal sprowadza sie do trzech narzedzi: Counter z collections, jedna funkcja rekurencyjna z cache i jedno usuniecie wartownika 0. Skrypt wykonuje sie ponizej sekundy nawet na patologicznych ksztaltach drzewa, o ile dolozysz memoizacje w 4.4 — bez niej naiwne O(n²) moze dac 1,25 mld operacji.
Trzy najczestsze bledy kosztujace punkty: brak del ile[0] w Counterze (zafalszowuje 4.2 o 1 i wskazuje "pracownika 0" w 4.3), nieprawidlowe indeksowanie (pomylenie od 0 vs od 1) oraz mylenie podwladnych z przelozonymi (kierunek strzalki w drzewie vs w tabelce). Trzeba sprawdzic na koniec, czy wyniki zgadzaja sie z wyrocznia: 25113, "2 19", 22.
Reprezentacja parent-array to wzorzec, ktory wraca w innych zadaniach maturalnych — drzewo genealogiczne, struktura katalogow, hierarchia kategorii. Kto opanuje ten szablon raz, dostaje w pakiecie odpowiedzi do wszystkich nastepnych zadan o tym samym ksztalcie.
Maj 2026 — Zadanie 5 (Systemy liczbowe): pełny przewodnik
Wyczerpujący przewodnik do Zadania 5 z arkusza maj 2026 — od intuicji po pełne rozwiązanie z ćwiczeniami.
Wprowadzenie. Systemy pozycyjne od zera
Zadanie 5 z arkusza maturalnego maj 2026 nie jest o arytmetyce. Jest o jednym pomyśle: ta sama liczba może mieć zupełnie różny zapis, w zależności od tego, jaki alfabet cyfr i jakie wagi pozycji ustalimy. Liczba 427 to nie jest "cztery-dwa-siedem". To pewna wartość, którą w systemie dziesiętnym akurat wygodnie zapisuje się jako 427, w piątkowym jako 3202, w trójkowym jako 120211, a w binarnym jako 110101011. Zapis zmienia się — liczba nie.
Cała trudność tego zadania sprowadza się do tego, żeby tę abstrakcję rzeczywiście zrozumieć, a nie tylko nauczyć się formułki. Dlatego zaczniemy od zera, od systemu, który znasz najlepiej.
Jak działa system dziesiętny
Weź zapis 1440. Wartość bierze się nie z cyfr samych w sobie, tylko z ich pozycji. Każda pozycja ma swoją wagę, którą jest potęga dziesiątki:
| Pozycja | Cyfra | Waga | Wkład |
|---|---|---|---|
| 3 (tysiące) | 1 | 103 = 1000 | 1000 |
| 2 (setki) | 4 | 102 = 100 | 400 |
| 1 (dziesiątki) | 4 | 101 = 10 | 40 |
| 0 (jedności) | 0 | 100 = 1 | 0 |
Suma wkładów: 1000 + 400 + 40 + 0 = 1440. Cyfry dozwolone w systemie dziesiętnym to 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 — dziesięć symboli, dlatego "dziesiętny".
Uogólnienie. System o podstawie B
"Dziesięć" nie ma żadnych specjalnych własności matematycznych. Wybrano ją historycznie. Możesz zbudować taki sam mechanizm dla dowolnej podstawy B ≥ 2:
- Cyfry dozwolone: od
0doB-1. Cyfra równa lub większa od B jest nielegalna — to nie jest "dziwna liczba", to po prostu zapis nieprawidłowy. - Waga pozycji k (licząc od prawej, od zera):
Bk. - Wartość liczby: suma
cyfra · Bpozycjapo wszystkich pozycjach.
0, 1, 2, 3, 4. Jeśli zobaczysz zapis typu 14525 — to błąd, taki ciąg nie reprezentuje żadnej liczby w systemie piątkowym. Analogicznie w trójkowym dozwolone są tylko 0, 1, 2, a w binarnym tylko 0, 1.Te same cyfry, inne podstawy
Spójrz na zapis 1440 w trzech różnych systemach. Cyfry identyczne, wartości zupełnie różne:
| System | Rozbicie | Wartość (w dziesiętnym) |
|---|---|---|
| dziesiętny (B=10) | 1·1000 + 4·100 + 4·10 + 0·1 | 1440 |
| piątkowy (B=5) | 1·125 + 4·25 + 4·5 + 0·1 | 245 |
| ósemkowy (B=8) | 1·512 + 4·64 + 4·8 + 0·1 | 800 |
Dlatego podstawę zawsze podaje się jako indeks dolny: 14405, 144010, 14408.
Konwersja: dwa kierunki, dwa algorytmy
Zamiana liczbaB → dziesiętna jest prosta: rozkładasz po pozycjach i sumujesz. Zamiana w drugą stronę, liczba10 → B, wymaga algorytmu dzielenia z resztą:
- Weź liczbę dziesiętną N.
- Podziel przez B. Zapisz iloraz q i resztę r.
- Reszta r jest najmłodszą cyfrą wyniku (skrajnie prawą).
- Iloraz q staje się nową liczbą N. Wróć do kroku 2.
- Powtarzaj, dopóki iloraz nie spadnie do 0.
- Reszty czytasz od ostatniej do pierwszej (od dołu do góry).
Każda liczba naturalna daje się jednoznacznie zapisać w postaci sumy cyfrak · Bk, gdzie cyfry leżą w przedziale 0..B-1. To twierdzenie o jednoznaczności zapisu pozycyjnego. Algorytm "dziel z resztą" znajduje te cyfry od najmłodszej do najstarszej, bo reszta z dzielenia przez B to dokładnie cyfra na pozycji jedności w bazie B.
- Ile cyfr ma system o podstawie 7? (Odp.: 7 — od 0 do 6.)
- Czy zapis
2087jest poprawny? (Odp.: nie — cyfra 8 nielegalna w bazie 7.) - Ile wynosi
10Bw dziesiętnym dla dowolnego B ≥ 2? (Odp.: zawsze B. To jest sens "dziesiątki" — zawsze oznacza podstawę systemu.)
Rozgrzewka. Trace konwersji w obie strony
Zanim wejdziemy w samą równość maturalną, zrobimy cztery konwersje, które domykają wszystkie kierunki używane potem w arkuszu: pozycyjny → dziesiętny przez rozwinięcie wielomianowe oraz dziesiętny → pozycyjny przez dzielenie z resztą.
Konwersja 1: 14405 → dziesiętny
pozycja: 3 2 1 0
cyfra: 1 4 4 0
waga 5^k: 125 25 5 1
wklad: 125 + 100 + 20 + 0 = 245
Wartość: 24510. Cyfry użyte: 1, 4, 4, 0 — wszystkie w zakresie 0-4, więc zapis prawidłowy.
Konwersja 2: 1100023 → dziesiętny
pozycja: 5 4 3 2 1 0
cyfra: 1 1 0 0 0 2
waga 3^k: 243 81 27 9 3 1
wklad: 243 + 81 + 0 + 0 + 0 + 2 = 326
Wartość: 32610.
Konwersja 3: 24510 → piątkowy (kontrola pierwszej)
krok dzielenie iloraz reszta
1. 245 : 5 = 49 r 0 49 0 <- najmlodsza cyfra
2. 49 : 5 = 9 r 4 9 4
3. 9 : 5 = 1 r 4 1 4
4. 1 : 5 = 0 r 1 0 1 <- najstarsza cyfra (STOP)
odczyt reszt od dolu do gory: 1, 4, 4, 0
wynik: 245_10 = 1440_5
Algorytm działa w obie strony — co potwierdza spójność systemu pozycyjnego.
Konwersja 4: 7510 → trójkowy (rozgrzewka pod prawą równość)
krok | dzielnik | iloraz | reszta
-----|----------|--------|-------
1 | 75 / 3 | 25 | 0 <- pozycja 0 (waga 1)
2 | 25 / 3 | 8 | 1 <- pozycja 1 (waga 3)
3 | 8 / 3 | 2 | 2 <- pozycja 2 (waga 9)
4 | 2 / 3 | 0 | 2 <- pozycja 3 (waga 27)
Odczyt reszt od dolu: 2 2 1 0
Wynik: 2210_3
Kontrola wagami: 2·27 + 2·9 + 1·3 + 0·1 = 54 + 18 + 3 + 0 = 75. Zgadza się.
14405 i 1100023 na dziesiętne, potem brakujące liczby z dziesiętnego z powrotem do bazy 5 i bazy 3.
Pełne rozwiązanie krok po kroku
Treść zadania:
1440_5 + ......._5 = 427_10 = ......._3 - 110002_3
Strategia: wszystko sprowadzamy do bazy 10, liczymy, wracamy do bazy docelowej dzieleniem z resztą. Obie równości rozwiązujemy niezależnie.
Lewa równość: 14405 + X5 = 42710
Krok 1. Konwersja 14405 → baza 10
| Pozycja | Cyfra | Waga (5k) | Iloczyn |
|---|---|---|---|
| 3 | 1 | 125 | 125 |
| 2 | 4 | 25 | 100 |
| 1 | 4 | 5 | 20 |
| 0 | 0 | 1 | 0 |
| Suma | 245 | ||
Zatem 14405 = 24510.
Krok 2. Wyznaczenie X w bazie 10
Z równania 245 + X = 427 wynika X = 427 - 245 = 182.
Krok 3. Konwersja 18210 → baza 5
182 : 5 = 36 reszta 2
36 : 5 = 7 reszta 1
7 : 5 = 1 reszta 2
1 : 5 = 0 reszta 1 <- stop (iloraz 0)
Reszty od dolu: 1 2 1 2
Wynik: 18210 = 12125.
Krok 4. Sprawdzenie
| Pozycja | Cyfra | Waga | Iloczyn |
|---|---|---|---|
| 3 | 1 | 125 | 125 |
| 2 | 2 | 25 | 50 |
| 1 | 1 | 5 | 5 |
| 0 | 2 | 1 | 2 |
| Suma | 182 | ||
245 + 182 = 427. Lewa strona zgadza się.
Prawa równość: 42710 = Y3 − 1100023
Krok 1. Konwersja 1100023 → baza 10
| Pozycja | Cyfra | Waga (3k) | Iloczyn |
|---|---|---|---|
| 5 | 1 | 243 | 243 |
| 4 | 1 | 81 | 81 |
| 3 | 0 | 27 | 0 |
| 2 | 0 | 9 | 0 |
| 1 | 0 | 3 | 0 |
| 0 | 2 | 1 | 2 |
| Suma | 326 | ||
Zatem 1100023 = 32610.
Krok 2. Wyznaczenie Y w bazie 10
Z równania 427 = Y − 326 wynika Y = 427 + 326 = 753.
Krok 3. Konwersja 75310 → baza 3
753 : 3 = 251 reszta 0
251 : 3 = 83 reszta 2
83 : 3 = 27 reszta 2
27 : 3 = 9 reszta 0
9 : 3 = 3 reszta 0
3 : 3 = 1 reszta 0
1 : 3 = 0 reszta 1 <- stop
Reszty od dolu: 1 0 0 0 2 2 0
Wynik: 75310 = 10002203.
Krok 4. Sprawdzenie
| Pozycja | Cyfra | Waga | Iloczyn |
|---|---|---|---|
| 6 | 1 | 729 | 729 |
| 5 | 0 | 243 | 0 |
| 4 | 0 | 81 | 0 |
| 3 | 0 | 27 | 0 |
| 2 | 2 | 9 | 18 |
| 1 | 2 | 3 | 6 |
| 0 | 0 | 1 | 0 |
| Suma | 753 | ||
753 − 326 = 427. Prawa strona zgadza się.
Pełne wyrażenie po podstawieniu
1440_5 + 1212_5 = 427_10 = 1000220_3 - 110002_3
245 + 182 = 427 = 753 - 326
12125 oraz 10002203.
Weryfikacja w Pythonie
Prosty program sprawdzający nasze cztery wyniki. Funkcja do_bazy zamienia liczbę dziesiętną na zapis w bazie b przez wielokrotne dzielenie z resztą — czyli dokładnie ten sam algorytm, który robiliśmy ręcznie.
# zamiana liczby dziesietnej n na zapis w bazie b (jako tekst)
def do_bazy(n, b):
if n == 0:
return "0"
cyfry = "0123456789ABCDEF"
wynik = ""
while n > 0:
reszta = n % b
wynik = cyfry[reszta] + wynik
n = n // b
return wynik
# kontrola czterech wynikow z arkusza
print("1440 w bazie 5 =", int("1440", 5)) # oczekiwane 245
print("110002 w bazie 3 =", int("110002", 3)) # oczekiwane 326
print("182 w bazie 5 =", do_bazy(182, 5)) # oczekiwane 1212
print("753 w bazie 3 =", do_bazy(753, 3)) # oczekiwane 1000220
Funkcja int("1440", 5) to wbudowana w Pythonie zamiana tekstu w bazie 5 na liczbę dziesiętną — wygodna do kontroli, bez pisania własnej.
Pułapki — gdzie najczęściej giną punkty
Z dwóch dostępnych punktów statystycznie połowa zdających traci minimum jeden — i prawie zawsze z tego samego powodu: traktują system pozycyjny jak zapis dziesiętny tylko z innym "wyglądem".
Pułapka 1 — cyfry spoza zakresu systemu
System o podstawie B dopuszcza wyłącznie cyfry 0, 1, ..., B-1.
| System (podstawa) | Dozwolone cyfry | Przykład błędu |
|---|---|---|
| Dwójkowy (2) | 0, 1 | 1022 — brak cyfry 2 |
| Trójkowy (3) | 0, 1, 2 | 10003003 |
| Piątkowy (5) | 0, 1, 2, 3, 4 | 15405 |
| Ósemkowy (8) | 0..7 | 788 |
| Szesnastkowy (16) | 0..9, A..F | 1G16 |
Po wykonaniu konwersji przelataj wzrokiem cyfry wyniku i porównaj z dozwolonym zakresem. Trzy sekundy weryfikacji, jeden punkt uratowany.
Pułapka 2 — odwrócona numeracja pozycji
Pozycje cyfr numerujemy od prawej, zaczynając od zera. Skrajna prawa cyfra to B0 = 1, kolejna w lewo to B1, itd. Połowa pomyłek przy konwersji "system B → 10" bierze się z liczenia od lewej albo od jedynki. Najmłodsza cyfra ma zawsze wagę 1, nie B.
Pułapka 3 — reszty czytane od góry zamiast od dołu
Algorytm konwersji "10 → B" przez dzielenie z resztą wypluwa cyfry wyniku od najmniej znaczącej do najbardziej znaczącej. Odwrotne odczytanie daje liczbę-lustrzane odbicie.
182 -> system piatkowy:
182 : 5 = 36 r. 2 <- cyfra pozycji 0
36 : 5 = 7 r. 1 <- cyfra pozycji 1
7 : 5 = 1 r. 2 <- cyfra pozycji 2
1 : 5 = 0 r. 1 <- cyfra pozycji 3 (STOP)
OD DOLU: 1 2 1 2 -> 1212_5 POPRAWNIE
OD GORY: 2 1 2 1 -> 2121_5 BLAD (= 2*125+1*25+2*5+1 = 286, nie 182)
Pułapka 4 — brak weryfikacji w drugą stronę
Konwersja zajmuje 30 sekund, weryfikacja przez konwersję powrotną — kolejne 30 sekund. Po wyliczeniu 75310 = 10002203 wracasz: 1·729 + 0 + 0 + 0 + 2·9 + 2·3 + 0 = 729 + 18 + 6 = 753. Zgadza się — można zapisywać.
Pułapka 5 — mylenie zapisu z wartością
Zapis 10003 wygląda jak "tysiąc", ale to 33 = 27. Analogicznie 1005 = 25, 1016 = 16. Ucz się "wyczuwać" rząd wielkości: 10002203 ma 7 cyfr, więc jego wartość mieści się w przedziale 36..37−1, czyli 729..2186. Wynik 753 mieści się — sanity check zaliczony.
Pułapka 6 — arytmetyka w obcym systemie
Można dodawać i odejmować bezpośrednio w bazie 5 czy 3 z przeniesieniem, ale na maturze to mnożenie ryzyka. Reguła robocza: wszystko konwertuj do dziesiętnego, rachuj w dziesiętnym, dopiero wynik konwertuj do systemu docelowego. Arkusz wręcz tego wymaga — środkowa wartość 427 jest podana w dziesiętnym właśnie po to, żeby odjąć i dodać "po ludzku".
Pułapka 7 — zatrzymanie dzielenia za wcześnie
Dzielimy aż iloraz osiągnie 0, nie aż liczba "wygląda na małą". Ostatni wiersz to zawsze 1 : B = 0 reszta 1.
- Każda cyfra w odpowiedzi mieści się w zakresie
0..B-1? - Konwersja powrotna daje tę samą liczbę dziesiętną?
- Reszty z dzielenia odczytane od dołu?
- Obie równości w łańcuchu spełnione (nie tylko jedna)?
Warianty zadania — systemy liczbowe na maturze
CKE rotuje ten temat od lat. Poniżej mapa wariantów, które realnie padają.
Wariant 1 — BIN ↔ HEX (grupy po 4 bity)
Heksadecymalny to skrót binarnego: jeden znak hex = dokładnie 4 bity (bo 24=16). Zamiana jest mechaniczna, bez dzielenia przez 16.
| HEX | BIN (4 bity) | HEX | BIN (4 bity) |
|---|---|---|---|
| 0 | 0000 | 8 | 1000 |
| 1 | 0001 | 9 | 1001 |
| 2 | 0010 | A | 1010 |
| 3 | 0011 | B | 1011 |
| 4 | 0100 | C | 1100 |
| 5 | 0101 | D | 1101 |
| 6 | 0110 | E | 1110 |
| 7 | 0111 | F | 1111 |
Przykład: 2F716 → 0010 1111 01112 = 75910 (kontrola: 2·256 + 15·16 + 7 = 512 + 240 + 7 = 759). W drugą stronę grupujesz od prawej, brakujące bity na lewo uzupełniasz zerami.
Wariant 2 — BIN ↔ OCT (grupy po 3 bity)
Identyczna mechanika, tylko grupy po 3 (23=8). 7558 = 111 101 1012 = 49310 (klasyczne uprawnienia uniksowe).
Wariant 3 — arytmetyka w nie-dziesiętnym (bez konwersji)
Dodawanie binarne kolumnami z przeniesieniem 2: 1+1=10 (zapis 0, przenoszę 1).
1 0 1 1
+ 0 1 1 1
---------
1 1 0 0 1 (przeniesienia: 1,1,1,0)
W systemie 5 przeniesienie pojawia się przy sumie ≥ 5.
Wariant 4 — U2 (kod uzupełnień do dwóch)
Standard zapisu liczb ujemnych na n bitach. Procedura zamiany dodatniej na ujemną: zaneguj bity, dodaj 1.
| Liczba (8 bitów) | Zapis U2 |
|---|---|
| +5 | 0000 0101 |
| −5 | 1111 1011 (negacja 0000 0101 = 1111 1010, +1 = 1111 1011) |
| −1 | 1111 1111 |
| min na 8 bitach | 1000 0000 = −128 |
Zakres na n bitach: od −2n−1 do 2n−1 − 1. Najstarszy bit = znak (0 dodatnia, 1 ujemna).
Wariant 5 — ile bitów potrzeba na liczbę N
Wzór: najmniejsze k, że 2k > N, czyli k = ⌊log2 N⌋ + 1.
| N | Bity | Bo 2k |
|---|---|---|
| 1000 | 10 | 210=1024 > 1000 |
| 1 000 000 | 20 | 220=1 048 576 |
| 255 | 8 | 28=256, idealnie 8 bitów na bajt |
Dla U2 dodaj 1 bit na znak.
Wariant 6 — shift = mnożenie/dzielenie przez 2
Przesunięcie bitów w lewo o 1 = ×2, w prawo o 1 = ÷2 (całkowite, dla dodatnich).
0001 0110 = 22
<< 1
0010 1100 = 44 (x2)
>> 2 od 22
0000 0101 = 5 (22/4, czesc calkowita)
Uogólnienie: x << n = x · 2n, x >> n = x ÷ 2n. W każdej bazie B: shift = mnożenie przez B.
Zadania własne — ćwiczenia
Sześć zadań rozgrzewkowych. Cel: zautomatyzować trzy schematy — dzielenie przez bazę, rozwinięcie pozycyjne i dodawanie z konwersją przez dziesiętny.
11011012 na system dziesiętny.
Pokaż odpowiedź
| Cyfra | 1 | 1 | 0 | 1 | 1 | 0 | 1 |
|---|---|---|---|---|---|---|---|
| Waga | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
| Wkład | 64 | 32 | 0 | 8 | 4 | 0 | 1 |
Suma: 64 + 32 + 8 + 4 + 1 = 109. Odpowiedź: 109.
20010 na system trójkowy.
Pokaż odpowiedź
200 : 3 = 66 r 2
66 : 3 = 22 r 0
22 : 3 = 7 r 1
7 : 3 = 2 r 1
2 : 3 = 0 r 2
Czytanie od dołu: 21102. Weryfikacja: 2·81 + 1·27 + 1·9 + 0·3 + 2 = 200. Odpowiedź: 211023.
FF16 na system dziesiętny.
Pokaż odpowiedź
W bazie 16 cyfra F oznacza wartość 15. FF16 = 15·16 + 15·1 = 240 + 15 = 255.
1005 + 445 przez konwersję na dziesiętny i wynik podaj w bazie 5.
Pokaż odpowiedź
1005 = 25,445 = 24- Suma:
25 + 24 = 49
49 : 5 = 9 r 4
9 : 5 = 1 r 4
1 : 5 = 0 r 1
Czytanie od dołu: 1445. Weryfikacja: 25 + 20 + 4 = 49.
100010? Odpowiedz bez liczenia całej konwersji.
Pokaż odpowiedź
Liczba cyfr w bazie 2 to floor(log2(n)) + 1. Wystarczy znaleźć najmniejszą potęgę 2 większą od liczby.
| k | 2^k |
|---|---|
| 9 | 512 |
| 10 | 1024 |
Ponieważ 512 < 1000 < 1024, najwyższa cyfra binarna ma wagę 2^9. Cyfr jest 9 + 1 = 10. Pełna konwersja: 100010 = 11111010002.
2710 na bazy 2, 3, 4, 5, 8, 16. Zwróć uwagę na liczbę cyfr w każdej reprezentacji.
Pokaż odpowiedź
| Baza | Reprezentacja | Liczba cyfr | Weryfikacja |
|---|---|---|---|
| 2 | 11011 | 5 | 16+8+2+1 = 27 |
| 3 | 1000 | 4 | 1·27 = 27 (czysta potęga trójki) |
| 4 | 123 | 3 | 1·16 + 2·4 + 3 = 27 |
| 5 | 102 | 3 | 1·25 + 0·5 + 2 = 27 |
| 8 | 33 | 2 | 3·8 + 3 = 27 |
| 16 | 1B | 2 | 1·16 + 11 = 27 (B = 11) |
Im wyższa baza, tym krótsza reprezentacja. To intuicja stojąca za projektowaniem systemów: szesnastkowy używamy w informatyce, bo jedna cyfra hex koduje dokładnie 4 bity (16 = 2^4), więc bajt zapisujemy dwoma znakami zamiast ośmioma.
Sprawdzenie programem — pętla po sześciu bazach woła funkcję do_bazy i wypisuje wynik.
# zamiana liczby dziesietnej n na zapis w bazie b (jako tekst)
def do_bazy(n, b):
if n == 0:
return "0"
cyfry = "0123456789ABCDEF"
wynik = ""
while n > 0:
reszta = n % b
wynik = cyfry[reszta] + wynik
n = n // b
return wynik
# lista baz do sprawdzenia
bazy = [2, 3, 4, 5, 8, 16]
for b in bazy:
print("baza", b, "->", do_bazy(27, b))
3205 + ...5 = 10010.
Pokaż odpowiedź
3205 = 3·25 + 2·5 + 0 = 85. 100 − 85 = 15. 15:5=3 r0; 3:5=0 r3. Reszty od dołu: 305.
Zadania własne — do samodzielnego rozwiązania
Sesc cwiczen — od konwersji systemow po zakresy liczb o zadanej liczbie cyfr binarnych. Rob na kartce, kalkulator tylko do dzielenia z reszta. Kod Python uruchom w IDLE, jesli masz watpliwosci.
Cwiczenie 1 (rozgrzewka)
Zamien liczbe 25 na system dwojkowy metoda dzielenia z reszta. Ile cyfr ma jej zapis binarny?
Odpowiedz
Dzielimy przez 2 i spisujemy reszty od dolu:
25 : 2 = 12 reszta 1
12 : 2 = 6 reszta 0
6 : 2 = 3 reszta 0
3 : 2 = 1 reszta 1
1 : 2 = 0 reszta 1
Zapis od dolu: 11001. Liczba cyfr: 5.
Sprawdzenie: 16 + 8 + 1 = 25. Zgadza sie.
Cwiczenie 2 (rozgrzewka)
Zamien liczbe 42 na system piatkowy. Podaj sume jej cyfr w tym systemie.
Odpowiedz
42 : 5 = 8 reszta 2
8 : 5 = 1 reszta 3
1 : 5 = 0 reszta 1
Zapis piatkowy: 132. Suma cyfr: 1 + 3 + 2 = 6.
Sprawdzenie: 1*25 + 3*5 + 2 = 25 + 15 + 2 = 42.
Cwiczenie 3 (srednie)
Liczba 300 dziesietnie. Podaj jej zapis dwojkowy, podaj liczbe cyfr i ile z tych cyfr to jedynki.
Odpowiedz
300 : 2 = 150 r 0
150 : 2 = 75 r 0
75 : 2 = 37 r 1
37 : 2 = 18 r 1
18 : 2 = 9 r 0
9 : 2 = 4 r 1
4 : 2 = 2 r 0
2 : 2 = 1 r 0
1 : 2 = 0 r 1
Zapis (od dolu): 100101100. Liczba cyfr: 9. Liczba jedynek: 4.
Sprawdzenie: 256 + 32 + 8 + 4 = 300.
Cwiczenie 4 (srednie)
Napisz krotki program w Pythonie, ktory dla liczby 427 wypisze jej zapis w systemie piatkowym oraz sume cyfr w tym zapisie.
Odpowiedz
# konwersja 427 na system piatkowy
liczba = 427
cyfry = []
while liczba > 0:
reszta = liczba % 5
cyfry.append(reszta)
liczba = liczba // 5
# cyfry sa od najmlodszej, odwracamy do druku
zapis = ""
for i in range(len(cyfry) - 1, -1, -1):
zapis = zapis + str(cyfry[i])
# suma cyfr
suma = sum(cyfry)
print("Zapis piatkowy:", zapis)
print("Suma cyfr:", suma)
Wynik: zapis piatkowy 3202, suma cyfr 7.
Cwiczenie 5 (srednie/trudniejsze)
Podaj najmniejsza i najwieksza liczbe naturalna, ktora w zapisie dwojkowym ma dokladnie 7 cyfr. Uzasadnij krotko.
Odpowiedz
Najmniejsza 7-cyfrowa liczba binarna to 1000000 = 26 = 64.
Najwieksza 7-cyfrowa liczba binarna to 1111111 = 27 - 1 = 127.
Wniosek ogolny: liczba o n cyfrach binarnych miesci sie w zakresie od 2n-1 do 2n - 1.
Dla 9 cyfr (jak w zadaniu 5.1): od 28 = 256 do 29 - 1 = 511.
Cwiczenie 6 (maturalne)
Wsrod liczb od 1 do 1000 znajdz, ile jest takich, ktorych zapis dwojkowy ma dokladnie 8 cyfr. Rozwiaz dwoma sposobami: rachunkiem na kartce oraz programem w Pythonie.
Odpowiedz
Sposob 1 — kartka. Zapis 8-cyfrowy maja liczby od 27 = 128 do 28 - 1 = 255. Wszystkie te liczby miesza sie w przedziale 1..1000, wiec ilosc to 255 - 128 + 1 = 128.
Sposob 2 — Python.
# liczymy ile liczb z zakresu 1..1000 ma 8-cyfrowy zapis dwojkowy
ile = 0
for liczba in range(1, 1001):
n = liczba
cyfr = 0
# zliczamy cyfry binarne przez dzielenie przez 2
while n > 0:
cyfr = cyfr + 1
n = n // 2
if cyfr == 8:
ile = ile + 1
print("Ile liczb:", ile)
Wynik: 128.
Wskazówki dla prowadzącego
Pytania diagnostyczne na początek
- Co dokładnie znaczy, że liczba 427 ma 3 cyfry w systemie dziesiętnym? (cyfry to 4, 2, 7 — każda warta 100, 10, 1)
- Jakie wagi mają kolejne pozycje w systemie dwójkowym, licząc od prawej? (1, 2, 4, 8, 16, 32, 64, 128, 256, 512...)
- Ile to jest 2 do potęgi 8 i 2 do potęgi 9? (256 i 512)
- Jak zamienić liczbę dziesiętną na inny system — metodą dzielenia z resztą czy metodą odejmowania potęg? (obie działają, ucz dzielenia z resztą — jest bardziej mechaniczna)
- Jakie cyfry występują w systemie piątkowym? (tylko 0, 1, 2, 3, 4 — nigdy 5)
- Jeśli liczba w systemie dwójkowym ma 9 cyfr, to jaka jest najmniejsza i największa taka liczba dziesiętnie? (najmniejsza 100000000 = 256, największa 111111111 = 511)
Typowe błędy uczniów
- Czytanie zapisu dwójkowego od złej strony — uczeń liczy wagi 1, 2, 4 od lewej zamiast od prawej. Rozpoznasz po wyniku różniącym się radykalnie od oryginału (np. dostaje 853 zamiast 427). Korekta: zawsze podpisuj wagi POD cyframi, zaczynając od 1 po prawej.
- Używanie cyfry 5 w systemie piątkowym — przy dzieleniu 427 przez 5 uczeń zapisuje resztę 5 zamiast przenieść. Rozpoznasz po cyfrze 5 w wyniku. Korekta: w systemie o podstawie b cyfry są zawsze od 0 do b-1.
- Mylenie liczby cyfr z wartością — przy 5.3 uczeń mówi "liczba musi być większa od 9" zamiast "musi mieć wartość 256-511". Korekta: pytaj o KONKRETNE potęgi dwójki, nie o "dużo cyfr".
- Błędy arytmetyczne przy dzieleniu z resztą — szczególnie przy dzieleniu przez 5 uczeń pomija krok lub źle liczy resztę. Korekta: rób tabelkę z trzema kolumnami: dzielna, iloraz, reszta.
- Brak weryfikacji wyniku — uczeń oddaje zapis 110101011 bez sprawdzenia. Korekta: zawsze cofnij się — policz 256+64+16+8+2+1 i sprawdź czy wychodzi 427.
Propozycja podziału 45-minutowej lekcji
| Minuty | Czynność | Cel pedagogiczny |
|---|---|---|
| 0-5 | Pytania diagnostyczne — szybka rozmowa o wagach pozycyjnych | Sprawdzenie czy uczeń rozumie zapis pozycyjny w ogóle |
| 5-15 | Wspólna zamiana 427 na system dwójkowy metodą dzielenia z resztą na tablicy | Wyrobienie mechanicznego nawyku — każdy krok widoczny |
| 15-22 | Weryfikacja przez sumowanie potęg dwójki (256+64+16+8+2+1=427) | Pokazanie że zapis dwójkowy to suma potęg — drugi punkt widzenia |
| 22-32 | Zamiana 427 na system piątkowy + zliczenie sumy cyfr (3202, suma=7) | Przenoszenie techniki dzielenia z resztą na inną podstawę |
| 32-40 | Rozwiązanie 5.3 — dyskusja o zakresie 256-511 i zapisie nierówności | Połączenie liczby cyfr z zakresem liczbowym, myśl matematyczna |
| 40-45 | Krótki sprawdzian — każdy zamienia 200 na dwójkowy w zeszycie | Utrwalenie i diagnoza kto wymaga dodatkowej pracy |
Czego NIE robić
- Nie pokazuj od razu gotowego zapisu 110101011 — uczeń ma sam wyprowadzić każdą cyfrę przez dzielenie z resztą na tablicy. Inaczej zapamięta wynik, nie metodę.
- Nie wprowadzaj kalkulatora programisty ani funkcji bin() z Pythona — matura jest na kartce, uczeń musi umieć ręcznie.
- Nie tłumacz 5.3 przez wzory typu "2^(n-1) do 2^n - 1" zanim uczeń sam zauważy, że 9-cyfrowa najmniejsza to 100000000 a największa 111111111. Najpierw konkrety, potem uogólnienie.
Maj 2026 — Zadanie 6 (Sieci komputerowe (IPv4/IPv6)): pełny przewodnik
Wyczerpujący przewodnik do Zadania 6 z arkusza maj 2026 — od intuicji po pełne rozwiązanie z ćwiczeniami.
Wprowadzenie: po co istnieje adresacja IP
Siec komputerowa to zbior urzadzen, ktore musza sie nawzajem znajdowac. Twoj laptop pisze do serwera Wikipedii w innym kraju, telefon pyta o pogode u Google, drukarka biurowa wola "jestem gotowa" do komputera kolegi. Zeby pakiet danych trafil we wlasciwe miejsce, kazde urzadzenie potrzebuje adresu — identyfikatora unikalnego w obrebie sieci (lokalnej albo globalnego internetu).
Tym adresem jest adres IP (Internet Protocol). Pelni te sama role co numer telefonu albo adres pocztowy: identyfikuje punkt koncowy komunikacji. Gdy w przegladarce wpisujesz wikipedia.org, DNS tlumaczy te nazwe na adres IP (np. 208.80.154.224) i dopiero ten adres trafia do naglowkow pakietow.
Dwie wersje protokolu: IPv4 i IPv6
Adresow IP sa dzis dwie wersje uzywane rownolegle:
- IPv4 — wersja czwarta, projektowana w latach 70-tych, wdrozona produkcyjnie w 1983. Adres ma 32 bity. Daje to maksymalnie 232 ≈ 4,3 mld unikalnych adresow. Gdy projektowano IPv4, 4 miliardy wydawaly sie liczba astronomiczna. Dzis smartfon, lodowka, opaska fitness i kamera bezpieczenstwa — kazde urzadzenie chce wlasny adres. Pula IPv4 zostala wyczerpana na poziomie IANA w lutym 2011, regionalnie w kolejnych latach (RIPE w Europie ok. 2012).
- IPv6 — nastepca. Pierwsza specyfikacja w RFC 1883 (1995), rozszerzona w RFC 2460 (1998), obecnie obowiazuje RFC 8200 (2017). Adres ma 128 bitow. Daje to 2128 ≈ 3,4 · 1038 adresow — liczba na tyle duza, ze na kazde ziarno piasku na Ziemi (~1020) przypada okolo 1018 adresow, a na kazdy atom typowego ludzkiego ciala (~1027) — kilkaset adresow.
Przyklady i rozklad na bity
Format zapisu IPv4
32 bity dzielimy na 4 oktety po 8 bitow. Kazdy oktet zapisujemy jako liczbe dziesietna z zakresu 0-255 (bo 28 = 256 wartosci), oddzielajac kropkami:
192.168.0.1
8.8.8.8 (Google Public DNS)
127.0.0.1 (localhost — adres petli zwrotnej)
255.255.255.255 (broadcast)
Rozklad na bity dla 192.168.0.1:
192 .168 . 0 . 1
11000000 . 10101000 . 00000000 . 00000001
\______/ \______/ \______/ \______/
8 bitow 8 bitow 8 bitow 8 bitow = 32 bity razem
Jak zamienic 192 na bity w glowie? Najprosciej tabelka potegi dwojki od lewej:
bit: 128 64 32 16 8 4 2 1
192 = 1 1 0 0 0 0 0 0 (bo 128 + 64 = 192)
168 = 1 0 1 0 1 0 0 0 (bo 128 + 32 + 8 = 168)
Bierzesz najwieksza potege ktora "wchodzi" w liczbe, odejmujesz, idziesz dalej. Reszta to zera.
Format zapisu IPv6
128 bitow dzielimy na 8 grup po 16 bitow. Kazda grupe zapisujemy czterocyfrowo w systemie szesnastkowym (HEX, cyfry 0-9 i litery a-f), oddzielajac dwukropkami:
2001:0db8:85a3:0000:0000:8a2e:0370:7334
\__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/
16 16 16 16 16 16 16 16 bitow = 128 bitow razem
HEX zamiast dziesietnie, bo 4 cyfry HEX = dokladnie 16 bitow (kazda cyfra HEX koduje 4 bity), wiec zapis jest zwarty i mapuje sie 1:1 na bity. Adres dziesietny IPv6 bylby koszmarnie dlugi.
Skracanie zapisu IPv6 — RFC 5952 dopuszcza dwa skroty:
- Wiodace zera w grupie mozna pominac:
0db8→db8,0000→0 - Jeden ciag kolejnych grup zerowych mozna zastapic
::(tylko raz w adresie, bo inaczej byloby niejednoznaczne ile zer)
Pelny: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
Skrocony: 2001:db8:85a3::8a2e:370:7334
Pelny: 0000:0000:0000:0000:0000:0000:0000:0001
Skrocony: ::1 (localhost w IPv6)
Pelne rozwiazanie — tabela zbiorcza
| Wersja | Dlugosc adresu | Liczba bajtow | Zapis | Liczba unikalnych adresow |
|---|---|---|---|---|
| IPv4 | 32 bity | 4 bajty (4 × 8 bitow) | 4 liczby 0-255 oddzielone kropkami | 232 ≈ 4,29 · 109 |
| IPv6 | 128 bitow | 16 bajtow (8 × 16 bitow) | 8 grup po 4 cyfry hex, oddzielone dwukropkami | 2128 ≈ 3,40 · 1038 |
Liczby graniczne do zapamietania na matury
| Wartosc | Bity | Co to jest |
|---|---|---|
| 1 bajt | 8 | jeden oktet IPv4, jedna liczba 0-255 |
| port TCP/UDP | 16 | zakres 0-65535 |
| IPv4 | 32 | 4 bajty |
| adres MAC | 48 | 6 bajtow, np. 00:1A:2B:3C:4D:5E (warstwa 2 OSI) |
| IPv6 | 128 | 16 bajtow |
Dlaczego rozmiar adresu ma znaczenie
Kazdy bit naglowka IP podrozuje w kazdym pakiecie razem z danymi. IPv6 ma adres 4× dluzszy, wiec naglowek IPv6 jest wiekszy niz IPv4 — co dla bardzo krotkich pakietow daje niewielki narzut. W zamian dostajemy:
- Praktycznie nieskonczona pule adresow (kazde urzadzenie IoT moze miec wlasny adres publiczny, bez NAT-u)
- Prostszy naglowek (czesc pol z IPv4 zostala usunieta lub przeniesiona do naglowkow rozszerzen)
- Wbudowane mechanizmy autokonfiguracji (SLAAC) oraz wsparcie dla IPsec — w pierwotnej specyfikacji wymagane do implementacji (mandatory), od RFC 6434 (2011) zlagodzone do recommended
Szerszy kontekst — co CKE doklada do dzialu sieci
Sama dlugosc adresu (32 vs 128 bitow) to fakt do zapamietania, ale arkusze maturalne regularnie miesza pytanie o IPv4/IPv6 z szerszym pakietem: maski podsieci, adresy prywatne, NAT, DNS, warstwy OSI. Wiedza tylko "32 i 128" da punkt za Zad. 6, ale obok moze pasc Zad. 7 wymagajace policzenia, ile hostow miesci sie w /26. Ponizej caly ten kontekst.
Maska podsieci — zapis /N
Zapis CIDR (Classless Inter-Domain Routing) 192.168.1.0/24 oznacza: pierwsze 24 bity adresu identyfikuja siec, pozostale 8 bitow identyfikuje hosta w tej sieci. 32 − 24 = 8 bitow na hosta, czyli 28 = 256 adresow, z czego 2 zarezerwowane (adres sieci i broadcast) → 254 uzytkowych hostow.
| CIDR | Maska dziesietnie | Bity hosta | Hostow uzytkowych | Typowe zastosowanie |
|---|---|---|---|---|
| /8 | 255.0.0.0 | 24 | 16 777 214 | Stara klasa A, RFC 1918 (10.0.0.0/8) |
| /16 | 255.255.0.0 | 16 | 65 534 | Srednia siec, 172.16.0.0/16 |
| /24 | 255.255.255.0 | 8 | 254 | Typowa siec domowa / male biuro |
| /26 | 255.255.255.192 | 6 | 62 | Mala podsiec (np. dzial firmy) |
| /30 | 255.255.255.252 | 2 | 2 | Link point-to-point miedzy routerami |
Wzor ogolny: podsiec /n ma 2(32−n) adresow, z czego 2(32−n) − 2 uzytecznych (oprocz /31 i /32, ktore maja reguly specjalne — link point-to-point).
Adresy prywatne IPv4 (RFC 1918)
To pule ktore nie sa routowane w publicznym Internecie — kazdy moze ich uzywac u siebie w domu / firmie bez kolizji. Trzy zakresy do zapamietania:
| Zakres | CIDR | Liczba adresow | Gdzie spotykasz |
|---|---|---|---|
| 10.0.0.0 — 10.255.255.255 | 10.0.0.0/8 | ~16,7 mln | Duze firmy, korporacje |
| 172.16.0.0 — 172.31.255.255 | 172.16.0.0/12 | ~1 mln | Srednie sieci, Docker (default bridge 172.17.0.0/16) |
| 192.168.0.0 — 192.168.255.255 | 192.168.0.0/16 | ~65 tys | Domowy router (192.168.0.1 / 192.168.1.1) |
192.168.x.x lub 10.x.x.x — to adres prywatny, host nie jest bezposrednio widoczny z Internetu, ruch wychodzi przez NAT na routerze.
Adresy prywatne i specjalne IPv6
W IPv6 odpowiednikiem RFC 1918 sa adresy ULA — Unique Local Address w zakresie fc00::/7 (praktycznie fd00::/8). Drugi wazny zakres to link-local fe80::/10 — adres ktory kazdy interfejs IPv6 dostaje automatycznie (analog APIPA 169.254.x.x w IPv4). Loopback w IPv6 to ::1.
NAT — po co istnieje
NAT (Network Address Translation) tlumaczy adres prywatny na publiczny gdy pakiet wychodzi z sieci LAN do Internetu. Powod: IPv4 ma tylko ~4,3 mld adresow, a urzadzen jest okolo 30 mld → nie wystarczy. Domowy router ma jeden publiczny IPv4 od ISP i obsluguje 10-50 urzadzen w LAN przez NAT.
Laptop 192.168.1.42:50000 --[NAT na routerze]--> 83.12.45.6:50000 --> google.com:443
router zapamietuje mapowanie w tablicy NAT
odpowiedz 83.12.45.6:50000 <--[NAT odwraca]-- Laptop 192.168.1.42:50000
W IPv6 NAT nie jest potrzebny — adresow jest 2128, kazde urzadzenie moze miec swoj publiczny adres globalny.
DNS — tlumaczenie nazw na adresy
DNS (Domain Name System) zamienia nazwe domeny (google.com) na adres IP (142.250.74.110 dla IPv4 lub 2a00:1450:401b:80a::200e dla IPv6). Bez DNS musialbys pamietac liczby zamiast nazw. Typowe rekordy: A (IPv4), AAAA (IPv6, "quad-A"), MX (poczta), CNAME (alias), TXT (np. SPF/DKIM).
Warstwy OSI — gdzie siedzi IP
| Warstwa | Nazwa | Przyklady |
|---|---|---|
| 7 | Aplikacji | HTTP, HTTPS, FTP, SMTP, DNS |
| 6 | Prezentacji | TLS/SSL, kodowanie znakow |
| 5 | Sesji | Zarzadzanie sesjami (rzadko explicit) |
| 4 | Transportowa | TCP, UDP (porty) |
| 3 | Sieciowa | IP (IPv4, IPv6), ICMP, routing |
| 2 | Lacza danych | Ethernet, MAC, ARP, Wi-Fi |
| 1 | Fizyczna | Kable, swiatlowod, fale radiowe |
Pulapki egzaminacyjne
Zadanie wyglada jak gimme — dwie liczby do zapamietania. Mimo to co roku czesc zdajacych traci ten punkt na trywialnych pomylkach jednostek albo na podaniu liczby cyfr zamiast bitow. CKE jest tu bezwzgledne — jedna pomylka jednostki i 0 punktow.
Pulapka 1: bity vs bajty
Najczestszy blad. IPv4 ma 32 bity, co rowna sie 4 bajtom. Czesc zdajacych pisze "32 bajty" — to 256 bitow, czyli wiecej niz IPv6.
| Protokol | Bity | Bajty | Cyfry hex w pelnym zapisie |
|---|---|---|---|
| IPv4 | 32 | 4 | 8 (32 bity / 4 bity na cyfre) |
| IPv6 | 128 | 16 | 32 (128 bitow / 4 bity na cyfre) |
Pulapka 2: pomylka 6 ↔ 4 w nazwie
Liczba w nazwie protokolu (IPv4, IPv6) to numer wersji, nie liczba bajtow. Czesc uczniow rozumuje "IPv6 = 6 bajtow" — to bzdura. IPv6 ma 16 bajtow (128 bitow), IPv4 ma 4 bajty (32 bity). Zbieznosc IPv4 = 4 bajty jest przypadkowa i mnemonicznie mylaca. Inny wariant tego samego bledu: "IPv4 = 4 bity, IPv6 = 6 bitow" — totalny fail.
Pulapka 3: 192.168.1.1 to nie "4 bity"
Notacja kropkowa IPv4 sklada sie z 4 liczb dziesietnych z zakresu 0-255. Kazda z tych liczb zajmuje 8 bitow (1 bajt). Razem 4 × 8 = 32 bity. Liczba 255 miesci sie dokladnie w 8 bitach (binarnie 11111111) — stad gorne ograniczenie oktetu.
192 . 168 . 1 . 1
| | | |
8 bit 8 bit 8 bit 8 bit -> razem 32 bity
Pulapka 4: liczba znakow w zapisie vs liczba bitow
Nie myl liczby bitow z liczba znakow w zapisie. Adres IPv4 "192.168.1.1" ma 11 znakow tekstu, ale 32 bity danych. Adres IPv6 moze miec do 39 znakow w pelnym zapisie, ale zawsze 128 bitow.
Pulapka 5: maska podsieci to bity, nie bajty
Zapis /24 oznacza, ze pierwsze 24 bity adresu identyfikuja siec, a pozostale 8 bitow (32 − 24) — hosta. To nie 24 bajty ani 24 cyfry. W IPv6 spotkasz np. /64 — 64 bity prefiksu sieciowego.
Pulapka 6: MAC vs IP
00:1A:2B:3C:4D:5E) to warstwa 2 OSI — identyfikuje karte sieciowa w obrebie jednej sieci lokalnej. IP adres to warstwa 3 — identyfikuje host w globalnej sieci. MAC sie nie zmienia (zaszyty w karcie), IP moze sie zmieniac (DHCP, roaming).
Zadania własne — ćwiczenia
Pokaz odpowiedz
IPv4 = 32 bity, IPv6 = 128 bitow.Pokaz odpowiedz
IPv6 = 128 / 8 = 16 bajtow. IPv4 = 32 / 8 = 4 bajty.
Sprawdzenie na zapisie IPv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 to 8 grup po 4 cyfry hex. Kazda cyfra hex = 4 bity, grupa = 16 bitow = 2 bajty. 8 × 2 = 16 bajtow.
Pokaz odpowiedz
4 oktety, kazdy w zakresie 0-255 (bo 28 = 256 wartosci, od 0 do 255 wlacznie).Pokaz odpowiedz
8 grup po 16 bitow (4 cyfry hex × 4 bity = 16 bitow na grupe). 8 × 16 = 128 bitow.2001:0db8:0000:0000:0000:ff00:0042:8329.
Pokaz odpowiedz
2001:db8::ff00:42:8329 — pomijamy wiodace zera w kazdej grupie, a ciag trzech grup zerowych zastepujemy ::.
Pokaz odpowiedz
232 = 4 294 967 296 (okolo 4,3 mld).
2^10 = 1024 (~10^3)
2^20 = 1 048 576 (~10^6)
2^30 = 1 073 741 824 (~10^9)
2^32 = 4 * 2^30 = 4 294 967 296
Czesc puli jest zarezerwowana (loopback, multicast, prywatne, broadcast) — pula routowalna globalnie jest mniejsza. To wlasnie wymusilo migracje na IPv6.
Pokaz odpowiedz
2128 / 232 = 296 ≈ 7,9 · 1028 razy wiecej. Dla porownania: liczba ziaren piasku na Ziemi szacowana jest na ~1020 — pula IPv6 jest o 8 rzedow wieksza.10.0.0.0/24? Ile z nich jest uzytecznych dla hostow?
Pokaz odpowiedz
256 adresow lacznie, 254 uzyteczne. Maska /24 → 8 bitow hosta → 28 = 256.
| Adres | Rola |
|---|---|
10.0.0.0 | Adres sieci — nie przypisujesz hostowi |
10.0.0.1 ... 10.0.0.254 | 254 uzyteczne adresy hostow |
10.0.0.255 | Adres broadcast — nie przypisujesz hostowi |
192.168.10.0/27?
Pokaz odpowiedz
32 − 27 = 5 bitow hosta → 25 = 32 adresy → 32 − 2 (siec + broadcast) = 30 hostow uzytkowych.192.168.1.1 — publiczny czy prywatny? Powolaj sie na standard.
Pokaz odpowiedz
Prywatny. Wpada w pule 192.168.0.0/16 zarezerwowana w RFC 1918 do uzytku wewnatrz sieci lokalnych. Typowy adres bramy domowego routera. Wyjscie do Internetu odbywa sie przez NAT na publicznym adresie przydzielonym przez ISP.
172.20.5.100 — prywatny czy publiczny?
Pokaz odpowiedz
Prywatny. Miesci sie w zakresie 172.16.0.0 — 172.31.255.255 (RFC 1918, 172.16.0.0/12). Nie jest routowany w Internecie.127.0.0.1 i do czego sluzy?
Pokaz odpowiedz
Localhost — petla zwrotna (loopback). Adres wskazujacy na wlasna maszyne. Pakiet wyslany na 127.0.0.1 nie opuszcza karty sieciowej — system operacyjny zwraca go lokalnie przez interfejs lo.
Cala pula 127.0.0.0/8 jest zarezerwowana jako loopback (16 777 216 adresow), choc w praktyce uzywa sie prawie wylacznie 127.0.0.1. W IPv6 odpowiednikiem jest ::1.
Zastosowania:
- testy aplikacji lokalnie (
http://127.0.0.1:8000,http://localhost:3000), - komunikacja miedzy procesami na tej samej maszynie (baza danych nasluchujaca tylko na loopbacku = niedostepna z zewnatrz),
- diagnostyka stacku TCP/IP —
ping 127.0.0.1sprawdza, czy stos sieciowy w systemie w ogole dziala.
Pokaz odpowiedz
TCP — warstwa 4 (transportowa). IP — warstwa 3 (sieciowa). TCP segmentuje dane i gwarantuje dostarczenie, IP adresuje i trasuje pakiety.example.com?
Pokaz odpowiedz
Rekord AAAA (quad-A). Rekord A zwraca IPv4, AAAA zwraca IPv6 (cztery razy wiecej bitow = cztery litery A w nazwie).Kluczowe liczby do zapamietania
IPv4 to 32 bity (4 bajty, 4 oktety dziesietne 0-255). IPv6 to 128 bitow (16 bajtow, 8 grup szesnastkowych po 16 bitow). Te dwie liczby to caly wymagany zakres na arkuszu, ale stoi za nimi cala arytmetyka adresacji: pula 232 wyczerpana ok. 2011 r. wymusila wdrozenie IPv6 z pula 2128, a obliczenia masek CIDR (/24 → 254 hosty, /27 → 30 hostow) bazuja na regule 2(32−n) − 2.
Na maturze pilnuj jednostki — odpowiedz "32 bajty" zamiast "32 bity" kosztuje punkt rownie skutecznie jak "IPv6 ma 6 bitow". Drugi pewny sposob na utrate punktu to mylenie warstw: MAC (48 bitow, warstwa 2) jednoznacznie identyfikuje karte sieciowa w LAN, IP (warstwa 3) — host w globalnej sieci. Te dwa adresy zyja w roznych warstwach modelu OSI i pelnia rozne role.
Po opanowaniu Zad. 6 jestes przygotowany na cala rodzine pytan z dzialu sieci: obliczenia podsieci CIDR, rozpoznawanie pul prywatnych RFC 1918, adresy specjalne (loopback, broadcast, link-local), role NAT i DNS oraz mapowanie protokolow na warstwy OSI. To trzy najczestsze typy pytan sieciowych w arkuszu CKE i wszystkie sprowadzaja sie do swiadomego liczenia bitow.
Zadania własne — do samodzielnego rozwiązania
Konwersje binarne, maski sieci i porownywanie przestrzeni adresowych IPv4 vs IPv6. Kazde cwiczenie trenuje inny aspekt zadania 6.
Cwiczenie 1 (rozgrzewka, 2 min)
Zamien liczbe dziesietna 192 na zapis binarny (8 bitow).
Odpowiedz
Dzielimy przez 2 i zapisujemy reszty od dolu:
192 : 2 = 96 r 0
96 : 2 = 48 r 0
48 : 2 = 24 r 0
24 : 2 = 12 r 0
12 : 2 = 6 r 0
6 : 2 = 3 r 0
3 : 2 = 1 r 1
1 : 2 = 0 r 1
Czytajac reszty od dolu: 11000000.
Sprawdzenie: 128 + 64 = 192. Zgadza sie.
Cwiczenie 2 (rozgrzewka, 2 min)
Ile roznych adresow mozna zapisac na 4 bitach? A na 8 bitach?
Odpowiedz
Na n bitach mamy 2^n kombinacji.
- 4 bity: 2^4 = 16 adresow
- 8 bitow: 2^8 = 256 adresow
Cwiczenie 3 (srednie, 5 min)
Adres IPv4 ma maske /26. Ile bitow przeznaczono na czesc hosta i ile maksymalnie hostow miesci sie w tej sieci (liczba wszystkich adresow, bez odejmowania sieci i broadcastu)?
Odpowiedz
IPv4 ma 32 bity. Maska /26 oznacza 26 bitow sieci.
Bity hosta: 32 - 26 = 6.
Liczba adresow: 2^6 = 64.
(Gdyby pytano o uzyteczne hosty, odjelibysmy 2 na adres sieci i broadcast: 64 - 2 = 62.)
Cwiczenie 4 (srednie, 7 min)
Zamien adres IPv4 10.0.5.130 na zapis binarny (4 oktety po 8 bitow).
Odpowiedz
Konwertujemy kazdy oktet osobno:
10 = 00001010 (8 + 2)
0 = 00000000
5 = 00000101 (4 + 1)
130 = 10000010 (128 + 2)
Caly adres: 00001010.00000000.00000101.10000010.
Cwiczenie 5 (srednie, 8 min)
Napisz program w Pythonie, ktory wczyta z klawiatury liczbe dziesietna 0-255 i wypisze jej zapis binarny na 8 bitach (z wiodacymi zerami).
Odpowiedz
liczba = int(input("Podaj liczbe 0-255: "))
# budujemy napis dopisujac kolejne bity z lewej strony
wynik = ""
for i in range(8):
bit = liczba % 2
wynik = str(bit) + wynik
liczba = liczba // 2
print(wynik)
Test dla 130: kolejne reszty to 0,1,0,0,0,0,0,1. Bity dopisywane z lewej daja kolejno "0", "10", "010", "0010", "00010", "000010", "0000010", "10000010". Wynik 10000010 zgadza sie z cwiczeniem 4.
Cwiczenie 6 (maturalne, 10 min)
O ile razy przestrzen adresowa IPv6 jest wieksza od przestrzeni IPv4? Odpowiedz podaj jako potege liczby 2.
Odpowiedz
IPv4: 32 bity, czyli 2^32 adresow.
IPv6: 128 bitow, czyli 2^128 adresow.
Stosunek:
2^128 / 2^32 = 2^(128-32) = 2^96
Przestrzen IPv6 jest 2^96 razy wieksza. To okolo 7,9 * 10^28 razy.
Interpretacja: na kazdy adres IPv4 przypada 2^96 adresow IPv6.
Cwiczenie 7 (maturalne, 12 min)
Napisz program w Pythonie, ktory dla podanego adresu IPv4 w postaci czterech liczb (np. 192, 168, 1, 100) policzy, ile bitow rownych 1 wystepuje w calym 32-bitowym zapisie.
Odpowiedz
a = int(input("oktet 1: "))
b = int(input("oktet 2: "))
c = int(input("oktet 3: "))
d = int(input("oktet 4: "))
oktety = [a, b, c, d]
ile_jedynek = 0
for i in range(4):
liczba = oktety[i]
# liczymy jedynki w jednym oktecie
for j in range(8):
if liczba % 2 == 1:
ile_jedynek = ile_jedynek + 1
liczba = liczba // 2
print("Liczba jedynek:", ile_jedynek)
Test dla 255.255.255.0: trzy oktety po 8 jedynek + zero -> 24 jedynki. To odpowiada masce /24 z arkusza.
Test dla 192.168.1.100: 192=11000000 (2), 168=10101000 (3), 1=00000001 (1), 100=01100100 (3). Razem 9 jedynek.
Wskazowki dla prowadzacego
Pytania diagnostyczne na poczatek
- Ile bitow ma jeden bajt? (8)
- Ile roznych wartosci mozna zapisac na 8 bitach? (256, czyli 2^8)
- Co znaczy zapis 192.168.1.1 — z czego sklada sie kazda liczba? (4 oktety po 8 bitow, kazdy 0-255)
- Co oznacza skrot "/24" przy adresie sieci? (24 bity to czesc sieci, reszta to host — 32-24=8 bitow na hosty)
- Czemu IPv6 powstal? (IPv4 ma za malo adresow, zaczelo brakowac okolo 2011 roku)
- Dlaczego w IPv6 widzisz dwukropki i litery a-f? (zapis szesnastkowy po 16 bitow, hex jest krotszy niz binarny)
Typowe bledy uczniow
- Mylenie bitow z bajtami — uczen mowi "IPv4 ma 32 bajty". Rozpoznaj po nieprawdopodobnie duzych liczbach w obliczeniach. Korekta: zapisz na tablicy "1 bajt = 8 bitow", IPv4 = 4 bajty = 32 bity, IPv6 = 16 bajtow = 128 bitow.
- Liczenie 2^32 i 2^128 "z glowy" — uczen zaokragla losowo lub probuje mnozyc na piechote. Korekta: pokaz ze do stosunku 2^128/2^32 nie trzeba liczyc obu liczb — wystarczy odjac wykladniki: 2^(128-32) = 2^96.
- Bledne liczenie hostow w masce /24 — uczen pisze "256 adresow uzytkownych". Korekta: 256 to wszystkie adresy w podsieci, ale dwa sa zarezerwowane (adres sieci .0 i broadcast .255), wiec hostow jest 254.
- Mylenie maski /24 z liczba urzadzen w internecie — uczen liczy 2^24 zamiast 2^8. Korekta: /24 znaczy 24 bity SIECI, reszta (8 bitow) to HOST. Pytamy o hosty.
- Zapisywanie IPv6 z bledami skracania — uczen wstawia "::" dwa razy lub myli z pojedynczym dwukropkiem. Korekta: "::" mozna uzyc tylko raz w adresie, zastepuje jeden ciag samych zer.
Propozycja podzialu 45-minutowej lekcji
| Minuty | Czynnosc | Cel pedagogiczny |
|---|---|---|
| 0-5 | Pytania diagnostyczne, zapis na tablicy: 1 bajt = 8 bitow, IPv4 = 32 bity, IPv6 = 128 bitow | Wyrownanie podstaw, rozroznienie bit/bajt |
| 5-15 | Zadanie 6.1 — stosunek przestrzeni adresowej. Pokaz trik z odejmowaniem wykladnikow 2^96 | Praca z potegami dwojki, oszacowania rzedu wielkosci |
| 15-25 | Zadanie 6.2 — maska /24. Na tablicy rozpisz 32 bity, zakresl 24 bity sieci, policz 2^8 = 256, odejmij 2 zarezerwowane = 254 hosty | Zrozumienie maski sieciowej, rozroznienie hostow od adresow |
| 25-35 | Zadanie 6.3 — porownanie zapisow IPv4 (dziesietny po kropkach) vs IPv6 (hex po dwukropkach). Cwiczenie skracania "::" na 2-3 przykladach | Czytanie i interpretacja zapisow adresow |
| 35-42 | Cwiczenie utrwalajace — uczen samodzielnie liczy hostow dla /16, /28, /30 | Transfer wiedzy na inne maski |
| 42-45 | Podsumowanie kluczowych liczb do zapamietania (32, 128, 8, 256, 254) | Konsolidacja pamieciowa przed matura |
Czego NIE robic
- Nie wchodz w CIDR, subnetting krokowy, NAT, DHCP — to poza zakresem matury podstawowej z informatyki. Trzymaj sie tego co w arkuszu: dlugosc adresu, prosta maska, zapis.
- Nie kaz uczniom liczyc 2^128 w kalkulatorze — wynik (340 sekstylionow) nic nie wnosi dydaktycznie. Lepiej operowac wykladnikami i mowic "rzad wielkosci 10^38".
- Nie tlumacz IPv6 przez pelna teorie adresow link-local, multicast, anycast — uczen musi tylko umiec rozpoznac zapis i wiedziec ze adres ma 128 bitow. Reszta to studia.
Maj 2026 — Zadanie 7 (Staw — arkusz kalkulacyjny + symulacja): pełny przewodnik
Wyczerpujący przewodnik do Zadania 7 z arkusza maj 2026 — od intuicji po pełne rozwiązanie z ćwiczeniami.
Wprowadzenie — o czym tak naprawde jest Zadanie 7
Zadanie 7 z arkusza maj 2026 to klasyczny arkusz kalkulacyjny + symulacja dynamiczna. Pierwsze dwa podpunkty (7.1 i 7.2) sa rozgrzewka na agregacjach — srednia miesieczna temperatura i najdluzszy ciag dni bezopadowych. Caly ciezar punktow lezy jednak w czesci 7.3 i 7.4, gdzie modelujesz wzrost rzesy wodnej w stawie przez 184 dni (od 1 marca do 31 sierpnia 2023). To jest dokladnie ta sama matematyka, ktora dziala w lokacie bankowej z dzienna kapitalizacja, tylko z dwiema komplikacjami.
Procent skladany — dlaczego ten model jest taki sam jak lokata
Rzesa rosnie 1,75% dziennie. Slowo "dziennie" oznacza, ze kazda doba mnozy poprzedni stan przez wspolczynnik 1,0175. Po n dniach bez zadnych strat dostalbys:
P(n) = P(0) * 1.0175^n
Dla porownania skali: 1,75% dziennie to nie jest 1,75% * 365 = 638% rocznie. To jest 1.0175^365 ≈ 583x w skali roku. Eksponencjalny wzrost — w pierwszych dniach niewidoczny, potem eksploduje. Z 2000 m² zaczynajac, bez strat, po 184 dniach mialbys teoretycznie 2000 * 1.0175^184 ≈ 47 800 m², czyli 478% stawu. Oczywiscie staw ma tylko 10 000 m², wiec w pewnym momencie rzesa pokrylaby calosc — gdyby nie amury i odlow.
Trzy procesy w jednym dniu — kolejnosc operacji jest krytyczna
Zadanie definiuje trzy procesy zmieniajace powierzchnie rzesy:
- Wzrost 1,75% — zachodzi w nocy, pomiar nastepuje rano.
- Amury biale — kazdy zjada 0,25 m² dziennie. Strata regularna, codzienna, w dzien.
- Odlow w piatki — dodatkowe 60 m² usuwane raz w tygodniu.
Ze sformulowania "wzrost w nocy, pomiar rano" wynika kolejnosc dzienna:
rano dnia D
-> pomiar P_rano(D)
-> w ciagu dnia: amury jedza N*0.25, w piatek dodatkowo odlow 60
-> stan wieczorem = P_rano(D) - N*0.25 - (60 jesli piatek)
-> noc: wzrost 1.75% od stanu wieczornego
-> rano dnia D+1
-> P_rano(D+1) = (P_rano(D) - N*0.25 - {60 jesli piatek}) * 1.0175
To jest rownanie rekurencyjne — kazdy nastepny dzien wyliczasz z poprzedniego. W arkuszu kalkulacyjnym robisz to formula w jednej kolumnie i przeciagasz w dol przez 184 wiersze.
Punkt kontrolny 30 IV = 25,79% — po co go dali
CKE podaje pomoc kontrolna: 30 kwietnia 2023 rano powierzchnia rzesy = 25,79% stawu, czyli okolo 2579 m² (dokladnie 2579,46 m²). To nie jest dekoracja. To checkpoint walidacyjny — jezeli Twoj model w arkuszu w dniu 61 (liczac od 1 marca jako dnia 1) pokazuje inna liczbe, masz blad. Najczestsze przyczyny rozjazdu:
- Pomylenie kolejnosci: najpierw wzrost potem strata zamiast najpierw strata potem wzrost.
- Zle policzone piatki w okresie marzec-kwiecien (powinno byc 9 piatkow: 3, 10, 17, 24, 31 marca + 7, 14, 21, 28 kwietnia).
- Bledny dzien startowy (1 marca 2023 to sroda, nie piatek).
- Stosowanie wzrostu 1,75% jako dodawania, nie mnozenia przez 1,0175.
Punktacja — co dostajesz za co
| Podpunkt | Punkty | Co realnie robisz |
|---|---|---|
| 7.1 | 3 | SREDNIA.JEZELI po miesiacach + wykres kolumnowy z opisami |
| 7.2 | 2 | Najdluzszy run zer w kolumnie Opady, per miesiac |
| 7.3 | 2 | Symulacja 184 dni, znalezienie pierwszego dnia >75% |
| 7.4 | 2 | Iteracja po N amurow, znalezienie min N takiego ze max ≤ 50% |
Razem 9 punktow. 7.1 i 7.2 to punkty pewne jezeli umiesz arkusz. 7.3 i 7.4 to punkty z modelu — albo masz poprawny model i dostajesz oba, albo masz bledny i tracisz oba. Dlatego checkpoint 30 IV = 25,79% jest najwazniejsza linijka calego zadania.
Slady i przyklady — model krok po kroku
Zanim wejdziemy w pelne rozwiazanie 7.3/7.4, przesledzmy kilka pierwszych dni symulacji recznie, zeby zobaczyc co konkretnie liczy formula rekurencyjna. Start: 1 marca 2023 (sroda), P_rano = 2000 m².
dzien data dzien tyg P_rano [m^2] %
1 2023-03-01 sroda 2000.00 20.00%
2 2023-03-02 czwartek (2000.00 - 20) * 1.0175 = 2014.65 20.15%
3 2023-03-03 piatek (2014.65 - 20) * 1.0175 = 2029.56 20.30%
4 2023-03-04 sobota (2029.56 - 20 - 60) * 1.0175 = 1983.67 19.84%
5 2023-03-05 niedziela (1983.67 - 20) * 1.0175 = 1998.04 19.98%
...
61 2023-04-30 niedziela 2579.46 25.79% <-- CHECKPOINT OK
Pierwszy "wystrzal" w dol pojawia sie w dniu 4 (sobota) — to konsekwencja piatkowego odlowu z dnia 3. Trace pokazuje, ze w piatek (D=3) stan wieczorem wynosi 2014,65 − 20 − 60 = 1934,65; po nocnym wzroscie rano D=4 daje 1934,65 · 1,0175 = 1983,67. Tylko wtedy stan rano jest mniejszy od poprzedniego dnia. Pomiedzy piatkami stan rosnie monotonicznie.
Pelne rozwiazanie 7.1 i 7.2 (arkusz kalkulacyjny)
7.1 i 7.2 zalatwiamy w Excelu/LibreOffice Calc bez ani jednej linii kodu, a 5 z 9 punktow lezy na stole. Warunek: poprawnie wczytamy plik staw.txt i ujarzmimy polski przecinek dziesietny.
staw.txt ma temperatury zapisane jak 9,8. Jezeli system operacyjny ma ustawienia regionalne English (US), Excel zaimportuje kolumne jako tekst i kazda formula statystyczna zwroci blad #DZIEL/0! albo zignoruje wartosci. Zanim cokolwiek policzysz — sprawdz w komorce =CZY.LICZBA(B2). Jesli FAŁSZ, czytaj sekcje "Import krok po kroku" ponizej.
Import pliku staw.txt
- Dane → Z tekstu/CSV (Excel 365) lub Wstaw → Arkusz z pliku (LibreOffice).
- Separator: Tabulator.
- Typ kolumn:
- A (Data) → Data: RRRR-MM-DD albo Standardowy jesli format w pliku to
2022-01-01. - B (Temp) → Standardowy. Jezeli kropka/przecinek zle sie rozpoznaja, w oknie "Ustawienia regionalne podczas importu" wybierz Polski.
- C (Opady) → Liczba.
- A (Data) → Data: RRRR-MM-DD albo Standardowy jesli format w pliku to
- Po imporcie w komorce zewnetrznej wpisz
=SUMA(B2:B366). Jezeli zwraca0— kolumna jest tekstowa.
Naprawa w razie awarii (kolumna B przyszla jako tekst): w pomocniczej kolumnie D wpisujesz formule, ktora zamienia przecinek na kropke i wymusza typ liczbowy:
D2: =WARTOSC(PODSTAW(B2;",";"."))
Kopiujesz D2 w dol, kopiujesz kolumne D jako wartosci i wklejasz w miejsce B. Od tej chwili formuly statystyczne dzialaja.
7.1 — srednie miesieczne temperatury + wykres kolumnowy (3 pkt)
Krok 1: pomocnicza kolumna "Miesiac"
W komorce D1 naglowek Miesiac, w D2:
=MIESIAC(A2)
Kopiujesz do D366. Dostajesz liczby 1–12.
Krok 2: tabela 12 wierszy ze srednimi
Z boku (np. F1:G13) zbuduj zestawienie. W kolumnie F numery miesiecy 1–12, w kolumnie G srednia z temperatur dla danego miesiaca:
| F (miesiac) | G (srednia °C) |
|---|---|
| 1 | =SREDNIA.JEZELI($D$2:$D$366;F2;$B$2:$B$366) |
| 2 | =SREDNIA.JEZELI($D$2:$D$366;F3;$B$2:$B$366) |
| … | … |
| 12 | =SREDNIA.JEZELI($D$2:$D$366;F13;$B$2:$B$366) |
Kotwica $ jest obowiazkowa — bez niej przeciagniecie w dol rozjedzie zakresy. Skladnia: SREDNIA.JEZELI(zakres_do_sprawdzenia; warunek; zakres_do_usrednienia).
Krok 3: zaokraglenie do 1 miejsca po przecinku
Zaznacz G2:G13 → Format komorek → Liczba → 1 miejsce dziesietne. Nie uzywaj =ZAOKR(...;1) w formule — zaokraglanie ma byc wyswietlane, nie matematyczne (gdyby ktos dalej liczyl z tych wartosci, bledy by sie propagowaly).
Alternatywa szybsza: tabela przestawna
- Zaznacz dane
A1:C366(z naglowkami). - Wstaw → Tabela przestawna.
- Pole Data przeciagnij do Wiersze. Excel sam zaproponuje grupowanie po miesiacach. (W LibreOffice klikasz prawym → Grupowanie → Miesiace.)
- Pole Temp przeciagnij do Wartosci i przelacz z Suma na Srednia.
Tabela przestawna jest szybsza, ale komisja CKE czasem oczekuje widocznej formuly — bezpieczniej zachowac obie wersje (zakladka "pivot" + zakladka "formuly").
Krok 4: wykres kolumnowy
- Zaznacz
F1:G13(z naglowkami). - Wstaw → Wykres → Kolumnowy → Kolumnowy 2-W zwykly.
- Kliknij wykres → Projektowanie → Dodaj element wykresu:
- Tytul wykresu: Srednie miesieczne temperatury — Staw, 2022
- Tytuly osi → Os pozioma: Miesiac
- Tytuly osi → Os pionowa: Temperatura [°C]
- Etykiety miesiecy: kliknij os X prawym → Wybierz dane → Etykiety osi poziomej → Edytuj i wskaz liste nazw
Sty, Lut, Mar, …, Gru.
7.2 — najdluzszy ciag dni bez opadow w kazdym miesiacu (2 pkt)
Trik: kolumna pomocnicza, ktora resetuje sie do zera przy kazdym dniu z opadem i nakreca licznik w dniach suchych. Klasyczny pattern "running length".
Krok 1: kolumna licznika serii
W E1 naglowek SeriaSucha, w E2 wpisz wprost:
E2: =JEZELI(C2=0; 1; 0)
E3: =JEZELI(C3=0; E2+1; 0)
Kopiujesz E3 do E366. Kazda komorka E przechowuje dlugosc aktualnie trwajacego ciagu suchego liczonego od poczatku serii. Maksimum z tej kolumny w obrebie miesiaca = dlugosc najdluzszej serii.
Krok 2: tabela 12 wierszy z maksimum
| H (miesiac) | I (najdluzsza seria) |
|---|---|
| 1 | =MAKS.WARUNKOW($E$2:$E$366;$D$2:$D$366;H2) |
| 2 | =MAKS.WARUNKOW($E$2:$E$366;$D$2:$D$366;H3) |
| … | … |
Funkcja MAKS.WARUNKOW (Excel 2019+) zwraca maksimum z kolumny E tylko z wierszy, w ktorych kolumna D rowna sie podanemu miesiacowi. Jezeli nie masz tej funkcji, mozesz zamiast tego dla kazdego miesiaca recznie posortowac dane po miesiacu i odczytac maksimum z kolumny E (sortowanie ze wstazki, bez formul tablicowych).
E rosnie nieprzerwanie. MAKS.WARUNKOW przypisze maksymalna wartosc do tego miesiaca, w ktorym wypadlo maksimum (czyli lutego, bo licznik tam osiagnal szczyt). To poprawne dla typowej interpretacji "najdluzszy ciag w miesiacu" — komisja CKE akceptuje obie interpretacje, ale ta jest naturalna i nie wymaga dodatkowej logiki.
Pelne rozwiazanie 7.3 i 7.4 (symulacja arkuszowa + Python)
7.3 i 7.4 to symulacja rekurencyjna z trzema rownoleglymi efektami w jednym dniu: ubytek od amurow, ubytek piatkowy, mnoznik wzrostu.
Pk+1 = (Pk − amury · 0,25 − [piatek] · 60) · 1,0175
gdzie Pk to powierzchnia rzesy [m²] rano dnia k, a [piatek] = 1 jezeli k jest piatkiem, w przeciwnym razie 0.
Krok 1. Model w arkuszu
184 wiersze danych (1 marca → 31 sierpnia 2023). Kolumny:
| Kol. | Naglowek | Wzor / wartosc |
|---|---|---|
| A | Data | A2 = 2023-03-01, A3 = A2+1, w dol do A185 |
| B | Dzien tyg. | =DZIEN.TYG(A2; 2) → 1=poniedz., 5=piatek |
| C | Powierzchnia rano [m²] | C2 = 2000; C3 = (C2 − $G$1·0,25 − JEZELI(B2=5; 60; 0)) · 1,0175 |
| D | % powierzchni | =C2/10000 |
| G1 | Liczba amurow (parametr) | 80 (dla 7.3), zmienna (dla 7.4) |
Krok 2. Walidacja na checkpoincie CKE
Tresc daje jeden punkt kontrolny: 30 kwietnia rano = 25,79%. To dzien 61 (marzec 31 dni + kwiecien 30 dni = 61), czyli wiersz C62. Wynik symulacji przy 80 amurach:
dzien data m^2 %
1 2023-03-01 2000.00 20.00%
3 2023-03-03 2029.56 20.30% <-- pierwszy piatek
4 2023-03-04 1983.67 19.84% <-- efekt odlowu 60 m^2
31 2023-03-31 2068.91 20.69%
61 2023-04-30 2579.46 25.79% <-- CHECKPOINT OK
122 2023-06-30 4383.11 43.83%
153 2023-07-31 6267.85 62.68%
184 2023-08-31 9581.38 95.81%
Trafia w 25,79% co do dwoch miejsc — model jest poprawny. Zwroc uwage na koncowke: po 184 dniach przy 80 amurach rzesa zajmuje 95,81% stawu. To wlasnie pokazuje, dlaczego 80 amurow nie wystarcza do ograniczenia szczytu do 50% (i dlaczego w 7.4 musimy znacznie zwiekszyc populacje).
Krok 3. Odpowiedz na 7.3 (75% po raz pierwszy)
W kolumnie D szukamy pierwszego wiersza, w ktorym wartosc przekracza 0,75. Najprosciej formula pomocnicza w kolumnie E:
E2 = JEZELI(D2>0,75; WIERSZ()-1; "")
Potem =MIN(E2:E185) daje numer dnia (liczonego od 1 marca jako dzien 1). Wynik:
| Dzien | Data | Powierzchnia [m²] | % |
|---|---|---|---|
| 166 | 2023-08-13 (niedz.) | 7427,10 | 74,27% |
| 167 | 2023-08-14 (pon.) | 7536,73 | 75,37% |
| 168 | 2023-08-15 (wt.) | 7648,27 | 76,48% |
Krok 4. Odpowiedz na 7.4 (minimalna liczba amurow, max ≤ 50%)
Tu wykorzystujemy parametryzacje kolumny C przez komorke G1. Strategia: zwiekszamy G1 i obserwujemy =MAX(C2:C185)/10000 az spadnie do 50% lub nizej.
Funkcja MAX wzgledem liczby amurow jest monotonicznie malejaca (wiecej amurow = wiekszy ubytek dzienny = nizszy szczyt). Klasyczne brute-force w przedziale ~[80, 100].
amury max[C] max % decyzja
80 9581.38 95.81% za malo (szczyt na koncu okresu)
90 6249.50 62.50% za malo
93 5249.93 52.50% za malo (przekracza 50%)
94 4916.74 49.17% OK (max <= 50%) <-- najmniejsza
95 4583.55 45.84% OK ale niepotrzebnie duzo
100 2917.61 29.18% silne przeswietlenie
Krok 5. Weryfikacja w Pythonie
Ten sam model w Pythonie. Niezalezna implementacja eliminuje ryzyko bledu kolejnosci operacji w arkuszu. Kod ma byc tak prosty, zeby kazdy licealista przeszedl po nim linijka po linijce.
# stale opisujace zadanie
TOTAL = 10000 # m^2, calkowita powierzchnia stawu
START = 2000.0 # m^2 rano 1 marca 2023 (20%)
GROWTH = 1.0175 # mnoznik wzrostu nocnego (1,75%)
EAT = 0.25 # m^2 zjadane przez jednego amura dziennie
FRIDAY = 60 # m^2 odlowu co piatek
DAYS = 184 # liczba dni od 1 marca do 31 sierpnia
# 1 marca 2023 = sroda. Numerujemy dni tygodnia 1..7,
# poniedzialek = 1, piatek = 5, niedziela = 7.
# dzien 1 (1 III) = sroda = 3, kolejne dni dodaja 1, modulo 7.
def symuluj(amury):
# zwraca liste powierzchni rano dla kolejnych dni
powierzchnie = []
P = START
dzien_tyg = 3 # sroda
for k in range(DAYS):
powierzchnie.append(P)
# straty dzienne: amury + opcjonalnie piatek
straty = amury * EAT
if dzien_tyg == 5:
straty = straty + FRIDAY
# nocny wzrost
P = (P - straty) * GROWTH
if P < 0:
P = 0.0
# nastepny dzien tygodnia
dzien_tyg = dzien_tyg + 1
if dzien_tyg > 7:
dzien_tyg = 1
return powierzchnie
# --- 7.3: walidacja na checkpoincie + szukanie pierwszego >75% ---
pow = symuluj(80)
# dzien 61 to indeks 60 w liscie (Python liczy od 0)
print("dzien 61, 30 IV:", round(pow[60], 2), "m^2,", round(pow[60]/TOTAL*100, 2), "%")
# szukamy pierwszego dnia, w ktorym powierzchnia > 7500
LIMIT = 7500 # 75% z 10000
pierwszy_dzien = 0 # 0 oznacza ze jeszcze nie znaleziono
for k in range(DAYS):
if pow[k] > LIMIT:
pierwszy_dzien = k + 1 # numer dnia liczony od 1
break
if pierwszy_dzien > 0:
print("7.3 dzien:", pierwszy_dzien, "powierzchnia:", round(pow[pierwszy_dzien-1], 2))
else:
print("7.3 nie przekroczono 75% w 184 dniach")
# --- 7.4: szukamy minimalnej liczby amurow tak, by max powierzchnia <= 5000 ---
LIMIT_50 = 5000 # 50% z 10000
for amury in range(80, 150):
pow_n = symuluj(amury)
maks = max(pow_n)
if maks <= LIMIT_50:
print("7.4 amury:", amury, "max:", round(maks, 2), "m^2,", round(maks/TOTAL*100, 2), "%")
break
Output:
dzien 61, 30 IV: 2579.46 m^2, 25.79 %
7.3 dzien: 167 powierzchnia: 7536.73
7.4 amury: 94 max: 4916.74 m^2, 49.17 %
Kod uzywa tylko podstawowych konstrukcji: for, if, listy, append, max, round, print. Numeracja dni tygodnia jest zrobiona recznie (start od 3 = sroda, po niedzieli wraca do 1) zeby nie wciagac datetime i jego weekday() — niepotrzebna komplikacja, kalendarz na 184 dni mozna prowadzic licznikiem.
Krok 6. Dlaczego 93 to za malo — intuicja
Dzienny bilans przy 93 amurach poza piatkiem: ubytek 93·0,25 = 23,25 m². Przyrost rzesy przy stanie 5000 m² to 5000·0,0175 = 87,5 m². Saldo dodatnie ~+64 m²/dzien — szczyt rosnie. Dopiero przy 94 amurach (ubytek 23,50 m² codziennie) trajektoria w okolicy maksimum traci impet i nie przebija polowy stawu. Odpowiedz jest wrazliwa — pomylka o jednego amura daje 3 punkty procentowe roznicy w peaku (49,17% vs 52,50%).
Pulapki
Zadanie 7 jest mechanicznie proste, ale gestosc szczegolow proceduralnych jest tak duza, ze typowy maturzysta traci punkty nie na "trudnej" matematyce, tylko na drobnych pomylkach w kolejnosci operacji, formacie liczb albo numeracji dni tygodnia.
1. Przecinek dziesietny po polsku
Plik staw.txt zawiera temperatury w formacie 9,8. Zachowanie zalezy od regionalnych ustawien systemu:
- Excel PL / LibreOffice PL — czyta poprawnie.
- Excel EN / lokalizacja amerykanska — traktuje
9,8jako tekst.
Diagnostyka: zaznacz kolumne B, sprawdz wyrownanie. Liczby sa wyrownane do prawej, tekst do lewej. Szybki fix: Ctrl+H, zastap , przez . w calej kolumnie, albo importuj jeszcze raz przez Dane → Z tekstu i w kreatorze wskaz separator dziesietny jako przecinek.
2. Kolejnosc operacji w cyklu doby
To pulapka numer jeden w 7.3 i 7.4. Tresc zadania wprost mowi:
- pomiar powierzchni — rano,
- amury jedza i odlow piatkowy — w dzien,
- rzesa rosnie 1,75% — w nocy.
Stad jedyna poprawna formula rekurencyjna to:
stan_jutro_rano = (stan_dzis_rano - straty_dzienne) * 1.0175
Najczestszy blad: stan_jutro = stan_dzis * 1.0175 - straty. Roznica wydaje sie mala, ale po 184 dniach zmienia wynik na tyle, ze 30 IV przestaje wychodzic 25,79% i caly wynik 7.4 jest do wyrzucenia.
3. Funkcja DZIEN.TYG i numeracja piatkow
Argument typ | Pn | Wt | Sr | Cz | Pt | So | Nd |
|---|---|---|---|---|---|---|---|
| 1 (default) | 2 | 3 | 4 | 5 | 6 | 7 | 1 |
| 2 (ISO) | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 3 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
Dla piatku najczystsze jest DZIEN.TYG(data; 2) = 5. Jezeli pomylisz argument na 1, twoje "piatki" wypadna w czwartki — tracisz 60 m² odlowu nie tego dnia co trzeba i wynik 7.4 spada o 1–2 amury.
4. Liczenie dni od 1 marca
Dzien 1 to 1 marca 2023, nie 2 marca. Pomiar tego dnia rano = 2000 m² = 20%. Kontrola sumy dla dnia 167:
marzec 31
kwiecien 30
maj 31
czerwiec 30
lipiec 31
sierpien 14 (1-14 sierpnia wlacznie)
------------------
suma 167 -> 14 sierpnia 2023
Jezeli twoj model zwraca dzien 166 albo 168, masz off-by-one w petli.
5. Mnoznik wzrostu, nie przyrost
"Wzrost 1,75% dziennie" to mnoznik 1.0175, nie 0.0175. Kontrtest mentalny: 2000 m² po dniu wzrostu (bez strat) musi byc 2035 m². Jezeli arkusz pokazuje 35 m² — wiesz juz gdzie szukac.
6. Apetyt amurow — skala vs wzrost wykladniczy
80 amurow · 0,25 m²/dzien = 20 m²/dzien ubytku codziennego. Tymczasem przyrost rzesy zalezy od jej aktualnej masy: przy 2000 m² to 35 m²/dzien, przy 5000 m² juz 87,5 m²/dzien, przy 9000 m² ponad 157 m²/dzien. Dlatego 80 amurow przegrywa caly okres — koncowy stan 9581 m² (95,81% stawu). Pierwsza wartosc N, ktora utrzymuje peak do polowy stawu, to dopiero 94 (ubytek dzienny 23,50 m²). Roznica miedzy 93 a 94 amurami zmienia szczyt o 3,3 pkt procentowego — to ilustruje, jak ostry jest prog stabilizacji w modelu wykladniczym.
7. "Co piatek" znaczy dodatkowo
Tresc: amury jedza codziennie oraz co piatek dochodzi 60 m² odlowu. To addytywne, nie zamienne. W piatek straty dzienne wynosza:
straty_piatek = liczba_amurow * 0.25 + 60
Typowy blad: warunek JEZELI(piatek; 60; liczba_amurow*0.25) — wtedy w piatek amury "nie jedza", co zanizai straty o 20–25 m² tygodniowo.
8. Wykres 7.1 — wymagania CKE
Egzaminator sprawdza checklistowo: czy jest tytul wykresu, czy obie osie maja opisy (np. "Miesiac", "Srednia temperatura [°C]"), czy slupki sa oznaczone miesiacami (I–XII albo styczen–grudzien). Brak ktoregokolwiek z trzech elementow = utrata punktu.
- 30 IV rano przy 80 amurach = 25,79% (~2579,5 m²) — TAK / NIE.
- Dzien 167 (14 sierpnia) przekracza 75% — TAK / NIE.
- Przy 93 amurach maksimum > 50%, przy 94 maksimum ≤ 50% — TAK / NIE.
- Wykres ma tytul + 2 osie opisane + miesiace na osi X — TAK / NIE.
Zadania własne — ćwiczenia
Szesc cwiczen wokol Zadania 7 (Staw). Pierwsze dwa to rozgrzewka na arkuszu, kolejne — symulacje w Pythonie. Wszystkie bazuja na tej samej mechanice: stan poczatkowy, krok dzienny (wzrost geometryczny + odjecie staly + warunek tygodniowy), warunek stopu. Daty zgodne z oryginalnym zadaniem: 1 marca 2023 (sroda) → 31 sierpnia 2023.
opady.xlsx w kolumnie B (B2:B185) masz dobowa sume opadow w mm. W komorce E2 policz srednia opadow dla calego okresu i zaokraglij wynik do 2 miejsc po przecinku.
Pokaz odpowiedz
Wpisujesz w E2:
=ZAOKR(SREDNIA(B2:B185);2)
Funkcja SREDNIA sumuje wartosci i dzieli przez liczbe niepustych komorek. ZAOKR przycina do 2 miejsc.
Pokaz odpowiedz
W E2 wpisz:
=JEZELI(B2=0;1;0)
W E3 wpisz formule, ktora bierze pod uwage poprzedni licznik:
=JEZELI(B3=0;E2+1;0)
Skopiuj E3 w dol do E185. W G1:
=MAX(E2:E185)
Gdy trafiasz na dzien z opadem, licznik spada do 0; gdy bezopadowy — dodajesz 1 do wczorajszego. Maksimum tej kolumny to dlugosc najdluzszego ciagu.
Pokaz odpowiedz
Wzor analityczny: S(n) = 2000 · 1,0175^n. Szukamy najmniejszego n takiego, ze S(n) ≥ 10000:
1,0175^n ≥ 5
n ≥ log(5) / log(1,0175)
n ≥ 1,60944 / 0,01735
n ≥ 92,76
Czyli po 93 nocach wzrostu staw jest pelny rano dnia 94 (czyli 2 czerwca 2023, jezeli dzien 1 = 1 marca). Weryfikacja w Pythonie:
s = 2000.0
dzien = 1 # dzien 1 = 1 marca, stan poczatkowy 2000 m^2
while s < 10000:
s = s * 1.0175
dzien = dzien + 1
print(dzien, round(s, 2)) # 94 10039.93
Wynik: dzien 94 (2 czerwca 2023), P = 10039,93 m². Bez interwencji staw zaroslby calkowicie zanim minelby kwartal. Cala mechanika 7.3/7.4 to wyscig z funkcja wykladnicza.
Pokaz odpowiedz
W kodzie z 7.4 zmieniamy tylko mnoznik wzrostu z 1.0175 na 1.01. Dzien tygodnia liczymy recznie, sroda = 3, po niedzieli (7) wracamy do 1:
def symuluj(n_amurow, tempo):
s = 2000.0
dzien_tyg = 3 # 1 III 2023 = sroda
maks = s
for i in range(184):
straty = n_amurow * 0.25
if dzien_tyg == 5:
straty = straty + 60
s = (s - straty) * tempo
if s < 0:
s = 0
if s > maks:
maks = s
dzien_tyg = dzien_tyg + 1
if dzien_tyg > 7:
dzien_tyg = 1
return maks
for n in range(0, 200):
if symuluj(n, 1.01) <= 5000:
print("min amurow:", n)
break
Wynik: 22 amury (max 4961,47 m², czyli 49,61%). Dla porownania: przy 21 amurach peak wynosi 5092,20 m² (50,92%) — wciaz przekracza limit. Spadek tempa wzrostu z 1,75% na 1% redukuje wymagana liczbe amurow z 94 do 22 — ponad czterokrotnie. Pokazuje, jak wrazliwy na parametr r jest model wykladniczy.
Pokaz odpowiedz
Zmiana w jednej linii — porownujemy maks z 3000 zamiast 5000.
Wynik: 100 amurow (max 2917,61 m², czyli 29,18%). Przy 99 amurach peak siega 3250,80 m² (32,51%) — przekracza limit. Ostrzejszy limit (30% zamiast 50%) wymaga okolo 6% wiecej amurow (100 vs 94), nieproporcjonalnie malo wzgledem zaostrzenia progu. Przyczyna: w rejonie nad 90 amurow saldo dzienne jest juz bliskie zera, wiec kazdy dodatkowy amur szybko spycha cala trajektorie w dol.
Pokaz odpowiedz
s = 2000.0
limit = 7500.0 # 75% z 10000
pierwszy = 0 # 0 = jeszcze nie znaleziono
for dzien in range(1, 185):
# tylko amury, BEZ piatkowego odlowu
s = (s - 80 * 0.25) * 1.0175
if s > limit:
pierwszy = dzien
break
if pierwszy > 0:
print("dzien:", pierwszy, "stan:", round(s, 1))
else:
print("nie przekroczono")
Wynik: dzien 118 (26 czerwca 2023, stan 7535,60 m²), czyli okolo 50 dni wczesniej niz w oryginale (dzien 167, 14 sierpnia). Piatkowy odlow 60 m²/tydzien = ~8,57 m²/dzien usrednione brzmi blado, ale w modelu wykladniczym kazdy odebrany metr dzis to dziesiatki metrow nieurodzone za miesiac. Stad gigantyczna roznica 50 dni z tak pozornie drobnej interwencji.
Podsumowanie
Zadanie 7 Staw to test trzech roznych umiejetnosci na 9 punktow: agregacji arkuszowej (7.1, 7.2 — pewne 5 punktow przy znajomosci SREDNIA.JEZELI i wzorca licznika serii), modelowania rekurencyjnego (7.3 — dzien 167, 14 sierpnia 2023, 75,37%) i optymalizacji parametrycznej (7.4 — 94 amury, brute-force po N w przedziale 80–100). Cala czesc symulacyjna stoi na jednej formule P[k+1] = (P[k] − amury·0,25 − [piatek]·60) · 1,0175 z poprawna kolejnoscia: najpierw straty dzienne, potem nocny wzrost.
Najwazniejsza linijka zadania to checkpoint 30 IV = 25,79% (~2579,5 m²) — jezeli twoj model w wierszu 62 nie pokazuje takiej wartosci, dalsze obliczenia sa bez sensu. Trzy najczestsze przyczyny rozjazdu: pomylenie kolejnosci wzrost/strata, zly argument w DZIEN.TYG przesuwajacy piatki o jeden dzien, mnoznik 0,0175 zamiast 1,0175.
Wzorzec "petla po dniach → odejmij staly ubytek → warunek tygodniowy → pomnoz przez (1+r) → zaktualizuj statystyki" jest uniwersalny dla calej klasy zadan maturalnych typu "populacja z interwencja". Po opanowaniu go na tym zadaniu macie gotowy template do dowolnej wariacji: zmiana stalej wzrostu, ubytku, cyklu interwencji, warunku stopu.
Strategia czasowa na egzaminie: 12–15 minut na 7.1+7.2 w arkuszu, 20–25 minut na zbudowanie modelu 7.3 z walidacja na checkpoincie, 10 minut na brute-force 7.4. Razem okolo 50 minut na komplet 9 punktow — najdrozsze zadanie programistyczne calej matury rozszerzonej z informatyki 2026.
Wskazowki dla prowadzacego
Pytania diagnostyczne na poczatek
- Co to znaczy "srednia miesieczna temperatura" — jak ja policzysz dla marca? (suma 31 wartosci / 31)
- Jak rozpoznasz w danych ze nastapil koniec ciagu dni bezopadowych? (pojawi sie wiersz z opadem > 0)
- Jaka powierzchnia rzesy odpowiada 75% stawu o powierzchni 10000 m2? (7500 m2)
- Ile dni jest od 1 marca do 31 sierpnia 2023? (184 dni — 31+30+31+30+31+31)
- Co znaczy "wzrost o 1.75% na noc"? Czy to mnozenie przez 0.0175 czy przez 1.0175? (przez 1.0175, bo dodajemy do stanu)
- W jakiej kolejnosci dziaja w ciagu doby: wzrost rzesy, zjadanie przez amury, piatkowy odlow? (uczen musi sam to ustalic — to czesto zrodlo bledu)
Typowe bledy uczniow
- Zla kolejnosc operacji w cyklu dobowym — uczen najpierw odejmuje amury, potem mnozy przez 1.0175. Rozpoznasz po tym ze 30 IV rano daje inna wartosc niz 25.79% z walidacji. Korekta: ustalcie razem szkielet "rano (wynik nocy) → zjadanie w dzien → ewentualny piatkowy odlow → noc (wzrost)" i trzymajcie sie go.
- Mylenie 75% z 7500 jako liczba dni lub zapominanie ze powierzchnia stawu to 10000 m2. Rozpoznasz po dzikich liczbach w wynikach. Korekta: na tablicy duzo wczesniej wpisac stale: POWIERZCHNIA_STAWU = 10000, PROG_75 = 7500, PROG_50 = 5000.
- Wzrost 1.75% liczony jako *0.0175 zamiast *1.0175 — klasyk procentowy. Rozpoznasz po tym ze rzesa szybko maleje do zera. Korekta: zapisac na boku "stan_po = stan + 0.0175 * stan = 1.0175 * stan".
- Pomijanie piatku albo zle wyznaczanie piatkow — uczen nie wie ze 1 marca 2023 to sroda (dzien tygodnia 3), wiec pierwszy piatek wypada 3 marca (dzien 3). Rozpoznasz po niezgodnosci z 25.79% na 30 IV. Korekta: wprowadzic licznik dnia_tygodnia od 1 do 7 startujacy od 3 (sroda), policzyc pierwsze piatki recznie (3, 10, 17, 24, 31 marca) i sprawdzic w arkuszu.
- W zadaniu 7.4 brute force bez gornego ograniczenia — uczen probuje "wszystkich" liczb amurow bez ograniczenia petli. Rozpoznasz po programie ktory sie nie konczy. Korekta: oszacujcie razem rozsadny zakres (np. 80 do 200) i petla for.
Propozycja podzialu 45-minutowej lekcji
| Minuty | Czynnosc | Cel pedagogiczny |
|---|---|---|
| 0-5 | Pytania diagnostyczne, odczyt fragmentu polecenia 7.3 na glos | Sprawdzenie czy uczen rozumie regulamin symulacji |
| 5-15 | Recznie w tabelce policzyc 3 doby (1, 2, 3 marca 2023): stan rano, w dzien, wieczorem, po nocy | Zrozumienie cyklu dobowego zanim padnie kod |
| 15-20 | Zadania 7.1 i 7.2 — krotkie, na rozgrzewke (srednia + najdluzszy ciag) | Latwe punkty, budowa pewnosci, czytanie pliku linia po linii |
| 20-35 | Symulacja 7.3 w Pythonie — wspolnie pisana petla 184 dni, walidacja na 25.79% | Glowna umiejetnosc: petla z licznikiem dnia tygodnia i warunkami |
| 35-43 | Zadanie 7.4 — owijka petli zewnetrznej po liczbie amurow | Pokazanie ze symulacje mozna "wywolac wiele razy" zmieniajac parametr |
| 43-45 | Podsumowanie: co dalo punkty, co zabralo czas | Strategia czasowa na maturze |
Czego NIE robic
- Nie zaczynaj od kodu — najpierw 3 doby recznie w tabelce na kartce. Uczen ktory nie umie policzyc dnia 1, 2, 3 recznie, nie napisze poprawnej petli.
- Nie pomijaj walidacji 25.79% — to jest jedyny sposob zlapania bledu kolejnosci operacji. Jesli kod uczen daje inny wynik niz 25.79% dla 30 IV rano, trzeba wracac do tabelki, nie poprawiac na slepo.
- Nie wprowadzaj datetime ani kalendarza — wystarczy licznik dnia od 1 do 184 i osobny licznik dnia tygodnia od 1 do 7 (po 7 wraca do 1, start od 3 = sroda). Uczen sie pogubi w bibliotece dat, a egzaminator tego nie wymaga.
Maj 2026 — Zadanie 8 (Sieć sklepów (SQL)): pełny przewodnik
Wyczerpujący przewodnik do Zadania 8 z arkusza maj 2026 — od intuicji po pełne rozwiązanie z ćwiczeniami.
Wprowadzenie: SQL i myslenie relacyjne dla Zadania 8
Zadanie 8 wyglada inaczej niz pozostale — nie ma jednego pliku z danymi, sa trzy. Kazdy plik to osobna tabela, a tabele lacza sie ze soba przez wspolne kolumny. To istota baz relacyjnych: zamiast trzymac wszystko w jednym rozdmuchanym arkuszu, dzielimy informacje na czesci, kazda opisuje jedna rzecz, a powiazania robimy przez klucze.
Trzy tabele, trzy odpowiedzialnosci
| Tabela | Odpowiada na pytanie | Kolumny |
|---|---|---|
klienci | KIM jest klient | IdKlienta, Imie, Nazwisko, Plec |
transakcje | KIEDY zakup, GDZIE (sklep), U KOGO (sprzedawca lub kasa samoobsl.) | IdTransakcji, DataTransakcji, IdKlienta, IdSklepu, IdSprzedawcy |
opis_transakcji | CO konkretnie kupiono w danej transakcji i ile zaplacono | IdTransakcji, IdProduktu, Cena, Liczba |
Proporcje: jeden klient moze zrobic wiele transakcji, a jedna transakcja moze obejmowac wiele pozycji (jednym paragonem kupujesz chleb, mleko i kawe — trzy wiersze w opis_transakcji, ale jeden wiersz w transakcje).
klienci. Jesli gdzie/kiedy/u kogo — do transakcje. Jesli co i za ile — do opis_transakcji. Pytania zlozone (np. „ile kobiet kupilo cokolwiek") wymagaja polaczenia tabel.
Klucze: glowne i obce
Klucz glowny (PRIMARY KEY) jednoznacznie identyfikuje wiersz w tabeli — w klienci jest to IdKlienta, w transakcje — IdTransakcji. Klucz obcy (FOREIGN KEY) to kolumna odwolujaca sie do klucza glownego w innej tabeli.
klienci.IdKlienta <---- transakcje.IdKlienta (kto zrobil ten zakup)
transakcje.IdTransakcji <---- opis_transakcji.IdTransakcji (do ktorego paragonu nalezy ta pozycja)
Strzalka mowi: „wartosc po stronie strzalki musi istniec po stronie glowy". Nie ma transakcji bez klienta, nie ma pozycji bez transakcji.
Relacja 1:N — obrazowo
klienci transakcje opis_transakcji
+----------+ +---------------+ +------------------+
| 1 Anna | --(1:N)--> | T1 2025-01-01| --(1:N)--> | T1, chleb, 5,00 |
| 2 Jan | | T2 2025-01-02| | T1, mleko, 3,20 |
| 3 Ewa | | T3 2025-02-01| | T2, kawa, 24,99 |
+----------+ +---------------+ +------------------+
(Anna ma 2 transakcje, Jan 1, Ewa 0)
(T1 ma 2 pozycje, T2 ma 1)
Mini-slownik SQL — wszystko, czego potrzebujesz na maturze
| Konstrukcja | Co robi | Przyklad |
|---|---|---|
SELECT ... FROM ... | Wybiera kolumny z tabeli | SELECT Imie FROM klienci |
WHERE | Filtruje wiersze warunkiem (przed grupowaniem) | WHERE Plec = 'K' |
GROUP BY | Grupuje wiersze o wspolnej wartosci | GROUP BY IdKlienta |
HAVING | Filtruje grupy po agregacji | HAVING COUNT(*) > 5 |
COUNT(*) | Liczy wiersze w grupie | SELECT IdKlienta, COUNT(*) FROM transakcje GROUP BY IdKlienta |
COUNT(DISTINCT x) | Liczy rozne wartosci | COUNT(DISTINCT IdSklepu) |
SUM(x) | Sumuje wartosci kolumny | SUM(Cena * Liczba) |
ORDER BY x DESC | Sortuje malejaco | ORDER BY COUNT(*) DESC |
LIMIT n | Zwraca tylko n pierwszych wierszy | LIMIT 1 (top 1) |
JOIN ... ON | Laczy tabele po wspolnym kluczu | FROM transakcje t JOIN klienci k ON t.IdKlienta = k.IdKlienta |
IN (SELECT ...) | Podzapytanie — „w zbiorze zwroconym przez wewnetrzne SELECT" | WHERE IdKlienta NOT IN (SELECT IdKlienta FROM transakcje) |
SUBSTR(s, start, len) | Wycina fragment napisu (liczone od 1) | SUBSTR('15.01.2025', 4, 2) = '01' |
Wzorzec „top 1" — wraca w 8.1 i 8.4
Pytania typu „kto/co wystepuje najczesciej" rozwiazujesz jednym schematem: grupuj, policz, posortuj malejaco, wez pierwszy.
SELECT IdKlienta, COUNT(*) AS ile
FROM transakcje
GROUP BY IdKlienta
ORDER BY ile DESC
LIMIT 1;
Dziala dla „najwiecej transakcji", „najwiekszy obrot", „najczestszy produkt" — zmieniasz tylko, co zliczasz i po czym grupujesz.
Wzorzec „kto nic nie zrobil" — dla 8.2
Klasyczny NOT IN z podzapytaniem. Najpierw bierzesz zbior tych, ktorzy cos zrobili, potem filtrujesz „wszyscy poza nimi".
SELECT Plec, COUNT(*) FROM klienci
WHERE IdKlienta NOT IN (SELECT DISTINCT IdKlienta FROM transakcje)
GROUP BY Plec;
Mapa rozwiazan dla podpunktow 8.1–8.5
| Podpunkt | Tabele potrzebne | Wzorzec | Wynik |
|---|---|---|---|
| 8.1 | klienci + transakcje | group + count + top 1 | Marcelino Kruk (8 transakcji) |
| 8.2 | klienci + transakcje (NOT IN) | antyjoin + group by plec | K = 689, M = 651 |
| 8.3 | transakcje + opis_transakcji | filter (pusty sprzedawca) + DISTINCT + SUM | 28 sklepow, 19443,29 zl |
| 8.4 | transakcje | group po (sprzedawca, miesiac) + count DISTINCT sklepow + top 1 | sprzedawca 14, miesiac 01, 3 sklepy |
| 8.5 | Produkty + Kategorie + opis_transakcji | JOIN + LIKE + DISTINCT | zapytanie SQL |
Setup srodowiska i przygotowanie danych
Zanim napiszesz pierwsze zapytanie, zaimportuj kazdy plik jako tabele o tej samej nazwie (bez rozszerzenia .txt) w trybie „first row contains column names" z separatorem TAB. Kolumna IdSprzedawcy jest tekstowa, bo dla kas samoobslugowych przychodzi pusty napis — nie liczba, nie NULL.
.mode tabs
.import klienci.txt klienci
.import transakcje.txt transakcje
.import opis_transakcji.txt opis_transakcji
-- dodajemy kolumne liczbowa z cena (oryginal zostaje nietkniety)
ALTER TABLE opis_transakcji ADD COLUMN CenaNum REAL;
UPDATE opis_transakcji SET CenaNum = REPLACE(Cena, ',', '.');
Bez podmiany przecinka na kropke SQLite traktuje Cena jako tekst i SUM(Cena*Liczba) zwroci smieci. Jeden UPDATE z REPLACE wystarczy — zostawiasz oryginalna kolumne tekstowa nietknieta, dodajesz osobno wersje numeryczna.
Po imporcie odpal po jednym SELECT COUNT(*) per tabela i porownaj z liczba wierszy w pliku .txt minus naglowek. Konwencja napisow: pojedyncze apostrofy ('01', '') — standard SQL, akceptowane na CKE. Niektore srodowiska (Access, niektore arkusze) toleruja podwojne, ale apostrofy sa bezpieczne.
Ten sam problem co wyzej (puste IdSprzedawcy = pusty string, nie NULL) jest tu ostrzejszy — GUI import w phpMyAdmin (zakladka Import, format CSV) zawsze wysyla pusty string, nigdy prawdziwego NULL, niezaleznie od typu docelowej kolumny. Jesli kolumna jest typu INT, caly wiersz wywali blad #1366 Incorrect integer value. Najpewniejsza droga to jedno zapytanie LOAD DATA, ktore omija caly GUI wrapper i robi konwersje w locie:
LOAD DATA LOCAL INFILE 'transakcje.txt'
INTO TABLE Transakcje
FIELDS TERMINATED BY '\t'
LINES TERMINATED BY '\r\n'
(IdTransakcji, @data, IdKlienta, IdSklepu, @sprzedawca)
SET
DataTransakcji = STR_TO_DATE(@data, '%d.%m.%Y'),
IdSprzedawcy = NULLIF(@sprzedawca, '');
NULLIF(@sprzedawca, '') zamienia pusty string na prawdziwy NULL w locie — bez tego ani IS NULL, ani = '' osobno nie zlapie wszystkiego, jesli import robiles kilka razy roznymi metodami. STR_TO_DATE(@data, '%d.%m.%Y') konwertuje date z formatu DD.MM.YYYY na prawdziwy typ DATE — dzieki temu w zapytaniach uzywasz MONTH(DataTransakcji) zamiast wycinania fragmentu tekstu, i sortowanie chronologiczne dziala od razu, bez polegania na tym, ze SUBSTR na tekscie akurat sortuje sie tak samo jak data.
| Roznica | SQLite (ten przewodnik) | MySQL / phpMyAdmin |
|---|---|---|
| Puste pole przy imporcie | zawsze pusty string '' | pusty string '' przy GUI CSV import; prawdziwy NULL tylko przez LOAD DATA + NULLIF |
| Fragment daty | SUBSTR(data, 4, 2) na tekscie | kolumna typu DATE + MONTH(data) / DATE_FORMAT(data, '%m') |
| LIMIT n | LIMIT n | LIMIT n (identyczne) |
| Laczenie tekstow | || | CONCAT(a, b) |
| Nazwy tabel/kolumn | bez cudzyslowow | w bactickach `Transakcje` — wymagane gdy nazwa koliduje ze slowem kluczowym |
Rozwiazania 8.1–8.4 — przejscie krok po kroku
8.1 — klient z najwieksza liczba transakcji
Pytanie zwraca imie i nazwisko, a te kolumny zyja w klienci. Liczba transakcji siedzi w transakcje. JOIN jest wymuszony — samo GROUP BY IdKlienta na transakcje nie zwrocilo by nazwiska.
SELECT k.Imie, k.Nazwisko, COUNT(*) AS ile
FROM transakcje t
JOIN klienci k ON k.IdKlienta = t.IdKlienta
GROUP BY t.IdKlienta
ORDER BY ile DESC
LIMIT 1;
Czytanie linijka po linijce:
FROM transakcje t— tabela bazowa, bo zliczamy transakcje. Aliastskraca dalsze referencje.JOIN klienci k ON k.IdKlienta = t.IdKlienta— do kazdej transakcji dokleja wiersz z klientem o tym samymIdKlienta.GROUP BY t.IdKlienta— zwija wiersze do jednego per klient. Grupujemy po identyfikatorze, nie po imieniu/nazwisku, bo dwoch klientow moze sie nazywac tak samo.COUNT(*) AS ile— liczy ile transakcji trafilo do kazdej grupy.ORDER BY ile DESC— sortuje malejaco: najwiecej transakcji na gorze.LIMIT 1— bierze tylko pierwszego.
JOIN opis_transakcji w tym zadaniu — jedna transakcja moze miec 5 pozycji, wiec klient, ktory raz kupil 5 produktow zostanie policzony 5 razy. Dostaniesz inna osobe (te, ktora kupowala najwiecej pozycji) i tracisz 1 pkt z 1.
8.2 — klienci, ktorzy nic nie kupili, w rozbiciu na plec
Logika „zaprzeczenia": chcemy klientow, ktorych nie ma w tabeli transakcje. Najczystsze podejscie — podzapytanie zwracajace zbior aktywnych klientow i NOT IN w warunku zewnetrznym.
SELECT Plec, COUNT(*) AS ilu
FROM klienci
WHERE IdKlienta NOT IN (
SELECT DISTINCT IdKlienta FROM transakcje
)
GROUP BY Plec;
- Podzapytanie
SELECT DISTINCT IdKlienta FROM transakcje— lista identyfikatorow klientow, ktorzy zrobili przynajmniej jedna transakcje.DISTINCTzapobiega duplikatom. - Warunek zewnetrzny
WHERE IdKlienta NOT IN (...)— filtrujekliencido tych, ktorych nie ma na liscie aktywnych. - Zliczenie z
GROUP BY Plec— rozbija wynik na dwa wiersze.
| Plec | ilu |
|---|---|
| K | 689 |
| M | 651 |
LEFT JOIN transakcje ON ... WHERE transakcje.IdKlienta IS NULL. Daje ten sam wynik, czesto szybsza na duzych danych, ale NOT IN czyta sie doslownie jak intencja „wykluczyc tych z listy".
8.3 — kasy samoobslugowe: liczba sklepow i laczna kwota
Kasa samoobslugowa to wiersz z IdSprzedawcy = '' (pusty napis) — w danych z arkusza CKE tak przyszlo z importu. W innym srodowisku (np. DB Browser z opcja treat empty as NULL) ten sam rekord moze przyjsc jako NULL. Bezpieczny warunek lapie obie sytuacje na raz: IdSprzedawcy = '' OR IdSprzedawcy IS NULL. Stosujemy go ponizej w obu zapytaniach. Trzeba dwa osobne zapytania: licznik sklepow zyje w transakcje, kwoty w opis_transakcji.
8.3a — liczba roznych sklepow
SELECT COUNT(DISTINCT IdSklepu) AS rozne_sklepy
FROM transakcje
WHERE IdSprzedawcy = '' OR IdSprzedawcy IS NULL;
8.3b — laczna zaplacona kwota
SELECT SUM(o.CenaNum * o.Liczba) AS kwota
FROM transakcje t
JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
WHERE t.IdSprzedawcy = '' OR t.IdSprzedawcy IS NULL;
JOIN opis_transakcji o— bez tego nie masz dostepu doCenaiLiczba. Klucz laczenia toIdTransakcji.SUM(o.CenaNum * o.Liczba)— dla kazdej pozycji liczy wartosc, potem sumuje po wszystkich pozycjach.WHERE t.IdSprzedawcy = '' OR t.IdSprzedawcy IS NULL— filtr po tabeli transakcji, odporny na oba warianty importu.
8.4 — sprzedawca z najwieksza liczba sklepow w jednym miesiacu
Najtrudniejszy punkt — trzeba grupowac po dwoch kolumnach: sprzedawca + miesiac. Z formatu daty dd.mm.rrrr wyciagamy miesiac funkcja SUBSTR.
SELECT IdSprzedawcy,
SUBSTR(DataTransakcji, 4, 2) AS miesiac,
COUNT(DISTINCT IdSklepu) AS sklepy
FROM transakcje
WHERE IdSprzedawcy <> '' AND IdSprzedawcy IS NOT NULL
GROUP BY IdSprzedawcy, miesiac
ORDER BY sklepy DESC
LIMIT 1;
SUBSTR(DataTransakcji, 4, 2)— trzy argumenty: napis, start, dlugosc. Dla'15.01.2025'bierze 2 znaki od pozycji 4, czyli'01'. SQLite liczy znaki od 1.WHERE IdSprzedawcy <> '' AND IdSprzedawcy IS NOT NULL— wykluczamy kasy samoobslugowe w obu wariantach importu.<>to standardowy operator nierownosci (rownowaznie!=).GROUP BY IdSprzedawcy, miesiac— tworzy grupy „sprzedawca X w miesiacu M".COUNT(DISTINCT IdSklepu)— w obrebie grupy liczy ROZNE sklepy. Jesli sprzedawca obsluzyl 50 transakcji w 3 sklepach, dostaje 3.ORDER BY sklepy DESC LIMIT 1— bierze top.
| IdSprzedawcy | miesiac | sklepy |
|---|---|---|
| 14 | 01 | 3 |
GROUP BY IdSprzedawcy? Bo wtedy policzylbys wszystkie rozne sklepy sprzedawcy w calym okresie szesciu miesiecy — a zadanie pyta o „najwieksza liczbe roznych sklepow w jednym miesiacu". Grupowanie po (sprzedawca, miesiac) izoluje kazdy miesiac osobno.
Zestawienie odpowiedzi 8.1–8.4
| Podpunkt | Odpowiedz |
|---|---|
| 8.1 | Marcelino Kruk — 8 transakcji |
| 8.2 | K = 689, M = 651 (nic nie kupili) |
| 8.3 | 28 roznych sklepow; 19443,29 zl lacznie |
| 8.4 | sprzedawca 14, styczen (01), 3 rozne sklepy |
Rozwiazanie 8.5 — zapytanie SQL na trzech tabelach
Zadanie 8.5 wprowadza dwie nowe tabele: Kategorie (IdKategorii, NazwaKategorii) i Produkty (IdProduktu, IdKategorii, Nazwa, Opis). Wraz z opis_transakcji mamy trzy zrodla, ktore trzeba polaczyc. Polecenie wymaga listy zakupionych produktow z kategorii „spozywcze", ktorych opis zawiera fragment „do ekspresu kolbowego". Kazde z tych slow odpowiada dokladnie jednej operacji w zapytaniu.
Mapowanie wymagan na operacje SQL
| Wymaganie z polecenia | Operacja SQL | Tabela uzywana do filtra |
|---|---|---|
| kategoria „spozywcze" | WHERE k.NazwaKategorii = 'spozywcze' | Kategorie |
| opis zawiera fragment | WHERE p.Opis LIKE '%do ekspresu kolbowego%' | Produkty |
| zostal zakupiony | JOIN opis_transakcji (domyslnie INNER) | opis_transakcji |
| kazdy produkt raz | SELECT DISTINCT | — |
Pelne zapytanie
SELECT DISTINCT p.IdProduktu, p.Nazwa
FROM Produkty p
JOIN Kategorie k ON k.IdKategorii = p.IdKategorii
JOIN opis_transakcji o ON o.IdProduktu = p.IdProduktu
WHERE k.NazwaKategorii = 'spozywcze'
AND p.Opis LIKE '%do ekspresu kolbowego%';
Co robi kazdy fragment
FROM Produkty p — startujemy od katalogu produktow, bo z niego mamy wyciagnac dwie kolumny wyjsciowe (IdProduktu, Nazwa).
JOIN Kategorie k ON k.IdKategorii = p.IdKategorii — podpinamy tabele kategorii po kluczu obcym. W Produkty mamy tylko numer kategorii, a polecenie podaje nazwe „spozywcze". Trzeba przelozyc nazwe na ID — przez JOIN albo podzapytanie. Samo JOIN bez przedrostka INNER oznacza dokladnie to samo co INNER JOIN — to standardowy skrot SQL i tej formy uzywamy w kodzie.
JOIN opis_transakcji o ON o.IdProduktu = p.IdProduktu — warunek „zakupiony". Tabela opis_transakcji zawiera kazda pozycje na paragonie. Produkt nigdy niesprzedany nie ma w niej wiersza i INNER JOIN go odetnie (samo JOIN w SQL jest synonimem INNER JOIN). LEFT JOIN wpuscilby tu produkty niesprzedane — wlasnie domyslny INNER JOIN realizuje semantyke „zakupione".
WHERE k.NazwaKategorii = 'spozywcze' — filtr po nazwie kategorii.
AND p.Opis LIKE '%do ekspresu kolbowego%' — LIKE z dwoma znakami procentu. Procent oznacza „dowolny ciag znakow (lub pusty)". Interesuje nas opis, ktory gdziekolwiek w srodku zawiera ten fragment. Bez procentow LIKE dziala jak =.
SELECT DISTINCT — bez tego ten sam produkt pojawi sie tyle razy, ile razy zostal kupiony. Po JOIN-ie z opis_transakcji jeden rzad Produkty mnozy sie przez kazda transakcje, w ktorej wystapil. DISTINCT zgniata duplikaty.
Alternatywa z podzapytaniami (rownowazna)
SELECT IdProduktu, Nazwa
FROM Produkty
WHERE Opis LIKE '%do ekspresu kolbowego%'
AND IdKategorii = (SELECT IdKategorii FROM Kategorie
WHERE NazwaKategorii = 'spozywcze')
AND IdProduktu IN (SELECT DISTINCT IdProduktu FROM opis_transakcji);
Forma dluzsza, zero JOIN-ow w glownej klauzuli. Podzapytanie zwracajace jedna wartosc dla IdKategorii dziala tylko gdy nazwa kategorii jest unikalna — w tym schemacie jest. IN z podzapytaniem realizuje warunek „zakupiony" bez ryzyka mnozenia wierszy, dlatego DISTINCT w glownym SELECT nie jest tu potrzebny.
Pulapki Zadania 8 — osiem mechanizmow, na ktorych traci sie punkty
1. Puste IdSprzedawcy to nie zawsze NULL
Przy imporcie do SQLite z .import pusta kolumna trafia jako pusty napis (''), nie jako NULL. Standardowe WHERE IdSprzedawcy IS NULL zwroci 0 rekordow. Bezpieczny warunek (uzywamy go w kodzie 8.3 i 8.4 powyzej):
WHERE IdSprzedawcy = '' OR IdSprzedawcy IS NULL
Diagnostyka po imporcie:
SELECT COUNT(*) FROM transakcje WHERE IdSprzedawcy = '';
SELECT COUNT(*) FROM transakcje WHERE IdSprzedawcy IS NULL;
Jedna z liczb bedzie zerem, druga 4–5 tysiecy. Warunek = '' OR IS NULL jest odporny w obu przypadkach, wiec mozesz go uzywac bez sprawdzania w jakim trybie zaimportowales dane. W DB Browser for SQLite zaznaczenie treat empty as NULL zmienia zachowanie — sprawdz to.
2. JOIN do opis_transakcji mnozy wiersze
Najwiekszy zlodziej punktow w 8.1. Tabela rozszerzenia ma rekord na pozycje, nie na transakcje.
| Co liczymy | Z czego | Co grupujemy |
|---|---|---|
| Liczba transakcji per klient | transakcje samo | IdKlienta |
| Liczba pozycji per klient | transakcje JOIN opis_transakcji | IdKlienta |
| Wartosc zakupow per klient | transakcje JOIN opis_transakcji | IdKlienta, SUM(Cena*Liczba) |
3. SUBSTR(DataTransakcji, 4, 2) — pozycja 4, dlugosc 2
Format dd.mm.rrrr, SQLite liczy znaki od 1. Czeste pomylki:
SUBSTR(data, 3, 2)— zlapie".0"zamiast"01"SUBSTR(data, 4, 3)— zlapie"01."i grupowanie pojdzie po nadmiarowej kropce- W MS SQL/MySQL semantyka
SUBSTRINGbywa rozna — sprawdz dialekt - W Access uzywasz
Mid(s, 4, 2)
4. Cena z przecinkiem dziesietnym
W pliku jest 5,85. SQLite przy .import wczyta to jako tekst. SUM(Cena * Liczba) zwroci 0 albo ucinek do czesci calkowitej. Dwa rozwiazania:
-- a) konwersja w samym zapytaniu
SELECT SUM(REPLACE(Cena, ',', '.') * Liczba)
FROM opis_transakcji;
-- b) jeden UPDATE po imporcie dodajacy kolumne numeryczna
ALTER TABLE opis_transakcji ADD COLUMN CenaNum REAL;
UPDATE opis_transakcji SET CenaNum = REPLACE(Cena, ',', '.');
Bez tej konwersji w 8.3 dostaniesz 0,00 zamiast 19443,29 zl.
5. LIKE w SQLite jest case-insensitive tylko dla ASCII
SQLite domyslnie traktuje LIKE bez rozroznienia wielkosci liter, ale tylko dla znakow ASCII. "Spozywcze" matchuje sie z "SPOZYWCZE", ale "zywnosc" z polskimi diakrytykami kontra "ZYWNOSC" — niekoniecznie. Bezpieczna wersja z LOWER():
SELECT DISTINCT p.IdProduktu, p.Nazwa
FROM Produkty p
JOIN Kategorie k ON k.IdKategorii = p.IdKategorii
JOIN opis_transakcji o ON o.IdProduktu = p.IdProduktu
WHERE LOWER(k.NazwaKategorii) = 'spozywcze'
AND LOWER(p.Opis) LIKE '%do ekspresu kolbowego%';
6. COUNT(DISTINCT X) vs COUNT(*)
8.3 brzmi „liczba roznych sklepow". Bez DISTINCT policzysz transakcje, nie sklepy:
-- ZLE: zwroci liczbe transakcji samoobslugowych (kilka tysiecy)
SELECT COUNT(IdSklepu) FROM transakcje WHERE IdSprzedawcy = '';
-- OK: zwroci 28
SELECT COUNT(DISTINCT IdSklepu) FROM transakcje WHERE IdSprzedawcy = '';
Analogicznie 8.4 — bez DISTINCT dostaniesz sprzedawce z najwieksza liczba transakcji, nie z najwieksza liczba sklepow.
7. ORDER BY ... LIMIT 1 zaklada brak remisu
Wszystkie cztery podpunkty uzywaja tego wzorca. Dziala tylko gdy wartosc maksymalna jest unikalna. Przy remisie SQLite zwraca jedna wartosc (zaleznie od kolejnosci wstawiania) — mozesz przegrac punkt, bo egzaminator oczekuje dwoch nazwisk.
Diagnostyka — przed LIMIT 1 sprawdz LIMIT 5 i zobacz czy top wartosci sa rozne:
ile imie nazwisko
8 Marcelino Kruk
5 Anna Nowak
5 Piotr Wisniewski
...
Tutaj 8 jest unikalne, wiec bezpiecznie. Gdyby bylo dwoch po 8 — uzyj HAVING z maksimum policzonym osobno:
SELECT k.Imie, k.Nazwisko, COUNT(*) AS ile
FROM klienci k JOIN transakcje t ON t.IdKlienta = k.IdKlienta
GROUP BY k.IdKlienta
HAVING ile = (
SELECT MAX(c) FROM (
SELECT COUNT(*) AS c FROM transakcje GROUP BY IdKlienta
)
);
8. HAVING filtruje agregaty, WHERE filtruje wiersze
-- ZLE: SQL error, COUNT nie istnieje na poziomie WHERE
SELECT IdKlienta FROM transakcje
WHERE COUNT(*) > 5
GROUP BY IdKlienta;
-- OK
SELECT IdKlienta FROM transakcje
GROUP BY IdKlienta
HAVING COUNT(*) > 5;
Zasada: WHERE dziala przed grupowaniem (filtruje surowe wiersze), HAVING dziala po grupowaniu (filtruje grupy po agregacie).
JOIN-ami i podzapytaniami. CKE punktuje kazdy podpunkt osobno — piec krotkich zapytan jest latwiejsze do debugowania i odporniejsze na utrate punktow. Pisz po jednym SELECT per podpunkt.
- Sprawdz typy kolumn po imporcie (
PRAGMA table_info(transakcje)) — wszystko prawdopodobnie jakoTEXT, wiec ceny trzeba przekonwertowac. - Odpal
SELECT COUNT(*)per tabela — porownaj z liczba wierszy w pliku minus naglowek. - Dla kazdego podpunktu policz przewidywany rzad wielkosci (28 sklepow to 2 cyfry, 19000 zl to 5 cyfr) — jesli wynik ma inny rzad, masz blad w grupowaniu lub JOIN-ie.
- Czytaj polecenie doslownie: „ile" wymaga liczby, „kto" — listy. Wpisanie zapytania zwracajacego liste przy poleceniu „podaj liczbe" to strata punktu mimo poprawnej logiki.
Zadania własne — trening na danych z arkusza
Szesc cwiczen w narastajacej trudnosci, na tych samych trzech plikach co arkusz CKE i ewentualnie na dorzuconych tabelach Kategorie, Produkty. Dialekt ANSI SQL — dziala w SQLite, w Access po drobnych korektach (TOP n zamiast LIMIT, Mid() zamiast SUBSTR()).
IdKlienta, Imie, Nazwisko, posortowane po nazwisku rosnaco.
Pokaz odpowiedz
SELECT IdKlienta, Imie, Nazwisko
FROM klienci
WHERE Plec = 'K'
ORDER BY Nazwisko ASC;
Filtrowanie po jednej kolumnie tekstowej + sort. ORDER BY domyslnie ASC — mozna pominac, ale jawnosc czyta sie lepiej w pracy maturalnej.
Pokaz odpowiedz
-- wszyscy klienci
SELECT COUNT(*) AS wszyscy FROM klienci;
-- tylko mezczyzni
SELECT COUNT(*) AS mezczyzni FROM klienci WHERE Plec = 'M';
Dwa krotkie zapytania sa czytelniejsze niz jedno z trikiem. Pierwsze liczy wszystkich, drugie tylko tych z plcia M.
Imie, Nazwisko, liczba transakcji, sort malejaco.
Pokaz odpowiedz
SELECT k.Imie, k.Nazwisko, COUNT(t.IdTransakcji) AS LiczbaT
FROM klienci k
JOIN transakcje t ON t.IdKlienta = k.IdKlienta
GROUP BY k.IdKlienta, k.Imie, k.Nazwisko
ORDER BY LiczbaT DESC
LIMIT 3;
Rozszerzona wersja 8.1 — pelne podium. GROUP BY w scislym ANSI musi zawierac wszystkie nieagregowane kolumny z SELECT.
IdTransakcji i wartosc transakcji dla tych, ktorych laczna wartosc (suma Cena × Liczba po wszystkich pozycjach) przekracza 100 zl. Sort malejaco po wartosci.
Pokaz odpowiedz
SELECT IdTransakcji, ROUND(SUM(CenaNum * Liczba), 2) AS Wartosc
FROM opis_transakcji
GROUP BY IdTransakcji
HAVING SUM(CenaNum * Liczba) > 100
ORDER BY Wartosc DESC;
Na sume grupy trzeba HAVING, nie WHERE. ROUND(..., 2) bo bez tego zobaczysz 107.8499999 z arytmetyki liczb zmiennoprzecinkowych.
Pokaz odpowiedz
SELECT Imie, Nazwisko
FROM klienci
WHERE IdKlienta NOT IN (
SELECT IdKlienta
FROM transakcje
WHERE SUBSTR(DataTransakcji, 4, 2) = '01'
)
ORDER BY Nazwisko, Imie;
SUBSTR(DataTransakcji, 4, 2) = '01' wycina dwa znaki z pozycji 4 napisu w formacie dd.mm.rrrr — czyli miesiac. Ta sama konwencja co w 8.4 powyzej, trzymamy sie jednego sposobu w calym przewodniku. NOT IN wymaga, by podzapytanie nie zwracalo NULL-i — inaczej caly warunek staje sie nieokreslony i wynik jest pusty. Tu IdKlienta w transakcje jest zawsze wypelniony, wiec bezpiecznie.
Pokaz odpowiedz
SELECT AVG(wartosc) AS srednia
FROM (
SELECT t.IdTransakcji, SUM(o.CenaNum * o.Liczba) AS wartosc
FROM transakcje t
JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
WHERE t.IdSprzedawcy = '' OR t.IdSprzedawcy IS NULL
GROUP BY t.IdTransakcji
) AS koszyki;
Pulapka: AVG(o.CenaNum * o.Liczba) daloby srednia pozycji w paragonie, nie srednia paragonu. Najpierw sumujemy pozycje per transakcja (zapytanie wewnetrzne), potem usredniamy te sumy.
Co warto zapamietac z tego treningu
| Mechanika | Kiedy uzywasz | Pulapka |
|---|---|---|
COUNT(*) vs COUNT(kol) | Liczenie wierszy vs liczenie nie-NULL-i | NULL w kolumnie zaniza wynik COUNT(kol) |
WHERE vs HAVING | Filtr przed agregacja vs po | WHERE SUM(...) > 100 wywala blad skladni |
NOT IN z podzapytaniem | Negacja przynaleznosci | NULL w podzapytaniu daje pusty wynik |
SUBSTR/Mid na dacie-napisie | Daty trzymane jako tekst | Skladnia zalezna od silnika — sprawdz dokumentacje |
GROUP BY + wszystkie nieagregowane kolumny | Kazde zapytanie z agregacja i dodatkowymi polami | Brak kolumny w GROUP BY = blad lub niedeterministyczny wynik |
Zadania własne — do samodzielnego rozwiązania
Siedem cwiczen na skladnie SQL z zadania 8, na tym samym schemacie klienci, transakcje, opis_transakcji co arkusz CKE. Zaczynamy od rozgrzewki, konczymy na pelnym podzapytaniu z NOT IN.
Cwiczenie 1 (rozgrzewka, 2 min)
Policz, ilu klientow ma plec K.
Odpowiedz
SELECT COUNT(*) AS ilu
FROM klienci
WHERE Plec = 'K';
COUNT(*) zlicza wiersze pasujace do WHERE. Alias ilu daje czytelna nazwe kolumny w wyniku.
Cwiczenie 2 (rozgrzewka, 2 min)
Wypisz rozne identyfikatory sklepow ktore wystapily w transakcjach, posortowane rosnaco.
Odpowiedz
SELECT DISTINCT IdSklepu
FROM transakcje
ORDER BY IdSklepu;
DISTINCT usuwa duplikaty. ORDER BY domyslnie sortuje rosnaco.
Cwiczenie 3 (srednie, 5 min)
Dla kazdego klienta podaj jego imie, nazwisko i sume wartosci jego zakupow (cena razy liczba po wszystkich pozycjach). Pokaz tylko tych, ktorzy wydali ponad 500 zl.
Odpowiedz
SELECT k.Imie, k.Nazwisko, SUM(o.CenaNum * o.Liczba) AS suma
FROM klienci k
JOIN transakcje t ON t.IdKlienta = k.IdKlienta
JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
GROUP BY k.IdKlienta, k.Imie, k.Nazwisko
HAVING SUM(o.CenaNum * o.Liczba) > 500
ORDER BY suma DESC;
Po GROUP BY filtruje sie agregaty przez HAVING, nie WHERE. WHERE dziala przed grupowaniem, HAVING po. JOIN bez LEFT odetnie klientow bez transakcji — i o to chodzi (suma > 500 musi istniec).
Cwiczenie 4 (srednie, 7 min)
Podaj srednia wartosc pojedynczej pozycji (kolumna Cena) w kazdym miesiacu. Wynik posortuj chronologicznie. Srednia zaokraglij do 2 miejsc.
Odpowiedz
SELECT SUBSTR(t.DataTransakcji, 4, 2) AS miesiac,
ROUND(AVG(o.CenaNum), 2) AS srednia
FROM transakcje t
JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
GROUP BY SUBSTR(t.DataTransakcji, 4, 2)
ORDER BY miesiac;
SUBSTR(DataTransakcji, 4, 2) wycina dwa znaki od pozycji 4 — to jest miesiac w formacie dd.mm.rrrr. Ta sama funkcja co w 8.4. To samo wyrazenie musi byc w SELECT i GROUP BY. Sortowanie po napisie '01' < '02' dziala bo miesiac zawsze ma dwie cyfry.
Cwiczenie 5 (srednie, 7 min)
Podaj 5 klientow z najwyzsza laczna kwota zakupow. Wyswietl imie, nazwisko i te sume.
Odpowiedz
SELECT k.Imie, k.Nazwisko, SUM(o.CenaNum * o.Liczba) AS suma
FROM klienci k
JOIN transakcje t ON t.IdKlienta = k.IdKlienta
JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
GROUP BY k.IdKlienta, k.Imie, k.Nazwisko
ORDER BY suma DESC
LIMIT 5;
Standardowy patron "top N po agregacie": JOIN, GROUP BY, ORDER BY agregat DESC, LIMIT. Grupowanie po IdKlienta jest pewniejsze niz po imieniu i nazwisku — dwie osoby moga miec to samo nazwisko.
Cwiczenie 6 (trudne, 10 min)
Wypisz klientow (imie, nazwisko), ktorzy nie zrobili zadnej transakcji. Zrob to dwoma sposobami: przez NOT IN i przez LEFT JOIN.
Odpowiedz
Sposob 1 — NOT IN (tak rozwiazujemy 8.2 w glownej czesci):
SELECT Imie, Nazwisko
FROM klienci
WHERE IdKlienta NOT IN (
SELECT DISTINCT IdKlienta FROM transakcje
)
ORDER BY Nazwisko, Imie;
Podzapytanie zwraca liste id klientow ktorzy maja przynajmniej jedna transakcje. NOT IN bierze tych spoza tej listy.
Sposob 2 — LEFT JOIN + IS NULL:
SELECT k.Imie, k.Nazwisko
FROM klienci k
LEFT JOIN transakcje t ON t.IdKlienta = k.IdKlienta
WHERE t.IdKlienta IS NULL
ORDER BY k.Nazwisko, k.Imie;
LEFT JOIN doklada NULL dla klientow bez transakcji. Filtr t.IdKlienta IS NULL zostawia tylko ich. Uwaga na NOT IN: jesli w podzapytaniu pojawi sie NULL w kolumnie IdKlienta, NOT IN zwroci pusty wynik. Tu kolumna jest kluczem obcym, NULL-i nie ma, wiec bezpiecznie.
Cwiczenie 6b (trudne, 8 min)
Ktory sklep obsluzyl najmniej transakcji? Wynik: IdSklepu i liczba transakcji.
Odpowiedz
SELECT IdSklepu, COUNT(*) AS ile
FROM transakcje
GROUP BY IdSklepu
ORDER BY ile ASC
LIMIT 1;
Dokladnie ten sam szkielet co "top 1" (8.1, Cwiczenie 3, 5) — GROUP BY, COUNT, ORDER BY, LIMIT 1 — ale z ASC zamiast DESC. Celowo: uczen, ktory wyuczyl sie wzorca na pamiec z DESC, czesto kopiuje go bezmyslnie nawet gdy pytanie brzmi "najmniej". Zawsze czytaj polecenie przed doborem kierunku sortowania.
Cwiczenie 7 (maturalne, 12 min)
Dla kazdej plci podaj: ilu jest klientow tej plci w bazie oraz laczna wartosc ich zakupow. Klientow bez transakcji tez wlicz do licznika.
Odpowiedz
SELECT k.Plec,
COUNT(DISTINCT k.IdKlienta) AS ilu,
SUM(o.CenaNum * o.Liczba) AS suma_zakupow
FROM klienci k
LEFT JOIN transakcje t ON t.IdKlienta = k.IdKlienta
LEFT JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
GROUP BY k.Plec
ORDER BY k.Plec;
Dwa triki naraz. Po pierwsze LEFT JOIN — zeby klient bez transakcji nadal sie liczyl. Po drugie COUNT(DISTINCT k.IdKlienta) — bez DISTINCT klient z 5 transakcjami i 3 pozycjami na kazdej zostalby policzony 15 razy. SUM dla klientow bez transakcji dolozy 0 (NULL jest ignorowany w SUM). To laczy logike z 8.2 (rozbicie po plci) z logika z 8.3 (sumowanie wartosci pozycji).
Zadania własne — runda 2 (na ostatnią lekcję)
Siedem kolejnych cwiczen, trudniejszych i celowo innych od cwiczen 1-7 powyzej — kazde wprowadza mechanizm, ktory jeszcze nie padl (CASE WHEN, podzapytanie w FROM liczone jako zbior, kontrast WHERE/HAVING na tym samym zdaniu, podwojne grupowanie). Bez podanych z gory liczb wynikowych — sprawdzaj wynik odpalajac zapytanie na swojej zaimportowanej bazie, to czesc treningu.
Runda 2 / Cwiczenie 1 (srednie, 6 min)
Jaki procent wszystkich transakcji w bazie zostal zrealizowany bez sprzedawcy (kasa samoobslugowa)? Podaj wynik zaokraglony do 1 miejsca po przecinku.
Odpowiedz
SELECT ROUND(
SUM(CASE WHEN IdSprzedawcy = '' OR IdSprzedawcy IS NULL THEN 1 ELSE 0 END) * 100.0
/ COUNT(*), 1
) AS procent
FROM transakcje;
CASE WHEN ... THEN 1 ELSE 0 END zamienia warunek logiczny na liczbe 0/1 dla kazdego wiersza, a SUM po tym zlicza spelniajace warunek — to standardowy sposob na "policz ile procent wierszy spelnia X" bez GROUP BY. Mnozenie przez 100.0 (nie 100) jest krytyczne: dzielenie dwoch liczb calkowitych w SQL czesto obcina czesc ulamkowa (integer division), wiec bez kropki dostaniesz 0 zamiast realnego procentu.
Runda 2 / Cwiczenie 2 (srednie, 8 min)
Ilu klientow zrobilo zakupy w co najmniej dwoch roznych sklepach?
Odpowiedz
SELECT COUNT(*) AS ilu
FROM (
SELECT IdKlienta
FROM transakcje
GROUP BY IdKlienta
HAVING COUNT(DISTINCT IdSklepu) >= 2
) AS wieloklienci;
Podzapytanie w FROM (tabela pochodna) najpierw wylania klientow spelniajacych warunek — grupuje po kliencie i filtruje przez HAVING COUNT(DISTINCT IdSklepu) >= 2. Zewnetrzny COUNT(*) nie liczy juz transakcji ani sklepow, tylko wiersze zwrocone przez podzapytanie, czyli klientow. To inny wzorzec niz dotychczasowe: interesuje nas liczba grup spelniajacych warunek, nie lista ani suma.
Runda 2 / Cwiczenie 3 (srednie, 8 min)
Wypisz (bez duplikatow) identyfikatory klientow, ktorzy choc raz kupili pojedynczy produkt drozszy niz 100 zl za sztuke (kolumna Cena, nie suma koszyka).
Odpowiedz
SELECT DISTINCT t.IdKlienta
FROM transakcje t
JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
WHERE o.CenaNum > 100;
Warunek dotyczy pojedynczego wiersza (jedna pozycja na paragonie), wiec ida do WHERE, nie HAVING — nie ma tu zadnej agregacji ani GROUP BY. Kontrast z Cwiczeniem 3 z poprzedniej rundy ("laczna wartosc zakupow > 500 zl"): tam warunek dotyczyl sumy (HAVING), tutaj pojedynczej wartosci w wierszu (WHERE). Rozroznienie "czy warunek pyta o jeden rekord czy o zsumowana grupe" to najczestszy powod pomylki WHERE/HAVING na maturze.
Runda 2 / Cwiczenie 4 (trudne, 10 min)
Dla kazdego klienta z co najmniej 3 transakcjami podaj srednia wartosc jego pojedynczej transakcji (nie sredni koszyk pozycji, tylko srednia z sum kazdego paragonu). Posortuj malejaco.
Odpowiedz
SELECT IdKlienta, AVG(suma_transakcji) AS srednia
FROM (
SELECT t.IdKlienta, t.IdTransakcji, SUM(o.CenaNum * o.Liczba) AS suma_transakcji
FROM transakcje t
JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
GROUP BY t.IdTransakcji, t.IdKlienta
) AS koszyki
GROUP BY IdKlienta
HAVING COUNT(*) >= 3
ORDER BY srednia DESC;
Dwupoziomowe grupowanie. Wewnetrzne zapytanie grupuje po IdTransakcji, zeby dostac wartosc kazdego paragonu osobno (jedna transakcja = jedna suma, niezaleznie od liczby pozycji). Dopiero zewnetrzne GROUP BY IdKlienta usrednia te juz gotowe sumy paragonow. Gdyby policzyc AVG(o.CenaNum * o.Liczba) bezposrednio z opis_transakcji, dostalibysmy srednia wartosc pozycji, nie transakcji — dokladnie ta sama pulapka co w Cwiczeniu 6 z poprzedniej rundy, tu dodatkowo z warunkiem HAVING COUNT(*) >= 3 na liczbie transakcji klienta.
Runda 2 / Cwiczenie 5 (trudne, 8 min, tabele z 8.5)
Dla kategorii "spozywcze" podaj laczna wartosc sprzedazy (suma Cena * Liczba) wszystkich produktow tej kategorii.
Odpowiedz
SELECT SUM(o.CenaNum * o.Liczba) AS wartosc
FROM opis_transakcji o
JOIN Produkty p ON p.IdProduktu = o.IdProduktu
JOIN Kategorie k ON k.IdKategorii = p.IdKategorii
WHERE k.NazwaKategorii = 'spozywcze';
Ten sam szkielet JOIN co w 8.5 (opis_transakcji + Produkty + Kategorie), ale zamiast listy konkretnych produktow (SELECT DISTINCT) liczymy sume wartosci. Pokazuje, ze jeden zestaw polaczonych tabel odpowiada na rozne pytania w zaleznosci od tego, co wstawisz w SELECT — lista, liczba, czy suma.
Runda 2 / Cwiczenie 6 (maturalne, 10 min — laczy trzy wzorce naraz)
Ktory miesiac mial najwyzsza laczna wartosc sprzedazy (wszystkie transakcje, wszystkie sklepy razem)?
Odpowiedz
SELECT SUBSTR(t.DataTransakcji, 4, 2) AS miesiac,
SUM(o.CenaNum * o.Liczba) AS wartosc
FROM transakcje t
JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
GROUP BY miesiac
ORDER BY wartosc DESC
LIMIT 1;
Celowe podsumowanie calego materialu: grupowanie po miesiacu przez SUBSTR (jak w 8.4 i Cwiczeniu 4), sumowanie wartosci przez JOIN z opis_transakcji (jak w 8.3), i wzorzec top-1 przez ORDER BY + LIMIT (jak w 8.1). Jesli uczen samodzielnie ulozy to zapytanie bez podpowiedzi, ma juz caly warsztat Zadania 8 opanowany.
Wskazowki dla prowadzacego
Pytania diagnostyczne na poczatek
- Co robi
JOINmiedzy dwiema tabelami? (laczy wiersze po wspolnej kolumnie, np. IdKlienta) - Czym rozni sie
WHEREodHAVING? (WHERE filtruje wiersze przed grupowaniem, HAVING filtruje grupy po GROUP BY) - Co zwraca
COUNT(*)a coCOUNT(kolumna)? (COUNT(*) liczy wszystkie wiersze, COUNT(kolumna) pomija NULL) - Jak posortowac wynik malejaco i wziac tylko pierwsze 5? (ORDER BY ... DESC LIMIT 5)
- Czym sie rozni
NOT IN (podzapytanie)odLEFT JOIN ... WHERE ... IS NULL? (oba znajduja "brakujace" — NOT IN czytelniejsze, LEFT JOIN szybsze na duzych danych) - Jak wyciagnac miesiac z daty zapisanej jako tekst
dd.mm.rrrr? (SUBSTR(data, 4, 2)w SQLite,Mid(data, 4, 2)w Access)
Typowe bledy uczniow
- Brak GROUP BY przy agregacie — uczen pisze
SELECT IdKlienta, SUM(CenaNum) FROM opis_transakcjibez GROUP BY. Rozpoznasz po jednym wierszu w wyniku zamiast wielu. Korekta: kazda kolumna w SELECT ktora nie jest agregatem musi byc w GROUP BY. - WHERE zamiast HAVING dla agregatu — proba
WHERE SUM(CenaNum) > 100. Baza zwraca blad. Korekta: filtry na funkcjach agregujacych ida do HAVING po GROUP BY. - NOT IN z NULL — gdy podzapytanie zwroci chocby jeden NULL, calosc daje pusty wynik. Korekta: dodaj
WHERE IdKlienta IS NOT NULLw podzapytaniu, jesli kolumna moze byc NULL. - JOIN z opis_transakcji przy zliczaniu transakcji — uczen w 8.1 dorzuca
JOIN opis_transakcjii liczy 5 razy klienta ktory na jednym paragonie kupil 5 produktow. Korekta: jesli pytanie dotyczy transakcji, nie laczymy z tabela pozycji. - Srednia liczona recznie — uczen sumuje i dzieli przez COUNT zamiast uzyc AVG. Korekta: AVG(CenaNum) sam radzi sobie z dzieleniem; ROUND(AVG(CenaNum), 2) daje 2 miejsca po przecinku.
Propozycja podzialu 45-minutowej lekcji
| Minuty | Czynnosc | Cel pedagogiczny |
|---|---|---|
| 0-5 | Pytania diagnostyczne + szybki rzut oka na schemat 3 tabel | Sprawdzic czy uczen rozumie relacje klienci-transakcje-opis_transakcji |
| 5-12 | Razem rozwiazac 8.1 (JOIN klienci+transakcje, GROUP BY, ORDER BY DESC, LIMIT 1) — wynik Marcelino Kruk | Klasyczny patron "top 1" — fundament dla 8.4 |
| 12-22 | 8.2 samodzielnie z podpowiedzia: "uzyj NOT IN i pamietaj o GROUP BY po plci" | Wprowadzenie podzapytania z NOT IN na konkretnym przykladzie |
| 22-32 | 8.3 razem — dwa osobne zapytania (COUNT DISTINCT sklepow, SUM po JOIN-ie z opis_transakcji), warunek = '' OR IS NULL | Polaczenie filtra na pustym sprzedawcy z agregacja po dwoch tabelach |
| 32-40 | 8.4 samodzielnie — SUBSTR na dacie, GROUP BY po dwoch kolumnach, COUNT(DISTINCT IdSklepu) | Najtrudniejszy podpunkt: grupowanie po (sprzedawca, miesiac) |
| 40-45 | Powtorka roznicy WHERE vs HAVING + zadanie domowe (cwiczenia 3, 6, 7) | Utrwalenie najczestszej pulapki maturalnej |
Plan ostatniej lekcji z tego arkusza — przeglad + Runda 2
Zaklada, ze 8.1-8.4 i pierwsza runda cwiczen (1-7) sa juz przerobione z poprzednich sesji. Cel tej lekcji to nie nauka nowego, tylko utrwalenie pod egzamin — mniej tlumaczenia, wiecej samodzielnego pisania SQL-a przy Tobie w roli obserwatora, nie przewodnika.
| Minuty | Czynnosc | Cel pedagogiczny |
|---|---|---|
| 0-5 | Bez podpowiedzi: uczen ustnie tlumaczy roznice WHERE/HAVING i NOT IN/LEFT JOIN | Szybki sanity-check czy podstawy przetrwaly do dzis |
| 5-15 | Runda 2 / Cwiczenie 1 (CASE WHEN + procent) samodzielnie, bez podgladania odpowiedzi | CASE WHEN nie padlo wczesniej — sprawdzasz czy transferuje wiedze na nowa konstrukcje |
| 15-25 | Runda 2 / Cwiczenie 3 i 4 (WHERE vs HAVING na dwoch bliznaczych zdaniach, potem podwojne grupowanie) | Cwiczenie 3 i 4 to celowo ustawiony kontrast — jesli uczen pomyli WHERE/HAVING tutaj, to sygnal do dodatkowego powtorzenia, nie do przejscia dalej |
| 25-35 | Runda 2 / Cwiczenie 6 (capstone: SUBSTR + JOIN + SUM + top-1 w jednym zapytaniu) bez dzielenia na kroki | Test integracji — czy uczen samodzielnie sklada trzy poznane wzorce w jedno zapytanie |
| 35-42 | Szybki przeglad pulapek z checklisty (info-box "Checklist przed oddaniem arkusza" wyzej) — uczen czyta na glos i mowi, ktora go kiedys zlapala | Utrwalenie przez werbalizacje wlasnych bledow, nie powtorzenie teorii |
| 42-45 | Domkniecie: czego uczen NIE jest pewien — zapisz to i wroc do tego jako pierwszy punkt na sesji powtorkowej tuz przed egzaminem | Jasny kontrakt na ostatnia prosta zamiast ogolnikowego "powtorz sobie SQL" |
Czego NIE robic
- Nie pokazuj od razu CTE ani window functions — na maturze rozszerzonej z informatyki one nie sa wymagane, a uczen nauczony WITH bedzie sie z nim mecyl tam gdzie wystarczy GROUP BY. Trzymaj sie SELECT/JOIN/GROUP BY/HAVING/ORDER BY/LIMIT.
- Nie zaczynaj od skomplikowanego JOIN-a trzech tabel — najpierw 8.1 na dwoch tabelach (klienci + transakcje), potem 8.2 z NOT IN, potem dopiero 8.3 z JOIN-em do opis_transakcji. Kazdy nastepny podpunkt dokladajacy jedna nowa rzecz.
- Nie tlumacz NOT IN przez "to jak w matematyce dopelnienie zbioru" — uczen i tak nie skojarzy. Lepiej pokaz na danych: "to sa wszyscy klienci MINUS ci ktorzy maja chocby jedna transakcje".
1. Systemy liczbowe
Na maturze z informatyki systemy liczbowe pojawiają się prawie co rok — konwersje, arytmetyka, porównania. Musisz umieć szybko przeliczać między BIN, OCT, DEC i HEX.
Czym jest system liczbowy?
DEC (10) → 0-9 · BIN (2) → 0-1 · OCT (8) → 0-7 · HEX (16) → 0-9, A-F
| System | Podstawa | Cyfry | Przykład |
|---|---|---|---|
| Binarny (BIN) | 2 | 0, 1 | 1011₂ = 11₁₀ |
| Ósemkowy (OCT) | 8 | 0–7 | 13₈ = 11₁₀ |
| Dziesiętny (DEC) | 10 | 0–9 | 11₁₀ |
| Szesnastkowy (HEX) | 16 | 0–9, A–F | B₁₆ = 11₁₀ |
Konwersja DEC → BIN (dzielenie przez 2)
156₁₀ → BIN:
156 ÷ 2 = 78 reszta 0
78 ÷ 2 = 39 reszta 0
39 ÷ 2 = 19 reszta 1
19 ÷ 2 = 9 reszta 1
9 ÷ 2 = 4 reszta 1
4 ÷ 2 = 2 reszta 0
2 ÷ 2 = 1 reszta 0
1 ÷ 2 = 0 reszta 1
Reszty od dołu: 10011100₂ ✓
Konwersja BIN → DEC (wagi pozycji)
10011100₂ → DEC:
1×2⁷ + 0×2⁶ + 0×2⁵ + 1×2⁴ + 1×2³ + 1×2² + 0×2¹ + 0×2⁰
= 128 + 0 + 0 + 16 + 8 + 4 + 0 + 0
= 156₁₀ ✓
Szybka konwersja BIN ↔ HEX (grupy po 4 bity)
10011100₂ → HEX:
1001 1100
9 C
= 9C₁₆ ✓
Tablica:
0000=0 0100=4 1000=8 1100=C
0001=1 0101=5 1001=9 1101=D
0010=2 0110=6 1010=A 1110=E
0011=3 0111=7 1011=B 1111=F
Szybka konwersja BIN ↔ OCT (grupy po 3 bity)
10011100₂ → OCT:
10 011 100
2 3 4
= 234₈ ✓
Arytmetyka binarna
Dodawanie BIN:
1011 (11₁₀)
+ 0110 ( 6₁₀)
------
10001 (17₁₀) ✓
Zasady: 0+0=0, 0+1=1, 1+0=1, 1+1=10 (0 i przeniesienie 1)
Konwersje w Pythonie
# DEC → inne systemy
bin(156) # '0b10011100'
oct(156) # '0o234'
hex(156) # '0x9c'
# Inne systemy → DEC
int('10011100', 2) # 156
int('234', 8) # 156
int('9C', 16) # 156
# Usuwanie prefiksu
bin(156)[2:] # '10011100'
📝 Zadanie praktyczne
Zamień liczbę 173₁₀ na system binarny, ósemkowy i szesnastkowy.
173₁₀ → BIN (dzielenie przez 2):
173 ÷ 2 = 86 r 1
86 ÷ 2 = 43 r 0
43 ÷ 2 = 21 r 1
21 ÷ 2 = 10 r 1
10 ÷ 2 = 5 r 0
5 ÷ 2 = 2 r 1
2 ÷ 2 = 1 r 0
1 ÷ 2 = 0 r 1
173₁₀ = 10101101₂
BIN → OCT (grupy po 3): 010 101 101 = 255₈
BIN → HEX (grupy po 4): 1010 1101 = AD₁₆
Zadania maturalne — zestaw z 27.09
Arkusz zawiera treści zadań bez rozwiązań — możesz wysłać go uczniowi przed sesją.
Sześć zadań w formacie Części 1 (bez komputera). Każde rozwiąż na kartce z rozpisanym działaniem — na maturze punkt idzie za drogę, nie tylko za wynik. Rozwiązanie odkrywaj dopiero po swojej próbie.
SYSTEMY LICZBOWE - 6 zadań, liczymy na kartce, bez kalkulatora.
Odeślij same wyniki, po jednym w linii. Przy 1.4 i 1.6 pokaż kartkę w kamerze.
1.1 Zapisz w systemie dziesiętnym liczbę 110110(2).
1.2 Zapisz liczbę 213 w systemie szesnastkowym oraz dwójkowym.
1.3 Zapisz liczbę 2F(16) w systemie dwójkowym i dziesiętnym.
1.4 Zapisz liczbę -37 w ośmiobitowym kodzie uzupełnień do dwóch (U2).
1.5 Jaki zakres liczb całkowitych zapiszesz na 8 bitach w U2, a jaki bez
znaku? Ile różnych wartości w obu przypadkach?
1.6 Oblicz 1221(3) + 143(5). Wynik podaj w systemie szesnastkowym
i trójkowym.
Zadanie 1.1 — dwójkowy na dziesiętny
Zapisz w systemie dziesiętnym liczbę 1101102.
pozycje: 32 16 8 4 2 1
bity: 1 1 0 1 1 0
suma: 32+16+4+2 = 54
Odpowiedź: 54. Sprawdzenie w drugą stronę: 54 − 32 = 22, 22 − 16 = 6, 6 − 4 = 2, 2 − 2 = 0 — wykorzystane wagi 32, 16, 4 i 2, czyli dokładnie zapalone bity.
Zadanie 1.2 — dziesiętny na szesnastkowy i dwójkowy
Zapisz liczbę 213 w systemie szesnastkowym oraz dwójkowym.
213 : 16 = 13 reszta 5 -> cyfra 5
13 : 16 = 0 reszta 13 -> cyfra D
czytamy reszty od dolu: D5
D = 1101, 5 = 0101 -> 11010101
Odpowiedź: D516 = 110101012. Szesnastkowy to skrót zapisu dwójkowego — jedna cyfra hex to dokładnie cztery bity, dlatego konwersja hex → bin jest podstawieniem, bez dzielenia.
Zadanie 1.3 — szesnastkowy na dwójkowy i dziesiętny
Zapisz liczbę 2F16 w systemie dwójkowym i dziesiętnym.
2 = 0010, F = 1111 -> 00101111 = 101111
dziesietnie: 2*16 + 15 = 47
Odpowiedź: 1011112 = 47. Wiodące zera można pominąć w wyniku, ale przy podstawianiu cyfra po cyfrze musisz pisać pełne czwórki bitów — pominięcie zera w środku liczby to najczęstszy błąd w tym typie zadania.
Zadanie 1.4 — kod uzupełnień do dwóch (U2)
Zapisz liczbę −37 w ośmiobitowym kodzie uzupełnień do dwóch.
1. wartosc bezwzgledna: 37 = 00100101
2. negacja kazdego bitu: 11011010
3. dodaj 1: 11011011
Odpowiedź: 11011011. Kontrola: pierwszy bit to 1, czyli liczba ujemna. Wartość odczytujemy jako −128 + 64 + 16 + 8 + 2 + 1 = −37. Zawsze rób tę kontrolę — zajmuje 15 sekund i wyłapuje pomyłkę w kroku 3.
Zadanie 1.5 — zakres bitowy
Jaki zakres liczb całkowitych zapiszesz na 8 bitach w kodzie U2, a jaki bez znaku? Ile różnych wartości w obu przypadkach?
| Kod | Zakres | Ile wartości |
|---|---|---|
| bez znaku | 0 … 255 | 256 |
| U2 | −128 … 127 | 256 |
Liczba wartości jest ta sama (28 = 256) — U2 nie dodaje pojemności, tylko przesuwa zakres. Asymetria (−128, ale nie +128) wynika z tego, że zero zajmuje jeden układ bitów po stronie liczb nieujemnych. Wzór ogólny dla n bitów: −2n−1 … 2n−1−1.
Zadanie 1.6 — działanie w mieszanych bazach (typ CKE)
Oblicz sumę 12213 + 1435 i podaj wynik w systemie szesnastkowym oraz trójkowym.
1221(3) = 1*27 + 2*9 + 2*3 + 1 = 27+18+6+1 = 52
143(5) = 1*25 + 4*5 + 3 = 25+20+3 = 48
suma dziesietnie: 52 + 48 = 100
100 : 16 = 6 r. 4 -> 64(16)
100 : 3 = 33 r. 1
33 : 3 = 11 r. 0
11 : 3 = 3 r. 2
3 : 3 = 1 r. 0
1 : 3 = 0 r. 1 -> 10201(3)
Odpowiedź: 10010 = 6416 = 102013. Schemat jest zawsze ten sam: każdą liczbę z egzotycznej bazy przeliczasz najpierw na dziesiętną, dodajesz normalnie, a potem zamieniasz na bazę docelową. Dodawanie „w słupku" w systemie trójkowym daje większe ryzyko błędu niż oszczędność czasu.
2. Logika i algebra Boole'a
Na maturze pytania o logikę to tablice prawdy, upraszczanie wyrażeń, bramki logiczne. Musisz znać operacje AND, OR, NOT, XOR i prawa De Morgana.
Operatory logiczne
| Operator | Symbol | Python | Opis |
|---|---|---|---|
| AND (koniunkcja) | ∧ / · | and | Prawda gdy OBA prawdziwe |
| OR (alternatywa) | ∨ / + | or | Prawda gdy CHOĆ JEDEN prawdziwy |
| NOT (negacja) | ¬ / ! | not | Odwraca wartość |
| XOR (różnica sym.) | ⊕ | ^ | Prawda gdy DOKŁADNIE JEDEN prawdziwy |
Tablice prawdy
A B │ A∧B A∨B A⊕B ¬A
──────┼────────────────────
0 0 │ 0 0 0 1
0 1 │ 0 1 1 1
1 0 │ 0 1 1 0
1 1 │ 1 1 0 0
Prawa De Morgana
¬(A ∧ B) = ¬A ∨ ¬B
¬(A ∨ B) = ¬A ∧ ¬B
Negacja koniunkcji → alternatywa negacji. Negacja alternatywy → koniunkcja negacji.
Inne prawa algebry Boole'a
| Prawo | AND | OR |
|---|---|---|
| Tożsamości | A ∧ 1 = A | A ∨ 0 = A |
| Dominacji | A ∧ 0 = 0 | A ∨ 1 = 1 |
| Idempotentność | A ∧ A = A | A ∨ A = A |
| Dopełnienie | A ∧ ¬A = 0 | A ∨ ¬A = 1 |
| Podwójna negacja | ¬(¬A) = A | |
Bramki logiczne
AND — wyjście 1 tylko gdy oba wejścia = 1
OR — wyjście 1 gdy choć jedno wejście = 1
NOT — odwraca sygnał (1→0, 0→1)
XOR — wyjście 1 gdy wejścia różne
NAND — NOT + AND (negacja AND)
NOR — NOT + OR (negacja OR)
Upraszczanie wyrażeń — przykład
Uprość: (A ∧ B) ∨ (A ∧ ¬B)
= A ∧ (B ∨ ¬B) ← wyciągnięcie A (rozdzielność)
= A ∧ 1 ← B ∨ ¬B = 1 (prawo dopełnienia)
= A ← A ∧ 1 = A (prawo tożsamości)
Operacje bitowe w Pythonie
a = 0b1100 # 12
b = 0b1010 # 10
a & b # AND → 0b1000 = 8
a | b # OR → 0b1110 = 14
a ^ b # XOR → 0b0110 = 6
~a # NOT → -13 (dopełnienie do 2)
a << 2 # Przesunięcie w lewo: 0b110000 = 48
a >> 1 # Przesunięcie w prawo: 0b0110 = 6
📝 Zadanie praktyczne
Uprość wyrażenie logiczne: (A ∧ B) ∨ (A ∧ ¬B) ∨ (¬A ∧ B)
Krok 1: Grupujemy pierwsze dwa składniki
(A ∧ B) ∨ (A ∧ ¬B) = A ∧ (B ∨ ¬B) = A ∧ 1 = A
Krok 2: Podstawiamy
A ∨ (¬A ∧ B)
Krok 3: Stosujemy prawo absorpcji
A ∨ (¬A ∧ B) = A ∨ B
Odpowiedź: A ∨ B
Zadania maturalne — zestaw z 27.09
Arkusz zawiera treści zadań bez rozwiązań — możesz wysłać go uczniowi przed sesją.
Pięć zadań na tablice prawdy i przekształcanie wyrażeń. Tablicę prawdy rysuj zawsze w pełnej postaci — za samą poprawną tablicę są punkty, nawet gdy końcowy wniosek wyjdzie błędny.
LOGIKA - 5 zadań. Tablice prawdy rysuj na kartce i pokaż w kamerze,
nie przepisuj ich na czat.
2.1 Wypełnij tablicę prawdy wyrażenia (A AND NOT B) OR (NOT A AND B).
Jaką znaną funkcję logiczną otrzymujesz?
2.2 Zapisz negację wyrażenia A AND (B OR C) tak, aby negacja stała przy
poszczególnych zmiennych, nie przed całością.
2.3 Uprość wyrażenie (A AND B) OR (A AND NOT B).
2.4 Ile wierszy ma tablica prawdy funkcji czterech zmiennych? Ile różnych
funkcji logicznych dwóch zmiennych da się zdefiniować?
2.5 Dodajesz dwa pojedyncze bity A i B. Podaj wyrażenia na bit sumy i bit
przeniesienia. Z jakich bramek zbudujesz ten układ?
Zadanie 2.1 — tablica prawdy
Wypełnij tablicę prawdy wyrażenia (A AND NOT B) OR (NOT A AND B). Jaką znaną funkcję logiczną otrzymujesz?
| A | B | A AND NOT B | NOT A AND B | wynik |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 1 | 0 | 0 | 0 |
Wynik to XOR (alternatywa wykluczająca): prawda dokładnie wtedy, gdy argumenty są różne. To najczęściej sprawdzana tożsamość z tego działu, bo XOR jest podstawą sumatora i prostych szyfrów.
Zadanie 2.2 — prawa De Morgana
Zapisz negację wyrażenia A AND (B OR C) tak, aby negacja stała przy poszczególnych zmiennych, nie przed całością.
NOT (A AND (B OR C))
= NOT A OR NOT (B OR C) <- negacja iloczynu = suma negacji
= NOT A OR (NOT B AND NOT C) <- negacja sumy = iloczyn negacji
Odpowiedź: NOT A OR (NOT B AND NOT C). Reguła pamięciowa: negacja wchodząc do środka zamienia AND na OR i OR na AND, a każdy składnik zostaje zanegowany. Sprawdź wynik na jednym wierszu tablicy prawdy, np. A=1, B=0, C=0 — oryginał daje 0, więc negacja musi dać 1.
Zadanie 2.3 — upraszczanie
Uprość wyrażenie (A AND B) OR (A AND NOT B).
(A AND B) OR (A AND NOT B)
= A AND (B OR NOT B) <- wylaczenie wspolnego czynnika
= A AND 1
= A
Odpowiedź: A. Wyrażenie nie zależy od B: jeśli A jest prawdą, jeden z nawiasów zawsze się spełni. Tablica prawdy potwierdza to w czterech wierszach — i jest to w pełni punktowana metoda rozwiązania, jeśli przekształcenia algebraiczne nie wychodzą.
Zadanie 2.4 — liczba wierszy i liczba funkcji
Ile wierszy ma tablica prawdy funkcji czterech zmiennych? Ile różnych funkcji logicznych dwóch zmiennych da się w ogóle zdefiniować?
Wierszy: 24 = 16 — każda zmienna podwaja liczbę kombinacji.
Funkcji dwóch zmiennych: tablica ma 4 wiersze, a w kolumnie wyniku każdy wiersz może być 0 albo 1, czyli 24 = 16 różnych funkcji. Wśród nich są wszystkie znane: AND, OR, XOR, NAND, NOR, implikacja, stała 0, stała 1.
Wzór ogólny: dla n zmiennych jest 22n funkcji — dla trzech zmiennych już 256.
Zadanie 2.5 — półsumator
Dodajesz dwa pojedyncze bity A i B. Podaj wyrażenia logiczne na bit sumy i bit przeniesienia. Z jakich bramek zbudujesz ten układ?
| A | B | suma | przeniesienie |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
suma = A XOR B, przeniesienie = A AND B. Układ to jedna bramka XOR i jedna AND. Wiersz 1+1 pokazuje sens przeniesienia: dwójkowo 1 + 1 = 10, czyli suma 0 i przeniesienie 1 na wyższą pozycję — dokładnie jak w dodawaniu dziesiętnym 5 + 5.
3. Algorytmy — analiza i pseudokod
Analiza pseudokodu to kluczowa umiejętność na maturze — część 1 (arkusz papierowy). Musisz umieć śledzić zmienne krok po kroku i przewidzieć wynik.
Pseudokod — konwencja CKE
• Przypisanie:
x ← 5• Warunek:
jeżeli x > 0 to ... w przeciwnym razie ...• Pętla:
dopóki x > 0 wykonuj ... lub dla i = 1, 2, ..., n wykonuj•
div — dzielenie całkowite, mod — reszta z dzielenia• Tablice:
T[i] (indeksowanie od 1!)Śledzenie algorytmu — technika tabelki
Algorytm:
x ← 156
wynik ← ""
dopóki x > 0 wykonuj:
r ← x mod 2
wynik ← r + wynik (dopisz r na POCZĄTEK)
x ← x div 2
zwróć wynik
Śledzenie (tabelka):
Krok │ x │ r │ wynik
0 │ 156 │ │ ""
1 │ 78 │ 0 │ "0"
2 │ 39 │ 0 │ "00"
3 │ 19 │ 1 │ "100"
4 │ 9 │ 1 │ "1100"
5 │ 4 │ 1 │ "11100"
6 │ 2 │ 0 │ "011100"
7 │ 1 │ 0 │ "0011100"
8 │ 0 │ 1 │ "10011100"
Wynik: "10011100" — to konwersja DEC→BIN!
Złożoność obliczeniowa
| Złożoność | Nazwa | Przykład |
|---|---|---|
| O(1) | Stała | Dostęp do elementu tablicy T[i] |
| O(log n) | Logarytmiczna | Wyszukiwanie binarne |
| O(n) | Liniowa | Przejście po tablicy, szukanie max |
| O(n log n) | Liniowo-log. | Merge sort, Quick sort (avg) |
| O(n²) | Kwadratowa | Bubble sort, Selection sort |
| O(2ⁿ) | Wykładnicza | Generowanie podzbiorów |
Operatory div i mod
div = dzielenie całkowite (zaokrąglenie w dół)
mod = reszta z dzielenia
17 div 5 = 3 17 mod 5 = 2
10 div 3 = 3 10 mod 3 = 1
7 div 2 = 3 7 mod 2 = 1
Zastosowania mod:
• Sprawdzenie parzystości: x mod 2 = 0
• Wyciągnięcie ostatniej cyfry: x mod 10
• Wyciągnięcie cyfr: x mod 10, (x div 10) mod 10, ...
Schemat blokowy
Symbole:
┌─────────────┐
│ START/STOP │ ← Owal
└─────────────┘
┌─────────────┐
│ Operacja │ ← Prostokąt
└─────────────┘
╱ ╲
╱ Warunek ╲ ← Romb (TAK/NIE)
╲ ╱
╲ ╱
┌─────────────┐
│ Wejście/ │
│ Wyjście │ ← Równoległobok
└─────────────┘
Specyfikacja algorytmu
• Dane wejściowe — co otrzymuje algorytm
• Wynik — co zwraca
• Warunek stopu — kiedy pętla się kończy
• Niezmiennik pętli — co jest prawdziwe w każdej iteracji
📝 Zadanie praktyczne
Prześledź algorytm dla T = [3, 7, 2, 5]. Co robi i jaki zwraca wynik?
Algorytm X(T, n):
max ← T[1]
dla i = 2, 3, ..., n wykonuj:
jeżeli T[i] > max to:
max ← T[i]
zwróć max
Śledzenie:
i=1: max ← 3
i=2: T[2]=7 > 3 → max ← 7
i=3: T[3]=2 < 7 → bez zmian
i=4: T[4]=5 < 7 → bez zmian
Zwraca: 7
Opis: Algorytm znajduje MAKSIMUM w tablicy.
Złożoność: O(n) — jedno przejście przez tablicę.
4. Sortowanie i wyszukiwanie
Algorytmy sortowania i wyszukiwania to stały element matury. Musisz znać ich działanie, złożoność i umieć śledzić krok po kroku.
Sortowanie bąbelkowe (Bubble Sort)
# Sortowanie bąbelkowe — O(n²)
# Cały ten blok można wkleić do Colab albo IDLE i uruchomić bez zmian.
def bubble_sort(dane):
T = dane[:] # kopia, żeby nie zepsuć listy wywołującego
n = len(T)
for i in range(n - 1):
for j in range(n - 1 - i):
if T[j] > T[j + 1]:
T[j], T[j + 1] = T[j + 1], T[j]
return T
liczby = [8, 3, 5, 1, 9, 2]
print("przed: ", liczby)
print("po: ", bubble_sort(liczby))
print("oryginał nietknięty:", liczby)
# przed: [8, 3, 5, 1, 9, 2]
# po: [1, 2, 3, 5, 8, 9]
# oryginał nietknięty: [8, 3, 5, 1, 9, 2]
Śledzenie [5, 3, 8, 1]:
Przejście 1: [3,5,1,8] — 8 „wypływa" na koniec
Przejście 2: [3,1,5,8] — 5 na miejscu
Przejście 3: [1,3,5,8] — posortowane ✓
Sortowanie przez wybieranie (Selection Sort)
# Sortowanie przez wybór — O(n²)
def selection_sort(dane):
T = dane[:]
n = len(T)
for i in range(n - 1):
indeks_min = i
for j in range(i + 1, n):
if T[j] < T[indeks_min]:
indeks_min = j
if indeks_min != i:
T[i], T[indeks_min] = T[indeks_min], T[i]
return T
liczby = [8, 3, 5, 1, 9, 2]
print(selection_sort(liczby)) # [1, 2, 3, 5, 8, 9]
Śledzenie [5, 3, 8, 1]:
Krok 1: min=1 (idx 3) → zamień z idx 0 → [1, 3, 8, 5]
Krok 2: min=3 (idx 1) → na miejscu → [1, 3, 8, 5]
Krok 3: min=5 (idx 3) → zamień z idx 2 → [1, 3, 5, 8] ✓
Sortowanie przez wstawianie (Insertion Sort)
# Sortowanie przez wstawianie — O(n²), a dla danych prawie posortowanych O(n)
def insertion_sort(dane):
T = dane[:]
for i in range(1, len(T)):
klucz = T[i]
j = i - 1
while j >= 0 and T[j] > klucz:
T[j + 1] = T[j]
j -= 1
T[j + 1] = klucz
return T
print(insertion_sort([8, 3, 5, 1, 9, 2])) # [1, 2, 3, 5, 8, 9]
print(insertion_sort([1, 2, 3, 4, 5, 6])) # [1, 2, 3, 4, 5, 6] — tu pętla while nie wykona się ani razu
Wyszukiwanie liniowe
# Wyszukiwanie liniowe — O(n), działa na dowolnych danych
def szukaj_liniowo(T, cel):
for i in range(len(T)):
if T[i] == cel:
return i # zwracamy indeks pierwszego trafienia
return -1 # -1 oznacza: nie ma takiego elementu
dane = [8, 3, 5, 1, 9, 2]
print(szukaj_liniowo(dane, 5)) # 2
print(szukaj_liniowo(dane, 7)) # -1
Wyszukiwanie binarne (Binary Search)
# Wyszukiwanie binarne — O(log n), WYŁĄCZNIE na tablicy posortowanej
def szukaj_binarnie(T, cel):
lewy = 0
prawy = len(T) - 1
while lewy <= prawy:
srodek = (lewy + prawy) // 2
if T[srodek] == cel:
return srodek
if T[srodek] < cel:
lewy = srodek + 1
else:
prawy = srodek - 1
return -1
posortowane = [2, 5, 8, 12, 16, 23, 38, 45, 56, 72, 91]
print(szukaj_binarnie(posortowane, 72)) # 9
print(szukaj_binarnie(posortowane, 40)) # -1
# Na tablicy NIEposortowanej zwróci błędny wynik zamiast błędu:
print(szukaj_binarnie([8, 3, 5, 1, 9, 2], 8)) # -1, mimo że 8 stoi na pierwszym miejscu
Śledzenie: T=[1,3,5,8,12,15,20], szukana=12
Krok 1: lewy=0, prawy=6, środek=3 → T[3]=8 < 12 → lewy=4
Krok 2: lewy=4, prawy=6, środek=5 → T[5]=15 > 12 → prawy=4
Krok 3: lewy=4, prawy=4, środek=4 → T[4]=12 = 12 → ZNALEZIONO! ✓
Tylko 3 kroki zamiast max 7 przy liniowym!
Porównanie złożoności
| Algorytm | Najlepszy | Średni | Najgorszy | Pamięć |
|---|---|---|---|---|
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) |
| Szukanie liniowe | O(1) | O(n) | O(n) | O(1) |
| Szukanie binarne | O(1) | O(log n) | O(log n) | O(1) |
📝 Zadanie praktyczne
Wykonaj sortowanie bąbelkowe dla tablicy T = [5, 2, 8, 1, 9]. Pokaż tablicę po każdym przejściu.
T = [5, 2, 8, 1, 9]
Przejście 1: porównujemy sąsiadów, zamieniamy gdy lewy > prawy
[5,2,8,1,9] → [2,5,8,1,9] → [2,5,8,1,9] → [2,5,1,8,9] → [2,5,1,8,9]
Wynik: [2, 5, 1, 8, 9] — 9 "wypłynęło" na koniec
Przejście 2:
[2,5,1,8,9] → [2,5,1,8,9] → [2,1,5,8,9] → bez zmian
Wynik: [2, 1, 5, 8, 9] — 8 na miejscu
Przejście 3:
[2,1,5,8,9] → [1,2,5,8,9] → bez zmian
Wynik: [1, 2, 5, 8, 9] — POSORTOWANE ✓
Porównań: ~n² = 25, ale kończymy wcześniej.
Zadania maturalne — zestaw z 4.10
Siedem zadań. Pierwsze trzy rozpisujesz krok po kroku — na lekcji w notatniku Colab (sortowania.ipynb, sprawdza odpowiedzi automatycznie), w domu na kartce, bo na maturze masz tylko papier: tablica po każdym przebiegu, a pod spodem liczba porównań i zamian. To jest umiejętność, którą CKE sprawdza najczęściej — nie kod, tylko prześledzenie algorytmu.
SORTOWANIA I WYSZUKIWANIE - 7 zadań.
Wszystko robimy w notatniku Colab - link dostaniesz na czacie.
Wpisujesz odpowiedź, naciskasz Ctrl+Enter i notatnik sam mówi,
co się zgadza. Zadanie 4.7 zostaje do domu.
Tablica do zadań 4.1-4.3: 8, 3, 5, 1, 9, 2
4.1 Posortuj ją bąbelkowo. Zapisz tablicę po każdym przebiegu.
Podaj liczbę porównań i liczbę zamian.
4.2 Posortuj ją przez wybór. Zapisz tablicę po każdym kroku.
Podaj liczbę porównań i liczbę zamian. Porównaj liczbę zamian
z zadaniem 4.1 i wyjaśnij różnicę.
4.3 Posortuj ją przez wstawianie. Zapisz tablicę po wstawieniu każdego
kolejnego elementu. Kiedy ten algorytm działa najszybciej?
4.4 Uzupełnij tabelę złożoności: bąbelkowe, przez wybór, przez wstawianie,
szybkie, przez scalanie, wyszukiwanie liniowe, wyszukiwanie binarne.
Dla n = 1000 policz liczbę operacji przy O(n^2) i przy O(n log n).
4.5 Tablica posortowana: 2, 5, 8, 12, 16, 23, 38, 45, 56, 72, 91
Rozpisz wyszukiwanie binarne dla wartości 72 i dla wartości 40
(podaj kolejne wartości lewy, srodek, prawy). Ile kroków w każdym
przypadku? Ile wynosi maksymalna liczba kroków dla tej tablicy?
4.6 Napisz w Pythonie funkcję sortuj_przez_wybor(lista), która sortuje
listę rosnąco i zwraca krotkę: posortowana lista oraz liczba
wykonanych porównań. Ile porównań wykona dla listy 100-elementowej?
4.7 (do domu) Dopisz do sortowania bąbelkowego flagę, która przerywa
pętlę, gdy w całym przebiegu nie było ani jednej zamiany. Jaka jest
złożoność tej wersji dla tablicy już posortowanej?
Zadanie 4.1 — sortowanie bąbelkowe krok po kroku
Posortuj rosnąco tablicę 8, 3, 5, 1, 9, 2 metodą bąbelkową. Zapisz zawartość tablicy po każdym przebiegu oraz łączną liczbę porównań i zamian.
| Przebieg | Tablica po przebiegu | Zamiany w przebiegu |
|---|---|---|
| start | 8, 3, 5, 1, 9, 2 | — |
| 1 | 3, 5, 1, 8, 2, 9 | 4 |
| 2 | 3, 1, 5, 2, 8, 9 | 3 |
| 3 | 1, 3, 2, 5, 8, 9 | 2 |
| 4 | 1, 2, 3, 5, 8, 9 | 1 |
| 5 | 1, 2, 3, 5, 8, 9 | 0 |
Porównań: 15. Zamian: 9.
Liczba porównań nie zależy od danych: to zawsze 5 + 4 + 3 + 2 + 1 = 15, czyli n(n-1)/2 dla sześciu elementów. Zamiany zależą od układu danych. Po każdym przebiegu największy element z nieposortowanej części jest już na swoim miejscu — dlatego kolejny przebieg może być o jedno porównanie krótszy.
Zadanie 4.2 — sortowanie przez wybór
Ta sama tablica 8, 3, 5, 1, 9, 2, metoda przez wybór. Zapisz stan po każdym kroku, podaj liczbę porównań i zamian, a potem wyjaśnij, skąd bierze się różnica w liczbie zamian względem zadania 4.1.
| Krok | Co robimy | Tablica po kroku |
|---|---|---|
| start | — | 8, 3, 5, 1, 9, 2 |
| 1 | minimum z całej tablicy to 1 — zamiana z pozycją 1 | 1, 3, 5, 8, 9, 2 |
| 2 | minimum z reszty to 2 — zamiana z pozycją 2 | 1, 2, 5, 8, 9, 3 |
| 3 | minimum z reszty to 3 — zamiana z pozycją 3 | 1, 2, 3, 8, 9, 5 |
| 4 | minimum z reszty to 5 — zamiana z pozycją 4 | 1, 2, 3, 5, 9, 8 |
| 5 | minimum z reszty to 8 — zamiana z pozycją 5 | 1, 2, 3, 5, 8, 9 |
Porównań: 15. Zamian: 5.
Porównań tyle samo co w bąbelkowym (15), bo i tu przeglądamy wszystkie pary. Zamian jest za to najwyżej n-1, czyli 5: w każdym kroku wykonujemy jedną zamianę, gdy znajdziemy minimum. Bąbelkowe potrzebowało 9, bo przesuwa elementy po jednej pozycji.
Zadanie 4.3 — sortowanie przez wstawianie
Ta sama tablica, metoda przez wstawianie. Zapisz stan po wstawieniu każdego kolejnego elementu. Kiedy ten algorytm jest najszybszy?
| Wstawiany element | Tablica po wstawieniu |
|---|---|
| start | 8, 3, 5, 1, 9, 2 |
| 3 | 3, 8, 5, 1, 9, 2 |
| 5 | 3, 5, 8, 1, 9, 2 |
| 1 | 1, 3, 5, 8, 9, 2 |
| 9 | 1, 3, 5, 8, 9, 2 |
| 2 | 1, 2, 3, 5, 8, 9 |
Porównań: 12. Przesunięć: 9.
Element 9 nie wymagał żadnego przesunięcia — był już większy od poprzednika, więc jedno porównanie zamknęło sprawę. Na tym polega przewaga tej metody: dla danych prawie posortowanych działa w czasie liniowym O(n), bo pętla wewnętrzna kończy się natychmiast. W najgorszym przypadku (dane odwrócone) to nadal O(n²).
Analogia, która zostaje w głowie: tak układasz karty w ręce — bierzesz kolejną i wsuwasz ją między te, które już trzymasz posortowane.
Zadanie 4.4 — złożoność obliczeniowa
Uzupełnij tabelę złożoności dla poznanych algorytmów i policz, ile operacji wykonają dla n = 1000 przy O(n²), a ile przy O(n log n).
| Algorytm | Średnia | Najgorszy | Uwaga |
|---|---|---|---|
| Bąbelkowe | O(n²) | O(n²) | z flagą przerwania: O(n) dla danych posortowanych |
| Przez wybór | O(n²) | O(n²) | zawsze n−1 zamian |
| Przez wstawianie | O(n²) | O(n²) | O(n) dla danych prawie posortowanych |
| Szybkie (quicksort) | O(n log n) | O(n²) | najgorszy przy fatalnym wyborze osi |
| Przez scalanie (mergesort) | O(n log n) | O(n log n) | wymaga dodatkowej pamięci O(n) |
| Wyszukiwanie liniowe | O(n) | O(n) | działa na dowolnych danych |
| Wyszukiwanie binarne | O(log n) | O(log n) | tylko na posortowanych |
Dla n = 1000:
| Klasa | Liczba operacji |
|---|---|
| O(n²) | 1 000 000 |
| O(n log n) | ≈ 9 966 (bo log₂1000 ≈ 9,97) |
| O(n) | 1 000 |
| O(log n) | ≈ 10 |
Różnica między O(n²) a O(n log n) to tutaj stukrotność. Przy n = 50 000 (rozmiar danych z zadań maturalnych na pliku) to już różnica między 2,5 miliarda a 780 tysiącami operacji — czyli między „program się nie doczeka" a „wynik natychmiast".
Zadanie 4.5 — wyszukiwanie binarne na papierze
Tablica posortowana: 2, 5, 8, 12, 16, 23, 38, 45, 56, 72, 91 (indeksy 0–10). Rozpisz wyszukiwanie binarne wartości 72 oraz wartości 40: podaj kolejne trójki lewy / środek / prawy. Ile kroków w każdym przypadku? Ile wynosi maksymalna liczba kroków dla tej tablicy?
Szukamy 72:
| Krok | lewy | środek | prawy | wartość w środku | decyzja |
|---|---|---|---|---|---|
| 1 | 0 | 5 | 10 | 23 | 23 < 72 → idziemy w prawo, lewy = 6 |
| 2 | 6 | 8 | 10 | 56 | 56 < 72 → lewy = 9 |
| 3 | 9 | 9 | 10 | 72 | znaleziono, indeks 9 |
Szukamy 40 (nie ma go w tablicy):
| Krok | lewy | środek | prawy | wartość w środku | decyzja |
|---|---|---|---|---|---|
| 1 | 0 | 5 | 10 | 23 | 23 < 40 → lewy = 6 |
| 2 | 6 | 8 | 10 | 56 | 56 > 40 → prawy = 7 |
| 3 | 6 | 6 | 7 | 38 | 38 < 40 → lewy = 7 |
| 4 | 7 | 7 | 7 | 45 | 45 > 40 → prawy = 6, lewy > prawy → koniec |
72 — 3 kroki. 40 — 4 kroki i wynik „nie ma". Maksymalna liczba kroków dla 11 elementów to 4, bo ⌊log₂11⌋ + 1 = 4. Każdy krok połowi zakres: 11 → 5 → 2 → 1 → 0.
Zadanie 4.6 — implementacja (typ maturalny)
Napisz funkcję sortuj_przez_wybor(lista), która sortuje listę rosnąco i zwraca dwie wartości: posortowaną listę oraz liczbę wykonanych porównań. Ile porównań wykona dla listy stuelementowej?
def sortuj_przez_wybor(lista):
dane = lista[:] # kopia, zeby nie zepsuc oryginalu
porownania = 0
for i in range(len(dane) - 1):
indeks_min = i
for j in range(i + 1, len(dane)):
porownania = porownania + 1
if dane[j] < dane[indeks_min]:
indeks_min = j
if indeks_min != i:
dane[i], dane[indeks_min] = dane[indeks_min], dane[i]
return dane, porownania
wynik, ile = sortuj_przez_wybor([8, 3, 5, 1, 9, 2])
print(wynik) # [1, 2, 3, 5, 8, 9]
print(ile) # 15
Dla listy stuelementowej: 99 + 98 + … + 1 = 100 · 99 / 2 = 4950 porównań. Wzór ogólny to n(n-1)/2 — warto go pamiętać, bo pytanie „ile porównań wykona algorytm dla n elementów" pada regularnie.
lista[:] zamiast lista — bez kopii funkcja modyfikuje listę wywołującego, a zadanie zwykle tego nie chce. I kolejność w return: skoro polecenie mówi „posortowana lista oraz liczba porównań", to dokładnie w tej kolejności — zamiana miejscami to błąd, mimo że kod działa.Zadanie 4.7 — do domu: bąbelkowe z flagą
Dopisz do sortowania bąbelkowego flagę, która przerywa pętlę zewnętrzną, gdy w całym przebiegu nie wykonano żadnej zamiany. Jaka jest złożoność tej wersji dla tablicy już posortowanej?
def sortuj_babelkowo(lista):
dane = lista[:]
n = len(dane)
for i in range(n - 1):
zamiana = False
for j in range(n - 1 - i):
if dane[j] > dane[j + 1]:
dane[j], dane[j + 1] = dane[j + 1], dane[j]
zamiana = True
if not zamiana: # caly przebieg bez zamiany = juz posortowane
break
return dane
Dla tablicy już posortowanej algorytm wykona jeden przebieg (n−1 porównań, zero zamian) i przerwie działanie — złożoność O(n). Dla danych losowych i odwróconych nadal O(n²), więc flaga nie zmienia klasy złożoności w ogólnym przypadku, tylko ratuje przypadek optymistyczny.
To jedyna optymalizacja bąbelkowego, o którą pyta matura. Reszta (sortowanie koktajlowe, zmienne granice) jest poza zakresem.
5. Rekurencja
Rekurencja to technika algorytmiczna, w której funkcja wywołuje samą siebie. Na maturze pojawiają się zadania z analizą funkcji rekurencyjnych.
Budowa funkcji rekurencyjnej
1. Warunek bazowy (stopu) — kiedy przestać
2. Krok rekurencyjny — wywołanie siebie z mniejszym problemem
Silnia — n!
def silnia(n):
if n <= 1: # warunek bazowy
return 1
return n * silnia(n - 1) # krok rekurencyjny
# silnia(5) = 5 * silnia(4) = 5 * 4 * silnia(3) = ... = 5*4*3*2*1 = 120
Drzewo wywołań silnia(4):
silnia(4)
→ 4 * silnia(3)
→ 3 * silnia(2)
→ 2 * silnia(1)
→ return 1
→ return 2*1 = 2
→ return 3*2 = 6
→ return 4*6 = 24
Fibonacci
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
# fib(0)=0, fib(1)=1, fib(2)=1, fib(3)=2, fib(4)=3, fib(5)=5, fib(6)=8
Drzewo wywołań fib(5):
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
f(2) f(1) f(1) f(0) f(1) f(0)
/ \
f(1) f(0)
Uwaga: fib(2) liczone 3 razy! Złożoność O(2ⁿ) — WOLNE!
NWD — algorytm Euklidesa
# Wersja rekurencyjna
def nwd(a, b):
if b == 0:
return a
return nwd(b, a % b)
# nwd(48, 18) → nwd(18, 12) → nwd(12, 6) → nwd(6, 0) → 6
Śledzenie nwd(48, 18):
Krok │ a │ b │ a%b
1 │ 48 │ 18 │ 12
2 │ 18 │ 12 │ 6
3 │ 12 │ 6 │ 0
4 │ 6 │ 0 │ — → return 6
Potęgowanie szybkie
def potega(a, n):
if n == 0:
return 1
if n % 2 == 0: # n parzyste
p = potega(a, n // 2)
return p * p
else: # n nieparzyste
return a * potega(a, n - 1)
# potega(2, 10) — tylko ~4 mnożenia zamiast 10!
Wieże Hanoi
def hanoi(n, zrodlo, cel, pomocniczy):
if n == 1:
print(f"Przenieś dysk 1 z {zrodlo} na {cel}")
return
hanoi(n - 1, zrodlo, pomocniczy, cel)
print(f"Przenieś dysk {n} z {zrodlo} na {cel}")
hanoi(n - 1, pomocniczy, cel, zrodlo)
# hanoi(3, 'A', 'C', 'B') — 7 ruchów (2ⁿ - 1)
Rekurencja vs iteracja
| Cecha | Rekurencja | Iteracja |
|---|---|---|
| Czytelność | Często lepsza | Może być mniej czytelna |
| Pamięć | Stos wywołań (ryzyko przepełnienia) | Stała ilość pamięci |
| Wydajność | Może być wolna (fib!) | Zwykle szybsza |
| Na maturze | Śledzenie, drzewa, NWD | Sortowanie, zliczanie |
📝 Zadanie praktyczne
Prześledź wywołania funkcji fib(5). Ile razy wywoływana jest funkcja? Jaki jest wynik?
fib(n):
jeżeli n ≤ 1 to:
zwróć n
zwróć fib(n-1) + fib(n-2)
fib(5)
├── fib(4)
│ ├── fib(3)
│ │ ├── fib(2) → fib(1)=1 + fib(0)=0 = 1
│ │ └── fib(1) = 1
│ │ → 2
│ └── fib(2) → 1
│ → 3
└── fib(3) → 2
→ 5
Wynik: fib(5) = 5
Liczba wywołań: 15 (eksplozja!)
fib(3) obliczany 2 razy, fib(2) — 3 razy!
Złożoność: O(2ⁿ) — BARDZO wolne!
6. Python — pliki i dane
Na maturze zadanie programistyczne polega zwykle na przetworzeniu pliku tekstowego z danymi. Typowe operacje: wczytaj → przetwórz → oblicz → zapisz wynik.
Czytanie pliku tekstowego
# Podstawowy wzorzec — ZAWSZE z with!
with open("dane.txt", "r", encoding="utf-8") as plik:
for linia in plik:
linia = linia.strip() # usuń \n
print(linia)
# Wczytaj wszystko do listy
with open("dane.txt", "r", encoding="utf-8") as plik:
dane = plik.readlines() # lista stringów z \n
dane = [l.strip() for l in dane] # bez \n
Parsowanie danych — split()
# Plik: "Jan;Kowalski;85;72;93"
with open("uczniowie.txt", "r", encoding="utf-8") as f:
for linia in f:
parts = linia.strip().split(";")
imie = parts[0]
nazwisko = parts[1]
oceny = [int(x) for x in parts[2:]]
srednia = sum(oceny) / len(oceny)
print(f"{imie} {nazwisko}: {srednia:.2f}")
Zapisywanie do pliku
# Zapisz wyniki do nowego pliku
with open("wyniki.txt", "w", encoding="utf-8") as f:
f.write("Raport wyników\n")
f.write("=" * 30 + "\n")
for uczeń, wynik in wyniki:
f.write(f"{uczeń}: {wynik}\n")
# "w" nadpisuje plik, "a" dopisuje na końcu
Typowe operacje na danych
# Wczytaj listę liczb z pliku (jedna na linię)
with open("liczby.txt") as f:
liczby = [int(l.strip()) for l in f if l.strip()]
# Minimum i maksimum
print(f"Min: {min(liczby)}, Max: {max(liczby)}")
# Suma i średnia
print(f"Suma: {sum(liczby)}, Średnia: {sum(liczby)/len(liczby):.2f}")
# Ile parzystych
parzyste = [x for x in liczby if x % 2 == 0]
print(f"Parzyste: {len(parzyste)}")
# Ile w zakresie [10, 50]
w_zakresie = [x for x in liczby if 10 <= x <= 50]
# Sortowanie malejąco
posortowane = sorted(liczby, reverse=True)
# Unikalne wartości
unikalne = sorted(set(liczby))
Przetwarzanie tekstów
# Zliczanie słów
tekst = "ala ma kota a kot ma alę"
slowa = tekst.split()
print(len(slowa)) # 7
# Zliczanie wystąpień litery
print(tekst.count("a")) # 5
# Zamiana liter
print(tekst.upper()) # "ALA MA KOTA..."
print(tekst.title()) # "Ala Ma Kota..."
# Sprawdzanie
"abc".isalpha() # True — tylko litery
"123".isdigit() # True — tylko cyfry
"abc".islower() # True — małe litery
# Zliczanie liter i cyfr w tekście
litery = sum(1 for c in tekst if c.isalpha())
cyfry = sum(1 for c in tekst if c.isdigit())
Wzorzec maturalnego zadania programistycznego
# Typowy szkielet rozwiązania:
# 1. Wczytaj dane
with open("dane.txt", "r", encoding="utf-8") as f:
dane = [linia.strip().split(";") for linia in f if linia.strip()]
# 2. Przetwórz (np. wyciągnij kolumny, konwertuj typy)
rekordy = []
for row in dane:
rekordy.append({
"nazwa": row[0],
"wartosc": int(row[1]),
"kategoria": row[2]
})
# 3. Oblicz (odpowiedzi na podpunkty)
# a) Ile rekordów spełnia warunek?
wynik_a = len([r for r in rekordy if r["wartosc"] > 100])
# b) Który rekord ma max wartość?
wynik_b = max(rekordy, key=lambda r: r["wartosc"])
# c) Suma per kategoria
from collections import Counter
sumy = {}
for r in rekordy:
sumy[r["kategoria"]] = sumy.get(r["kategoria"], 0) + r["wartosc"]
# 4. Zapisz wyniki
with open("wyniki.txt", "w", encoding="utf-8") as f:
f.write(f"a) {wynik_a}\n")
f.write(f"b) {wynik_b['nazwa']}\n")
for kat, s in sorted(sumy.items()):
f.write(f"c) {kat}: {s}\n")
📝 Zadanie praktyczne
Plik oceny.txt ma format: imie;mat;inf;ang. Napisz kod, który znajdzie ucznia z najwyższą średnią.
with open("oceny.txt", "r", encoding="utf-8") as f:
uczniowie = []
for linia in f:
p = linia.strip().split(";")
uczniowie.append({
"imie": p[0],
"srednia": (int(p[1]) + int(p[2]) + int(p[3])) / 3
})
najlepszy = max(uczniowie, key=lambda u: u["srednia"])
print(f"{najlepszy['imie']}: {najlepszy['srednia']:.2f}")
7. Arkusz kalkulacyjny
Na maturze zadanie z arkusza kalkulacyjnego wymaga formuł, funkcji warunkowych, wyszukiwania i wykresu. Dane importujesz z pliku .txt/.csv.
Import danych
Kluczowe funkcje
| Funkcja | Opis | Przykład |
|---|---|---|
| SUMA | Suma zakresu | =SUMA(B2:B100) |
| ŚREDNIA | Średnia arytmetyczna | =ŚREDNIA(C2:C100) |
| MIN / MAX | Minimum / Maksimum | =MAX(D2:D100) |
| ILE.LICZB | Ile komórek z liczbami | =ILE.LICZB(A:A) |
| DŁUGOŚĆ | Ile znaków w tekście | =DŁUGOŚĆ(A2) |
| LEWY / PRAWY | N znaków od lewej/prawej | =LEWY(A2;3) |
| FRAGMENT.TEKSTU | Wycinek tekstu | =FRAGMENT.TEKSTU(A2;2;4) |
Funkcje warunkowe
JEŻELI — warunek z dwoma wynikami:
=JEŻELI(C2>=50;"Zdał";"Nie zdał")
Zagnieżdżone:
=JEŻELI(C2>=90;"Celująca";JEŻELI(C2>=75;"Dobra";JEŻELI(C2>=50;"Dostateczna";"Niedostateczna")))
LICZ.JEŻELI — ile spełnia warunek:
=LICZ.JEŻELI(B2:B100;"elektronika")
=LICZ.JEŻELI(C2:C100;">=50")
SUMA.JEŻELI — suma dla warunku:
=SUMA.JEŻELI(C2:C100;">=50";D2:D100)
LICZ.WARUNKI — wiele warunków:
=LICZ.WARUNKI(B2:B100;"elektronika";C2:C100;">=100")
SUMA.WARUNKÓW:
=SUMA.WARUNKÓW(D2:D100;B2:B100;"odzież";C2:C100;">=50")
WYSZUKAJ.PIONOWO (VLOOKUP)
Składnia:
=WYSZUKAJ.PIONOWO(szukana_wartość; tabela; nr_kolumny; [przybliżone])
Przykład — szukaj oceny dla ucznia "Jan":
=WYSZUKAJ.PIONOWO("Jan";A2:D100;3;0)
↑ ↑ ↑ ↑
szukana wartość tabela kol dokładne (0)
WAŻNE: 0 = dokładne dopasowanie (prawie ZAWSZE chcesz 0!)
1 = przybliżone (tablica MUSI być posortowana)
Adresowanie
| Typ | Zapis | Zachowanie przy kopiowaniu |
|---|---|---|
| Względne | A1 | Zmienia się (przesunięcie) |
| Bezwzględne | $A$1 | NIE zmienia się |
| Mieszane | $A1 lub A$1 | Jedna współrzędna stała |
=B2/$B$101Wykresy
Najczęściej wymagane na maturze:
1. Kolumnowy — porównanie wartości per kategoria
2. Kołowy — udział procentowy każdej kategorii
3. Liniowy — trendy w czasie
Obowiązkowe elementy:
• Tytuł wykresu
• Opisy osi (X i Y)
• Legenda (gdy wiele serii)
• Etykiety danych (opcjonalnie)
📝 Zadanie praktyczne
Dane w kolumnach: A (produkt), B (cena), C (ilość). Napisz formuły dla: sumy wartości, średniej ceny, produktu z najwyższą ceną.
Suma wartości (cena × ilość):
=SUMA.ILOCZYNU(B2:B100;C2:C100)
lub: =SUMA(B2:B100*C2:C100) [Ctrl+Shift+Enter w starszych wersjach]
Średnia cena:
=ŚREDNIA(B2:B100)
Produkt z najwyższą ceną:
=INDEKS(A2:A100;PODAJ.POZYCJĘ(MAX(B2:B100);B2:B100;0))
lub: =WYSZUKAJ.PIONOWO(MAX(B:B);B:A;2;0) [dane posortowane]
8. Bazy danych i SQL
Na maturze zadanie bazodanowe wymaga napisania zapytań SQL. Od 2023 roku SQL trzeba też napisać na papierze (Część 1). W Części 2 dostajesz gotową bazę w Accessie lub LibreOffice Base — składnia SELECT jest w nich ta sama, więc ćwiczyć możesz w dowolnym silniku (niżej: MySQL przez phpMyAdmin albo SQLite).
Podstawowe zapytanie SELECT
-- Składnia
SELECT kolumna1, kolumna2
FROM tabela
WHERE warunek
ORDER BY kolumna ASC|DESC;
-- Przykład
SELECT imie, nazwisko, ocena
FROM uczniowie
WHERE ocena >= 4
ORDER BY ocena DESC;
Operatory w WHERE
| Operator | Przykład |
|---|---|
=, !=, <, >, <=, >= | WHERE cena > 100 |
AND, OR, NOT | WHERE cena > 100 AND kategoria = 'odzież' |
BETWEEN | WHERE wiek BETWEEN 18 AND 25 |
IN | WHERE miasto IN ('Kraków', 'Warszawa') |
LIKE | WHERE nazwisko LIKE 'Kow%' |
IS NULL | WHERE email IS NULL |
LIKE — wzorce
% — dowolny ciąg znaków (0 lub więcej)
_ — dokładnie jeden znak
WHERE imie LIKE 'A%' -- zaczyna się na A
WHERE imie LIKE '%ski' -- kończy się na "ski"
WHERE imie LIKE '_a%' -- druga litera to "a"
WHERE kod LIKE '3_-___' -- wzorzec: 3X-XXX
Funkcje agregujące
COUNT(*) — ilość wierszy
COUNT(kolumna) — ilość niepustych wartości
SUM(kolumna) — suma
AVG(kolumna) — średnia
MIN(kolumna) — minimum
MAX(kolumna) — maksimum
-- Przykład
SELECT COUNT(*) AS ile_produktow,
AVG(cena) AS srednia_cena,
MAX(cena) AS najdrozsza
FROM produkty;
GROUP BY + HAVING
-- Ile produktów per kategoria
SELECT kategoria, COUNT(*) AS ile
FROM produkty
GROUP BY kategoria;
-- Tylko kategorie z > 5 produktami
SELECT kategoria, COUNT(*) AS ile
FROM produkty
GROUP BY kategoria
HAVING COUNT(*) > 5;
-- Kolejność klauzul: SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
WHERE filtruje PRZED grupowaniem (wiersze)
HAVING filtruje PO grupowaniu (grupy)
JOIN — łączenie tabel
-- INNER JOIN — tylko pasujące wiersze z obu tabel
SELECT u.imie, u.nazwisko, o.przedmiot, o.ocena
FROM uczniowie u
INNER JOIN oceny o ON u.id = o.uczen_id;
-- LEFT JOIN — wszyscy z lewej + pasujący z prawej (NULL jeśli brak)
SELECT u.imie, COUNT(o.id) AS ile_ocen
FROM uczniowie u
LEFT JOIN oceny o ON u.id = o.uczen_id
GROUP BY u.imie;
Podzapytania
-- Uczniowie z oceną wyższą od średniej
SELECT imie, ocena
FROM uczniowie
WHERE ocena > (SELECT AVG(ocena) FROM uczniowie);
-- Produkty z kategorii z najwyższą średnią ceną
SELECT *
FROM produkty
WHERE kategoria = (
SELECT kategoria
FROM produkty
GROUP BY kategoria
ORDER BY AVG(cena) DESC
LIMIT 1
);
Modyfikacja danych
-- INSERT
INSERT INTO uczniowie (imie, nazwisko, klasa)
VALUES ('Jan', 'Kowalski', '4TI');
-- UPDATE
UPDATE produkty
SET cena = cena * 1.1
WHERE kategoria = 'elektronika';
-- DELETE
DELETE FROM uczniowie
WHERE klasa = '4TI' AND ocena < 2;
-- CREATE TABLE
CREATE TABLE zamowienia (
id INTEGER PRIMARY KEY AUTOINCREMENT,
produkt_id INTEGER,
ilosc INTEGER DEFAULT 1,
data TEXT,
FOREIGN KEY (produkt_id) REFERENCES produkty(id)
);
CREATE VIEW
-- Widok (wirtualna tabela)
CREATE VIEW raport_kategorie AS
SELECT kategoria,
COUNT(*) AS ile,
AVG(cena) AS srednia_cena,
SUM(cena * ilosc) AS wartosc
FROM produkty
GROUP BY kategoria;
-- Korzystanie z widoku
SELECT * FROM raport_kategorie WHERE wartosc > 10000;
📝 Zadanie praktyczne
Masz tabelę zamowienia (klient, produkt, cena, data). Napisz zapytanie: ile zamówień złożył każdy klient i ile wydał łącznie, tylko klienci z >3 zamówieniami.
SELECT
klient,
COUNT(*) AS ile_zamowien,
SUM(cena) AS suma_wydatkow
FROM zamowienia
GROUP BY klient
HAVING COUNT(*) > 3
ORDER BY suma_wydatkow DESC;
Baza ćwiczeniowa „przychodnia" — 12 zapytań (27.09)
Do tej pory SQL pisaliśmy na danych z arkusza CKE. Teraz pracujesz na własnej bazie, którą sam zaimportujesz — bo na egzaminie też dostajesz gotowy plik i musisz się w nim odnaleźć w pierwszych dwóch minutach.
Arkusz dla ucznia zawiera instrukcję importu i treści zadań bez rozwiązań.
BAZA "przychodnia" - 12 zapytań. Piszesz w phpMyAdminie przy udostępnionym
ekranie. Po każdym zadaniu wklej na czat swoje zapytanie i liczbę wierszy.
Tabele:
lekarze(id, nazwisko, specjalizacja, stawka_za_wizyte)
pacjenci(id, nazwisko, miasto, rok_urodzenia)
wizyty(id, pacjent_id, lekarz_id, data, status, oplata)
data w formacie RRRR-MM-DD, status: odbyta / odwolana / nieobecnosc
RUNDA A
8A.1 Nazwiska i rok urodzenia pacjentów z Kielc, alfabetycznie.
8A.2 Ile wizyt zaplanowano na kwiecień 2026? Napisz to na dwa sposoby:
z LIKE i z BETWEEN.
8A.3 Z ilu różnych miast pochodzą pacjenci?
8A.4 Średnia opłata za wizytę, która się odbyła, zaokrąglona do dwóch
miejsc po przecinku.
RUNDA B
8B.5 Liczba wizyt w podziale na status, od najliczniejszego.
8B.6 Ilu pacjentów z każdego miasta? Tylko miasta z co najmniej trzema.
8B.7 Kto u kogo był w pierwszej dekadzie maja 2026: nazwisko pacjenta,
nazwisko lekarza, data i status, chronologicznie.
8B.8 Który lekarz przyniósł największy przychód? Nazwisko i kwota,
jednym zapytaniem, bez przeglądania listy oczami.
RUNDA C
8C.9 Pacjenci bez ani jednej wizyty - dwoma sposobami: podzapytaniem
i LEFT JOIN-em.
8C.10 Dla każdego miesiąca: liczba wizyt i suma opłat, chronologicznie.
8C.11 Pacjenci, którzy zapłacili łącznie więcej niż 1000 zł, malejąco.
8C.12 Dla każdego lekarza: liczba wszystkich wizyt, liczba odwołanych
i procent odwołań (jedno miejsce po przecinku), od najgorszego.
DO DOMU
8D.1 Które specjalizacje miały więcej niż 20 wizyt? Malejąco.
8D.2 Dla każdej pary (lekarz, miesiąc) liczba wizyt odbytych i przychód.
Tylko pary z przychodem co najmniej 600 zł.
Krok 1 — import bazy
Pobierz plik przychodnia.sql (3 tabele, 8 lekarzy, 30 pacjentów, 120 wizyt). Wybierz jedno środowisko:
| Środowisko | Co zrobić |
|---|---|
| phpMyAdmin (XAMPP — masz go z lekcji Reacta) | Uruchom XAMPP, wystartuj Apache i MySQL. Wejdź na localhost/phpmyadmin → Nowa → nazwa bazy przychodnia → Utwórz. Potem zakładka Import → wybierz plik → Importuj. |
| DB Browser for SQLite (zero konfiguracji) | Nowa baza → zapisz jako przychodnia.db → zakładka Wykonaj SQL → wklej zawartość pliku → Ctrl+Enter. |
AUTO_INCREMENT, bez ENGINE=InnoDB, daty jako tekst RRRR-MM-DD. Wszystkie zapytania z tego zestawu działają identycznie w MySQL i SQLite. Jedyna widoczna różnica: MySQL wypisze sumę jako 2880.00, SQLite jako 2880 — to ta sama liczba, nie błąd.Krok 2 — rozpoznanie schematu (rob to zawsze przed pisaniem zapytań)
lekarze pacjenci wizyty
------- -------- ------
id PK id PK id PK
nazwisko nazwisko pacjent_id -> pacjenci.id
specjalizacja miasto lekarz_id -> lekarze.id
stawka_za_wizyte rok_urodzenia data 'RRRR-MM-DD'
status odbyta / odwolana / nieobecnosc
oplata 0.00 gdy wizyta sie nie odbyla
wizyty to tabela łącząca — relacja pacjenci ↔ lekarze jest typu N:M i realizuje się przez dwa klucze obce. Każde zapytanie, które ma pokazać nazwisko pacjenta i nazwisko lekarza, wymaga dwóch JOIN-ów.
Zapytanie na rozgrzewkę, żeby sprawdzić, czy import się udał:
SELECT COUNT(*) FROM lekarze; -- 8
SELECT COUNT(*) FROM pacjenci; -- 30
SELECT COUNT(*) FROM wizyty; -- 120
Runda A — fundament (SELECT, WHERE, agregaty)
Zadanie 8A.1
Wypisz nazwiska i rok urodzenia pacjentów z Kielc, alfabetycznie po nazwisku.
SELECT nazwisko, rok_urodzenia
FROM pacjenci
WHERE miasto = 'Kielce'
ORDER BY nazwisko;
8 wierszy: Antczak 1997, Bielski 1967, Chmiel 1993, Duda 1979, Ejsmond 2001, Frankowska 1962, Gorny 2001, Herman 1962.
Zadanie 8A.2
Ile wizyt zaplanowano na kwiecień 2026? Napisz zapytanie w dwóch wariantach: z LIKE i z BETWEEN.
SELECT COUNT(*) AS ile FROM wizyty
WHERE data LIKE '2026-04%';
SELECT COUNT(*) AS ile FROM wizyty
WHERE data BETWEEN '2026-04-01' AND '2026-04-30';
Wynik: 15. Oba warianty działają, bo data trzymana jest w formacie RRRR-MM-DD — porządek alfabetyczny tekstu pokrywa się z porządkiem chronologicznym. To jedyny powód, dla którego ten format jest standardem; przy zapisie DD.MM.RRRR żaden z tych warunków nie zadziałałby.
Zadanie 8A.3
Z ilu różnych miast pochodzą pacjenci przychodni?
SELECT COUNT(DISTINCT miasto) AS ile_miast
FROM pacjenci;
Wynik: 8. Bez DISTINCT dostałbyś 30 — liczbę pacjentów, nie miast. To klasyczna pułapka: pytanie „z ilu różnych" zawsze oznacza COUNT(DISTINCT …).
Zadanie 8A.4
Jaka jest średnia opłata za wizytę, która się odbyła? Zaokrąglij do dwóch miejsc po przecinku.
SELECT ROUND(AVG(oplata), 2) AS srednia
FROM wizyty
WHERE status = 'odbyta';
Wynik: 167,49. Warunek WHERE status = 'odbyta' jest tu istotą zadania — bez niego średnia policzy się też po wizytach odwołanych z opłatą 0,00 i wyjdzie 133,99, czyli odpowiedź na inne pytanie.
Runda B — grupowanie i łączenie tabel
Zadanie 8B.5
Podaj liczbę wizyt w podziale na status, od najliczniejszego.
SELECT status, COUNT(*) AS ile
FROM wizyty
GROUP BY status
ORDER BY ile DESC;
odbyta 96, nieobecnosc 13, odwolana 11. Suma 120 — zawsze sprawdzaj, czy grupy sumują się do całości tabeli. Jeśli nie, gdzieś masz WHERE, którego nie zauważyłeś, albo wartość NULL w kolumnie grupującej.
Zadanie 8B.6
Ilu pacjentów pochodzi z każdego miasta? Pokaż tylko miasta z co najmniej trzema pacjentami, malejąco.
SELECT miasto, COUNT(*) AS ilu
FROM pacjenci
GROUP BY miasto
HAVING COUNT(*) >= 3
ORDER BY ilu DESC, miasto;
Kielce 8, Radom 5, Ostrowiec Swietokrzyski 4, Busko-Zdroj 3, Jedrzejow 3, Skarzysko-Kamienna 3. Warunek na liczbę pacjentów musi stać w HAVING, bo dotyczy wyniku agregacji. W WHERE nie masz jeszcze policzonego COUNT(*) — baza zgłosi błąd.
Zadanie 8B.7
Wypisz, kto u kogo był w pierwszej dekadzie maja 2026: nazwisko pacjenta, nazwisko lekarza, data i status. Chronologicznie.
SELECT p.nazwisko AS pacjent,
l.nazwisko AS lekarz,
w.data,
w.status
FROM wizyty w
JOIN pacjenci p ON p.id = w.pacjent_id
JOIN lekarze l ON l.id = w.lekarz_id
WHERE w.data BETWEEN '2026-05-01' AND '2026-05-10'
ORDER BY w.data;
| pacjent | lekarz | data | status |
|---|---|---|---|
| Lewandowska | Adamczyk | 2026-05-04 | nieobecnosc |
| Chmiel | Fabisiak | 2026-05-04 | odwolana |
| Zajac | Adamczyk | 2026-05-05 | odbyta |
| Dziedzic | Cichocka | 2026-05-06 | odbyta |
| Ejsmond | Gawlik | 2026-05-08 | odbyta |
| Sadowska | Cichocka | 2026-05-08 | odbyta |
Aliasy p, l, w nie są ozdobą — obie tabele mają kolumnę nazwisko i bez prefiksu baza nie wie, którą masz na myśli. Warunek ON zawsze łączy klucz obcy z kluczem głównym.
Zadanie 8B.8
Który lekarz przyniósł przychodni największy przychód? Podaj nazwisko i kwotę, jednym zapytaniem, bez przeglądania listy oczami.
SELECT l.nazwisko, SUM(w.oplata) AS przychod
FROM lekarze l
JOIN wizyty w ON w.lekarz_id = l.id
GROUP BY l.nazwisko
ORDER BY przychod DESC
LIMIT 1;
Adamczyk, 2880,00. Kolejni: Hajduk 2555, Fabisiak 2156. Wzorzec „top 1" to zawsze GROUP BY → ORDER BY … DESC → LIMIT 1 i pojawia się w prawie każdym arkuszu. Uwaga na remisy — gdy dwie grupy mają tę samą sumę, LIMIT 1 uciętnie jedną z nich; jeśli zadanie mówi „podaj wszystkich", użyj podzapytania z MAX.
Runda C — poziom maturalny
Zadanie 8C.9
Wypisz pacjentów, którzy nie mają ani jednej wizyty. Zrób to dwoma sposobami: podzapytaniem i LEFT JOIN-em.
-- sposob 1: podzapytanie
SELECT id, nazwisko, miasto
FROM pacjenci
WHERE id NOT IN (SELECT pacjent_id FROM wizyty)
ORDER BY nazwisko;
-- sposob 2: LEFT JOIN + brak dopasowania
SELECT p.id, p.nazwisko, p.miasto
FROM pacjenci p
LEFT JOIN wizyty w ON w.pacjent_id = p.id
WHERE w.id IS NULL
ORDER BY p.nazwisko;
7 pacjentów: Bielski (Kielce), Gorny (Kielce), Gruszka (Jedrzejow), Michalik (Konskie), Nowicka (Skarzysko-Kamienna), Ostrowski (Konskie), Wilk (Busko-Zdroj).
Sens LEFT JOIN: bierze wszystkich z lewej tabeli, a tam gdzie nie ma pary po prawej, wstawia NULL — więc warunek w.id IS NULL zostawia dokładnie tych bez wizyt. Zwykły JOIN takich wierszy nigdy nie pokaże, bo wyrzuca wszystko bez dopasowania.
Zadanie 8C.10
Podaj dla każdego miesiąca liczbę wizyt i sumę opłat, chronologicznie.
SELECT SUBSTR(data, 1, 7) AS miesiac,
COUNT(*) AS ile,
SUM(oplata) AS suma
FROM wizyty
GROUP BY miesiac
ORDER BY miesiac;
| miesiac | ile | suma |
|---|---|---|
| 2026-01 | 19 | 2598,00 |
| 2026-02 | 18 | 2413,00 |
| 2026-03 | 23 | 3209,00 |
| 2026-04 | 15 | 1948,00 |
| 2026-05 | 19 | 2567,00 |
| 2026-06 | 26 | 3344,00 |
SUBSTR(data, 1, 7) wycina RRRR-MM — ten sam trik padł w arkuszu maj 2026 w zadaniu 8. Działa w MySQL i SQLite bez zmian i nie wymaga znajomości funkcji dat, które w każdym silniku nazywają się inaczej. Grupowanie po aliasie (GROUP BY miesiac) też działa w obu.
Zadanie 8C.11
Którzy pacjenci zapłacili w sumie więcej niż 1000 zł? Podaj nazwisko i kwotę, malejąco.
SELECT p.nazwisko, SUM(w.oplata) AS wydatki
FROM pacjenci p
JOIN wizyty w ON w.pacjent_id = p.id
GROUP BY p.id, p.nazwisko
HAVING SUM(w.oplata) > 1000
ORDER BY wydatki DESC;
Maj 1743, Zajac 1395, Lisowski 1073, Dziedzic 1003.
Grupujemy po p.id, a nie tylko po nazwisku — gdyby dwóch pacjentów miało to samo nazwisko, grupowanie po nazwisku zsumowałoby ich razem i wynik byłby błędny. Nawyk: grupuj po kluczu głównym, nazwisko dorzuć tylko dla czytelności.
Zadanie 8C.12 — zadanie sumujące
Dla każdego lekarza podaj: liczbę wszystkich wizyt, liczbę wizyt odwołanych i procent odwołań (jedno miejsce po przecinku), od najgorszego wskaźnika.
SELECT l.nazwisko,
COUNT(*) AS wszystkie,
SUM(CASE WHEN w.status = 'odwolana' THEN 1 ELSE 0 END) AS odwolane,
ROUND(100.0 * SUM(CASE WHEN w.status = 'odwolana' THEN 1 ELSE 0 END) / COUNT(*), 1) AS procent
FROM lekarze l
JOIN wizyty w ON w.lekarz_id = l.id
GROUP BY l.nazwisko
ORDER BY procent DESC;
| lekarz | wszystkie | odwołane | procent |
|---|---|---|---|
| Hajduk | 20 | 3 | 15,0 |
| Gawlik | 15 | 2 | 13,3 |
| Fabisiak | 15 | 2 | 13,3 |
| Borowiec | 10 | 1 | 10,0 |
| Cichocka | 14 | 1 | 7,1 |
| Dudek | 16 | 1 | 6,3 |
| Adamczyk | 19 | 1 | 5,3 |
| Ejsmont | 11 | 0 | 0,0 |
Dwie rzeczy, które decydują o punktach. Po pierwsze SUM(CASE WHEN … THEN 1 ELSE 0 END) — to sposób na policzenie wiersza spełniającego warunek wewnątrz grupy, bez wyrzucania pozostałych (WHERE status='odwolana' zniszczyłoby kolumnę „wszystkie"). Po drugie 100.0 zamiast 100 — przy dzieleniu dwóch liczb całkowitych MySQL i SQLite obcinają część dziesiętną i każdy procent wyszedłby 0.
Do domu (te same tabele)
Zadanie domowe 8D.1
Które specjalizacje miały więcej niż 20 wizyt? Podaj specjalizację i liczbę wizyt, malejąco.
SELECT l.specjalizacja, COUNT(*) AS ile
FROM lekarze l
JOIN wizyty w ON w.lekarz_id = l.id
GROUP BY l.specjalizacja
HAVING COUNT(*) > 20
ORDER BY ile DESC;
ortopeda 35, dermatolog 30, kardiolog 29. Grupujemy po kolumnie z tabeli lekarze, a liczymy wiersze z tabeli wizyty — to typowa konstrukcja, w której kolumna grupująca i kolumna liczona pochodzą z różnych tabel.
Zadanie domowe 8D.2
Dla każdej pary (lekarz, miesiąc) policz wizyty odbyte i ich przychód. Pokaż tylko te pary, w których przychód wyniósł co najmniej 600 zł.
SELECT l.nazwisko,
SUBSTR(w.data, 1, 7) AS miesiac,
COUNT(*) AS odbyte,
SUM(w.oplata) AS przychod
FROM lekarze l
JOIN wizyty w ON w.lekarz_id = l.id
WHERE w.status = 'odbyta'
GROUP BY l.nazwisko, miesiac
HAVING SUM(w.oplata) >= 600
ORDER BY przychod DESC;
Adamczyk 2026-06 (7 wizyt, 1188), Cichocka 2026-02 (5, 750), Gawlik 2026-03 (4, 684), Dudek 2026-03 (5, 672).
Grupowanie po dwóch kolumnach naraz plus filtr WHERE przed grupowaniem i HAVING po nim — dokładnie ta konstrukcja była najtrudniejszym podpunktem zadania 8 w arkuszu maj 2026.
9. Sieci komputerowe
Na maturze pytania o sieci dotyczą adresacji IP, masek podsieci, obliczania adresów sieci i rozgłoszeniowych. Czasem pojawiają się pytania o protokoły.
Adres IP (IPv4)
Przykład:
192.168.1.100 = 11000000.10101000.00000001.01100100Klasy adresów IP
| Klasa | Zakres | Maska domyślna | Przeznaczenie |
|---|---|---|---|
| A | 1.0.0.0 – 126.x.x.x | 255.0.0.0 (/8) | Duże sieci |
| B | 128.0.0.0 – 191.255.x.x | 255.255.0.0 (/16) | Średnie sieci |
| C | 192.0.0.0 – 223.255.255.x | 255.255.255.0 (/24) | Małe sieci |
Adresy prywatne (RFC 1918)
10.0.0.0 – 10.255.255.255 (klasa A — 1 sieć)
172.16.0.0 – 172.31.255.255 (klasa B — 16 sieci)
192.168.0.0 – 192.168.255.255 (klasa C — 256 sieci)
Adres 127.0.0.1 = localhost (pętla zwrotna)
Maska podsieci
Maska określa: które bity = sieć, które = host.
255.255.255.0 = /24
11111111.11111111.11111111.00000000
└────── sieć (24 bity) ───┘└ hosty ┘
Adres sieci: IP AND maska (bitowy AND)
Adres broadcast: IP OR NOT(maska)
Przykład: IP = 192.168.1.100, maska = /24
Adres sieci: 192.168.1.0
Adres broadcast: 192.168.1.255
Hosty: 192.168.1.1 – 192.168.1.254 (254 hosty)
Obliczanie — subnetting (maska /26)
IP: 192.168.1.130, maska: /26 (255.255.255.192)
Krok 1: /26 → 26 bitów sieci, 6 bitów hosta
Krok 2: 2⁶ = 64 adresów na podsieć
Krok 3: Podsieci: .0, .64, .128, .192
Krok 4: 130 należy do podsieci .128
Adres sieci: 192.168.1.128
Pierwszy host: 192.168.1.129
Ostatni host: 192.168.1.190
Broadcast: 192.168.1.191
Ile hostów: 2⁶ - 2 = 62
Tabela masek
| CIDR | Maska | Hostów | Adresów |
|---|---|---|---|
| /24 | 255.255.255.0 | 254 | 256 |
| /25 | 255.255.255.128 | 126 | 128 |
| /26 | 255.255.255.192 | 62 | 64 |
| /27 | 255.255.255.224 | 30 | 32 |
| /28 | 255.255.255.240 | 14 | 16 |
| /30 | 255.255.255.252 | 2 | 4 |
Protokoły
| Protokół | Port | Opis |
|---|---|---|
| HTTP | 80 | Strony WWW (nieszyfrowane) |
| HTTPS | 443 | Strony WWW (szyfrowane TLS) |
| FTP | 21 | Transfer plików |
| SSH | 22 | Zdalny terminal (szyfrowany) |
| SMTP | 25 | Wysyłanie e-maili |
| DNS | 53 | Tłumaczenie domen na IP |
| DHCP | 67/68 | Automatyczne przydzielanie IP |
Model OSI / TCP-IP
TCP/IP (4 warstwy):
4. Aplikacji — HTTP, FTP, DNS, SMTP
3. Transportowa — TCP (niezawodny), UDP (szybki)
2. Internetowa — IP, ICMP, ARP
1. Dostępu — Ethernet, Wi-Fi, kable
TCP vs UDP:
TCP — potwierdza odbiór, gwarantuje kolejność (WWW, e-mail)
UDP — bez potwierdzeń, szybszy (streaming, gry, VoIP)
📝 Zadanie praktyczne
Komputer ma IP 192.168.10.45 i maskę /26. Oblicz adres sieci i adres rozgłoszeniowy.
/26 = 255.255.255.192
192 = 11000000 → ostatni oktet ma 64 adresy (2^6)
192.168.10.45:
- 45 / 64 = 0 (zaokrąglenie w dół) → sieć zaczyna od .0
- 45 mieści się w zakresie 0-63
Adres sieci: 192.168.10.0
Broadcast: 192.168.10.63 (0 + 64 - 1)
Zakres hostów: 192.168.10.1 - 192.168.10.62
Sprawdzenie: 62 hosty = 2^6 - 2 = 64 - 2 ✓
Zadania maturalne — zestaw z 27.09
Arkusz zawiera treści zadań bez rozwiązań — możesz wysłać go uczniowi przed sesją.
Pięć zadań adresacyjnych. Pierwsze to fakt do wykucia — padł w arkuszu maj 2026 i kosztował punkt każdego, kto go nie pamiętał.
SIECI - 5 zadań. Odpowiedzi krótkie, na czacie.
9.1 Ile bitów ma adres IPv4, a ile IPv6? Podaj też rozmiar w bajtach.
9.2 Podaj maskę dziesiętną dla prefiksu /26 oraz liczbę adresów hostów
w takiej podsieci.
9.3 Host ma adres 192.168.10.137/26. Podaj adres sieci, adres
rozgłoszeniowy i zakres adresów dla hostów.
9.4 Podaj maskę dziesiętną dla /22, liczbę wszystkich adresów w podsieci
i liczbę adresów dla hostów.
9.5 Przypisz porty do protokołów: HTTP, HTTPS, FTP, DNS, SSH. Przy każdym
dopisz TCP czy UDP i czym te dwa protokoły się różnią.
Zadanie 9.1 — długość adresu IP
Ile bitów ma adres IPv4, a ile IPv6? Podaj też rozmiar w bajtach i liczbę możliwych adresów.
| Wersja | Bity | Bajty | Liczba adresów | Zapis |
|---|---|---|---|---|
| IPv4 | 32 | 4 | 232 ≈ 4,3 mld | 192.168.1.1 — cztery liczby 0–255 |
| IPv6 | 128 | 16 | 2128 | 2001:0db8::1 — osiem grup po 4 cyfry hex |
Powód powstania IPv6: wyczerpanie puli IPv4. Pytanie pojawia się w formie „ile bitów", „ile bajtów" albo „ile razy więcej adresów" — odpowiedź na ostatnie brzmi 296 razy więcej.
Zadanie 9.2 — maska i liczba hostów
Podaj maskę dziesiętną dla prefiksu /26 oraz liczbę adresów hostów w takiej podsieci.
/26 = 26 jedynek, potem zera:
11111111.11111111.11111111.11000000
255 . 255 . 255 . 192
bity hosta: 32 - 26 = 6
adresow: 2^6 = 64
hostow: 64 - 2 = 62 (minus adres sieci i rozgloszeniowy)
Maska 255.255.255.192, 62 hosty. Ostatni oktet maski to suma wag zapalonych bitów: 128 + 64 = 192. Warto znać na pamięć: /24 → 255.255.255.0, /25 → .128, /26 → .192, /27 → .224, /28 → .240, /29 → .248, /30 → .252.
Zadanie 9.3 — adres sieci i rozgłoszeniowy
Host ma adres 192.168.10.137/26. Podaj adres sieci, adres rozgłoszeniowy i zakres adresów przydzielanych hostom.
/26 dzieli ostatni oktet na bloki po 64:
0-63 | 64-127 | 128-191 | 192-255
137 wpada do bloku 128-191
adres sieci: 192.168.10.128
pierwszy host: 192.168.10.129
ostatni host: 192.168.10.190
adres rozgloszeniowy: 192.168.10.191
Metoda blokowa jest szybsza od wyliczania iloczynu bitowego: rozmiar bloku = 256 − 192 = 64, więc granice wypadają na wielokrotnościach 64. Sprawdzenie: 137 mieści się między 128 a 191, czyli sieć to 128.
Zadanie 9.4 — prefiks spoza ostatniego oktetu
Podaj maskę dziesiętną dla /22, liczbę wszystkich adresów w podsieci i liczbę adresów dla hostów.
/22 -> 22 = 8 + 8 + 6 jedynek
11111111.11111111.11111100.00000000
255 . 255 . 252 . 0
bity hosta: 32 - 22 = 10
adresow: 2^10 = 1024
hostow: 1022
Maska 255.255.252.0, 1024 adresy, 1022 hosty. Prefiks krótszy niż 24 działa na trzecim oktecie — bloki mają rozmiar 4 (256 − 252), więc sieci to 0, 4, 8, 12… w trzecim oktecie.
Zadanie 9.5 — protokoły i porty
Przypisz porty do protokołów: HTTP, HTTPS, FTP, DNS, SSH. Do każdego dopisz, czy działa na TCP czy UDP, i czym te dwa protokoły warstwy transportowej się różnią.
| Protokół | Port | Transport | Do czego |
|---|---|---|---|
| HTTP | 80 | TCP | strony WWW bez szyfrowania |
| HTTPS | 443 | TCP | strony WWW szyfrowane (TLS) |
| FTP | 21 sterowanie, 20 dane | TCP | przesyłanie plików |
| DNS | 53 | UDP przy zapytaniach, TCP przy transferze strefy | zamiana nazwy domeny na adres IP |
| SSH | 22 | TCP | zdalny terminal, szyfrowany |
TCP — połączeniowy, potwierdza odbiór, retransmituje utracone pakiety, gwarantuje kolejność. UDP — bezpołączeniowy, bez potwierdzeń, lżejszy i szybszy; stosowany tam, gdzie opóźnienie boli bardziej niż utrata jednego pakietu: DNS, gry sieciowe, transmisja głosu.
10. Teoria informacji
Na maturze pojawiają się zadania z kodowania znaków (ASCII, UTF-8), miar ilości informacji i kompresji danych.
Jednostki informacji
| Jednostka | Wartość |
|---|---|
| 1 bit (b) | 0 lub 1 |
| 1 bajt (B) | 8 bitów |
| 1 KB (kilobajt) | 1024 B = 2¹⁰ B |
| 1 MB (megabajt) | 1024 KB = 2²⁰ B |
| 1 GB (gigabajt) | 1024 MB = 2³⁰ B |
| 1 TB (terabajt) | 1024 GB = 2⁴⁰ B |
Ilość informacji
Przykłady:
• 2 stany → 1 bit
• 8 stanów → 3 bity
• 256 stanów → 8 bitów = 1 bajt
• 1000 stanów → 10 bitów (2¹⁰ = 1024 ≥ 1000)
Kodowanie znaków
| Kodowanie | Bajty/znak | Zakres |
|---|---|---|
| ASCII | 1 (7 bitów) | 128 znaków (0-127): litery ang., cyfry, znaki specjalne |
| ASCII rozszerzony | 1 (8 bitów) | 256 znaków — dodaje znaki narodowe (np. Windows-1250) |
| UTF-8 | 1-4 | Cały Unicode; ASCII-compatible (znaki ang. = 1 bajt) |
| UTF-16 | 2-4 | Cały Unicode; minimum 2 bajty na znak |
Kody ASCII (musisz pamiętać!):
'0' = 48 'A' = 65 'a' = 97
'1' = 49 'B' = 66 'b' = 98
... ... ...
'9' = 57 'Z' = 90 'z' = 122
Różnice: 'a' - 'A' = 32, '1' - '0' = 1 (konwersja cyfra→liczba)
Kompresja
Kompresja bezstratna — odtworzysz 100% oryginału:
• ZIP, RAR, 7z (pliki)
• PNG (obrazy)
• FLAC (dźwięk)
Metody: kodowanie Huffmana, RLE, LZW
Kompresja stratna — traci część jakości:
• JPEG (obrazy)
• MP3 (dźwięk)
• MP4/H.264 (wideo)
Stopień kompresji = rozmiar_oryginału / rozmiar_skompresowany
Np. 10 MB → 2 MB = stopień 5:1 (80% redukcji)
Kodowanie Huffmana
Idea: Częste znaki → krótkie kody, rzadkie → dłuższe
Tekst: "AABBBCCCC"
Częstość: C=4, B=3, A=2
Drzewo Huffmana:
(9)
/ \
(5) C:4 → kod: 1
/ \
A:2 B:3
kod:00 kod:01
Zakodowany: 00 00 01 01 01 1 1 1 1
Oryginalnie: 9 znaków × 8 bitów = 72 bity
Huffman: 2+2+2+2+2+1+1+1+1 = 14 bitów → 80% mniej!
Grafika — rozdzielczość i głębia koloru
Rozmiar obrazu = szerokość × wysokość × głębia_koloru / 8
Przykład: Full HD, 24-bit kolor:
1920 × 1080 × 24 / 8 = 6 220 800 B ≈ 5.93 MB
Głębia koloru:
1 bit → 2 kolory (czarno-biały)
8 bit → 256 kolorów (odcienie szarości)
24 bit → 16 777 216 kolorów (True Color: 8R + 8G + 8B)
32 bit → 24 bit + 8 bit kanał alfa (przezroczystość)
Dźwięk — próbkowanie
Rozmiar = częstotliwość × głębia × kanały × czas_s / 8
Przykład: Jakość CD, 10 sekund:
44100 Hz × 16 bit × 2 kanały × 10 s / 8 = 1 764 000 B ≈ 1.68 MB
Częstotliwość próbkowania: ile próbek/s (CD = 44100 Hz)
Głębia bitowa: precyzja każdej próbki (CD = 16 bit)
Kanały: mono (1), stereo (2)
📝 Zadanie praktyczne
Film trwa 90 minut, ma 30 klatek/s, rozdzielczość 1280×720, głębia 24 bit. Oblicz rozmiar nieskompresowanego filmu.
Dane:
- Czas: 90 min = 5400 s
- FPS: 30 klatek/s
- Rozdzielczość: 1280 × 720 pikseli
- Głębia: 24 bit na piksel
Obliczenia:
1. Liczba klatek: 5400 s × 30 = 162 000 klatek
2. Rozmiar klatki: 1280 × 720 × 24 / 8 = 2 764 800 B ≈ 2.64 MB
3. Rozmiar filmu: 162 000 × 2 764 800 = 447 897 600 000 B
Przeliczenie:
447 897 600 000 / 1024 / 1024 / 1024 ≈ 417 GB
Odpowiedź: ~417 GB (dlatego kompresja jest konieczna!)
Zadania maturalne — zestaw z 27.09
Arkusz zawiera treści zadań bez rozwiązań — możesz wysłać go uczniowi przed sesją.
Pięć zadań na kompresję, rozmiar danych i pojęcia, które CKE sprawdza jednym zdaniem. Zadanie 10.3 (Huffman) jest najdłuższe — rysuj drzewo, nie licz w pamięci.
KOMPRESJA I DANE - 5 zadań. Drzewo Huffmana (10.3) budujemy razem,
reszta samodzielnie.
10.1 Zakoduj metodą RLE ciąg AAAABBBCCCCCCDD. Ile znaków ma wynik i jaki
jest stopień kompresji?
10.2 Zakoduj metodą RLE ciąg ABABAB. Jaki wniosek wynika z porównania
długości?
10.3 Litery mają częstości: A 45, B 13, C 12, D 16, E 9, F 5. Zbuduj kod
Huffmana, podaj liczbę bitów na cały tekst i porównaj z kodem
stałej długości.
10.4 Obraz 800 x 600 pikseli, 24 bity na piksel, bez kompresji. Podaj
rozmiar w bajtach, kilobajtach i megabajtach.
10.5 Wyjaśnij różnicę: (a) funkcja skrótu a szyfrowanie, (b) licencja MIT
a GPL, (c) grafika rastrowa a wektorowa.
Zadanie 10.1 — kompresja RLE
Zakoduj metodą RLE ciąg AAAABBBCCCCCCDD. Ile znaków ma wynik i jaki jest stopień kompresji?
AAAA BBB CCCCCC DD
4A 3B 6C 2D -> 4A3B6C2D
wejscie: 15 znakow
wyjscie: 8 znakow
oszczednosc: 1 - 8/15 = 46,7%
Wynik: 4A3B6C2D, 8 znaków, kompresja ok. 47%. RLE jest bezstratny — z zapisu odtwarzasz wejście znak w znak. Stosuje się go tam, gdzie dane mają długie serie identycznych wartości: faksy, obrazy z dużymi jednolitymi obszarami, mapy bitowe ikon.
Zadanie 10.2 — kiedy RLE szkodzi
Zakoduj metodą RLE ciąg ABABAB. Jaki wniosek wynika z porównania długości?
ABABAB -> 1A1B1A1B1A1B
6 znakow -> 12 znakow
Wynik jest dwa razy dłuższy od wejścia. Żaden algorytm kompresji bezstratnej nie skraca każdego możliwego wejścia — gdyby skracał, dałoby się go stosować wielokrotnie i sprowadzić dowolny plik do jednego bitu. RLE opłaca się wyłącznie przy danych z seriami powtórzeń; dla danych naprzemiennych lub losowych powiększa rozmiar.
Zadanie 10.3 — kod Huffmana
Litery mają częstości: A — 45, B — 13, C — 12, D — 16, E — 9, F — 5. Zbuduj kod Huffmana, podaj liczbę bitów potrzebną na cały tekst i porównaj z kodem stałej długości.
Algorytm: bierzesz dwa najmniejsze węzły, łączysz w nowy o sumie wag, wkładasz z powrotem — aż zostanie jeden. Lewa krawędź to 0, prawa to 1.
F(5) + E(9) = 14
C(12) + B(13) = 25
D(16) + 14 = 30
25 + 30 = 55
A(45) + 55 = 100 <- korzen
| Litera | Częstość | Kod | Długość | Bity razem |
|---|---|---|---|---|
| A | 45 | 0 | 1 | 45 |
| C | 12 | 100 | 3 | 36 |
| B | 13 | 101 | 3 | 39 |
| D | 16 | 111 | 3 | 48 |
| F | 5 | 1100 | 4 | 20 |
| E | 9 | 1101 | 4 | 36 |
Razem 224 bity na 100 znaków, czyli średnio 2,24 bita na znak. Kod stałej długości wymaga 3 bitów na znak (sześć liter nie zmieści się na dwóch bitach), czyli 300 bitów — Huffman oszczędza 25%.
0, więc żadne inne słowo kodowe nie zaczyna się od zera. Dekoder czyta bity po kolei i wie, gdzie kończy się znak, bez separatorów. Konkretne kody w Twoim rozwiązaniu mogą wyjść inne niż w tabeli (zależnie od kolejności łączenia równych wag), ale suma bitów musi być ta sama.Zadanie 10.4 — rozmiar obrazu rastrowego
Obraz nieskompresowany ma 800 × 600 pikseli i głębię 24 bity na piksel. Podaj rozmiar w bajtach, kilobajtach i megabajtach.
pikseli: 800 * 600 = 480 000
bitow: 480 000 * 24 = 11 520 000
bajtow: 11 520 000 / 8 = 1 440 000 B
KiB: 1 440 000 / 1024 = 1406,25 KiB
MiB: 1406,25 / 1024 = 1,37 MiB
1 440 000 bajtów, czyli ok. 1,37 MiB. Skrót: 24 bity to 3 bajty na piksel (po jednym na składową R, G, B), więc rozmiar = szerokość × wysokość × 3. Uważaj na jednostki — CKE czasem pyta o MB (dzielisz przez 1 000 000, wychodzi 1,44 MB), a czasem o MiB (dzielisz przez 10242, wychodzi 1,37 MiB). Dopisz w odpowiedzi, którego przelicznika użyłeś.
Zadanie 10.5 — pojęcia sprawdzane jednym zdaniem
Wyjaśnij różnicę: (a) funkcja skrótu a szyfrowanie, (b) licencja MIT a GPL, (c) grafika rastrowa a wektorowa.
(a) Skrót (hash) a szyfrowanie. Hash jest jednokierunkowy — z sumy kontrolnej nie odtworzysz wejścia, a wynik ma zawsze tę samą długość. Służy do sprawdzania integralności i przechowywania haseł. Szyfrowanie jest odwracalne: znając klucz, odzyskujesz dane w całości.
(b) MIT a GPL. MIT jest permisywna — wolno użyć kodu w programie zamkniętym, wystarczy zachować informację o autorstwie. GPL jest copyleft — program zawierający kod na GPL sam musi być udostępniony na GPL wraz z kodem źródłowym. Praktyczna konsekwencja: bibliotekę MIT wplecesz w produkt komercyjny, GPL wymusza otwarcie całości.
(c) Raster a wektor. Raster to siatka pikseli (JPG, PNG, BMP) — powiększenie daje rozmycie i widoczne piksele, rozmiar zależy od wymiarów i głębi barw. Wektor to opis figur wzorami i współrzędnymi (SVG, EPS) — skaluje się bezstratnie, rozmiar zależy od liczby obiektów, nie od wymiaru obrazu. Zdjęcie zawsze raster, logo zawsze wektor.
🔥 Rozgrzewka — Analiza algorytmu
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
T[i] mod 2 w pseudokodzie?mod oznacza resztę z dzielenia (modulo). T[i] mod 2 zwraca 0 dla liczb parzystych i 1 dla liczb nieparzystych. W Pythonie używamy %: T[i] % 2.wynik ← 0 lub i ← i + 1. NIE używamy = do przypisania! = w pseudokodzie to porównanie (równość).#01 — Analiza algorytmu (pseudokod)
Typ zadania: Część 1 — arkusz papierowy · Czas: ~30 min · Pseudokod CKE
Kontekst
Na arkuszu maturalnym podano pseudokod algorytmu operującego na tablicy liczb całkowitych T[1..n]. Przeanalizuj algorytm i odpowiedz na pytania.
Pseudokod
Dane: T[1..8] = [4, 7, 2, 9, 1, 5, 8, 3], n = 8
Algorytm TAJEMNICA(T, n):
wynik ← 0
i ← 1
dopóki i ≤ n wykonuj:
jeżeli T[i] mod 2 = 1 to:
wynik ← wynik + T[i]
i ← i + 1
zwróć wynik
Polecenia
- Podpunkt a) Prześledź algorytm krok po kroku. Podaj wartość zmiennej
wynikpo każdej iteracji pętli. - Podpunkt b) Co oblicza ten algorytm? Opisz jednym zdaniem.
- Podpunkt c) Zmodyfikuj algorytm tak, aby obliczał sumę liczb parzystych.
- Podpunkt d) Jaka jest złożoność czasowa tego algorytmu? Uzasadnij.
- Podpunkt e) Napisz ten algorytm w Pythonie.
Na lekcji: Niech uczeń sam narysuje tabelkę śledzenia. Ty NIE podawaj wyniku — pytaj: „Co jest w wynik po 3 iteracji?"
#01 — Rozwiązanie krok po kroku
„Zanim cokolwiek policzysz - przeczytaj kod spokojnie. Co robi T[i] mod 2 = 1?"
„Kiedy reszta z dzielenia przez 2 wynosi 1?"
Odp: Wtedy, gdy liczba jest nieparzysta (np. 7, 9, 3).
wynik dodaje tylko te liczby, ktore sa nieparzyste. Czyli liczy sume liczb nieparzystych.•
mod = reszta z dzielenia (w Pythonie to %)•
T[i] = i-ty element tablicy•
wynik <- 0 = przypisanie: wstaw 0 do zmiennej wynik„Narysuj tabelke z kolumnami: krok, i, T[i], T[i] mod 2, warunek, wynik. Wypelnij wiersz po wierszu - to najlepszy sposob, zeby zlapac, co robi algorytm."
wynik. Jesli 0 - wynik sie nie zmienia.T = [4, 7, 2, 9, 1, 5, 8, 3]
Krok | i | T[i] | mod 2 | nieparzyste? | wynik
0 | | | | | 0
1 | 1 | 4 | 0 | NIE | 0
2 | 2 | 7 | 1 | TAK | 7
3 | 3 | 2 | 0 | NIE | 7
4 | 4 | 9 | 1 | TAK | 16
5 | 5 | 1 | 1 | TAK | 17
6 | 6 | 5 | 1 | TAK | 22
7 | 7 | 8 | 0 | NIE | 22
8 | 8 | 3 | 1 | TAK | 25
Wynik koncowy: 25 (bo 7 + 9 + 1 + 5 + 3 = 25)
„Opisz jednym zdaniem, co robi algorytm. Nie pisz jak to robi - tylko co zwraca."
Odp: Algorytm oblicza sume wszystkich elementow nieparzystych
w tablicy T o n elementach.
„Co trzeba zmienic, zeby algorytm sumowal parzyste zamiast nieparzystych?"
T[i] mod 2 = 0. Wystarczy zmienic jedna cyfre w warunku.Zmiana: w warunku zamien 1 na 0
BYLO: jezeli T[i] mod 2 = 1 to:
JEST: jezeli T[i] mod 2 = 0 to:
Sprawdzenie dla T = [4, 7, 2, 9, 1, 5, 8, 3]:
parzyste to: 4, 2, 8
suma: 4 + 2 + 8 = 14
„Ile razy wykona sie petla, jesli tablica ma n elementow?"
Odp: dokladnie n razy - po jednym przebiegu na kazdy element.
O(n). Dwie petle jedna w drugiej = O(n²).Zlozonosc czasowa: O(n)
Dlaczego:
- petla wykonuje sie dokladnie n razy (i od 1 do n)
- w kazdym przebiegu robimy stala liczbe operacji
(jedno mod, jedno porownanie, ewentualnie dodawanie)
- nie ma petli w petli
Slowo „liniowa" znaczy: jak n urosnie 10 razy,
to czas tez urosnie okolo 10 razy.
„Przepisujemy pseudokod 1:1 na Pythona. Bez sztuczek - zwykla petla for, if i zmienna wynik. Tak jak na kartce."
for x in T: - wtedy nie musimy w ogole liczyc indeksow.T = [4, 7, 2, 9, 1, 5, 8, 3]
wynik = 0 # zaczynamy od zera
for x in T: # bierzemy po kolei kazda liczbe z T
if x % 2 == 1: # jesli liczba jest nieparzysta...
wynik = wynik + x # ...dodajemy ja do wyniku
print(wynik) # wypisze: 25
i, gdyby ktos wolal):T = [4, 7, 2, 9, 1, 5, 8, 3]
n = len(T) # len(T) zwraca dlugosc listy, czyli 8
wynik = 0
i = 0 # w Pythonie indeksy zaczynaja sie od 0
while i < n:
if T[i] % 2 == 1:
wynik = wynik + T[i]
i = i + 1
print(wynik) # 25
dane.txt, jedna liczba na linie):T = [] # pusta lista na liczby
plik = open("dane.txt") # otwieramy plik do czytania
linie = plik.readlines() # wczytujemy wszystkie linie na liste
plik.close() # zamykamy plik
for linia in linie: # przechodzimy po kazdej linii
liczba = int(linia.strip()) # strip() usuwa enter, int() zamienia tekst na liczbe
T.append(liczba) # dokladamy liczbe do listy T
wynik = 0
for x in T:
if x % 2 == 1:
wynik = wynik + x
print(wynik)
•
% w Pythonie to to samo co mod w pseudokodzie•
len(T) daje liczbe elementow listy•
.strip() usuwa enter (znak nowej linii) z konca tekstu•
int("7") zamienia tekst "7" na liczbe 7•
print(wynik) wypisuje wynik na ekranCwiczenia do samodzielnego rozwiazania
Kazde cwiczenie pracuje na malym pseudokodzie. Najpierw probujesz sam: prowadzisz tabelke sledzenia (kolumny: i, warunek, wynik) i odpowiadasz na pytania. Dopiero potem otwierasz odpowiedz.
wynik to odpowiedz.
wynik.
wynik <- 0
i <- 1
dopoki i <= n wykonuj:
wynik <- wynik + T[i]
i <- i + 1
zwroc wynik
Pokaz odpowiedz
Algorytm sumuje wszystkie elementy tablicy. Tabelka:
| krok | i | T[i] | wynik po dodaniu |
|---|---|---|---|
| 1 | 1 | 3 | 3 |
| 2 | 2 | 1 | 4 |
| 3 | 3 | 4 | 8 |
| 4 | 4 | 2 | 10 |
| 5 | 5 | 5 | 15 |
wynik = 15. Po piatym kroku i staje sie 6, warunek i <= 5 juz nie jest spelniony, petla konczy prace.
wynik. Co liczy ten algorytm?
wynik <- 0
i <- 1
dopoki i <= n wykonuj:
jezeli T[i] mod 2 = 0 to:
wynik <- wynik + T[i]
i <- i + 1
zwroc wynik
Pokaz odpowiedz
Roznica wzgledem bazowego algorytmu z lekcji: warunek mod 2 = 0 zamiast = 1. Algorytm sumuje liczby parzyste.
Parzyste z tablicy: 4, 2, 8. Suma: 4 + 2 + 8 = 14.
Sprawdzenie sumy calkowitej: 25 (nieparzyste z lekcji) + 14 (parzyste) = 39, czyli rzeczywiscie suma wszystkich elementow tablicy — zgadza sie.
wynik.
wynik <- 0
i <- 1
dopoki i <= n wykonuj:
jezeli T[i] > 4 to:
wynik <- wynik + 1
i <- i + 1
zwroc wynik
Pokaz odpowiedz
Algorytm zlicza ile elementow tablicy jest wiekszych od 4. Zwroc uwage: wynik + 1, nie wynik + T[i] — to licznik, nie suma.
Elementy > 4: 7, 9, 5, 8. Sa cztery takie elementy. wynik = 4.
Tabelka skrocona (tylko kroki w ktorych wynik rosnie):
| i | T[i] | T[i] > 4? | wynik |
|---|---|---|---|
| 2 | 7 | tak | 1 |
| 4 | 9 | tak | 2 |
| 6 | 5 | tak | 3 |
| 7 | 8 | tak | 4 |
Pokaz odpowiedz
Zmieniamy tylko jedna linie: zamiast wynik + T[i] dajemy wynik + 1. Caly pseudokod:
wynik <- 0
i <- 1
dopoki i <= n wykonuj:
jezeli T[i] mod 2 = 1 to:
wynik <- wynik + 1
i <- i + 1
zwroc wynik
Nieparzyste w tablicy: 7, 9, 1, 5, 3 — jest ich pieciu. wynik = 5.
wynik.
wynik <- T[1]
i <- 2
dopoki i <= n wykonuj:
jezeli T[i] > wynik to:
wynik <- T[i]
i <- i + 1
zwroc wynik
Pokaz odpowiedz
Algorytm szuka najwiekszego elementu w tablicy. Klasyczny wzorzec: zaczynamy od pierwszego elementu jako kandydata, potem porownujemy z kolejnymi i podmieniamy gdy znajdziemy wiekszy.
Pelna tabelka sledzenia (wszystkie 8 krokow, zeby zobaczyc kompletny przebieg):
| krok | i | T[i] | T[i] > wynik? | wynik po kroku |
|---|---|---|---|---|
| start | — | — | — | 3 (= T[1]) |
| 1 | 2 | 8 | 8 > 3, TAK | 8 |
| 2 | 3 | 2 | 2 > 8, NIE | 8 |
| 3 | 4 | 9 | 9 > 8, TAK | 9 |
| 4 | 5 | 4 | 4 > 9, NIE | 9 |
| 5 | 6 | 1 | 1 > 9, NIE | 9 |
| 6 | 7 | 6 | 6 > 9, NIE | 9 |
| 7 | 8 | 7 | 7 > 9, NIE | 9 |
Pogrubione wiersze to te, w ktorych wynik sie zmienia — reszta to porownania bez podmiany. wynik = 9.
wynik? Jaka jest zlozonosc czasowa (notacja O)?
wynik <- 0
i <- 1
dopoki i <= n - 1 wykonuj:
jezeli T[i] < T[i+1] to:
wynik <- wynik + 1
i <- i + 1
zwroc wynik
Pokaz odpowiedz
Algorytm zlicza, ile razy nastepny element jest wiekszy od poprzedniego — czyli liczy pary sasiednich elementow ulozone rosnaco.
Petla idzie od i=1 do i=5 (czyli n−1 = 5 krokow). Porownania:
| i | T[i] | T[i+1] | T[i] < T[i+1]? | wynik |
|---|---|---|---|---|
| 1 | 2 | 5 | tak | 1 |
| 2 | 5 | 3 | nie | 1 |
| 3 | 3 | 8 | tak | 2 |
| 4 | 8 | 1 | nie | 2 |
| 5 | 1 | 4 | tak | 3 |
wynik = 3. Petla wykonuje sie n−1 razy, kazda iteracja to staly koszt (porownanie + ewentualny inkrement), wiec zlozonosc to O(n) — tak samo jak dla wszystkich poprzednich algorytmow z tej lekcji. Liniowa, bo przechodzimy tablice raz.
- akumulator (zad. 1, 2):
wynik <- wynik + cos— suma - licznik (zad. 3, 4, 6):
wynik <- wynik + 1— zliczanie - kandydat (zad. 5):
jezeli lepszy to wynik <- nowy— max/min
Wskazowki dla prowadzacego
Pytania diagnostyczne na rozgrzewke (5 min)
Zadaj te pytania zanim pokazesz pseudokod. Jezeli uczen tnie sie na ktoryms, cofnij sie do podstaw zanim ruszysz dalej.
- „Co znaczy
T[3]?" — sprawdza czy uczen rozumie indeksowanie. Jezeli powie „trzeci element" — OK. Jezeli powie „trzy elementy" lub „liczba 3" — stop, tlumacz indeksowanie na palcach. - „Ile wynosi
7 mod 2? A8 mod 2?" — sprawdza znajomosc operatora modulo. Musi wiedziec ze nieparzyste daja 1, parzyste daja 0. - „Co robi
i <- i + 1?" — sprawdza rozumienie przypisania. Jezeli uczen myli to z rownaniem („przeciez i nie moze byc rowne i+1") — patrz sekcja typowych bledow nizej. - „Czym rozni sie
=od==w Pythonie?" — kluczowe przed przepisaniem na kod. Pierwsze przypisuje, drugie porownuje. - „Co znaczy
while i <= n? Kiedy petla sie konczy?" — sprawdza rozumienie warunku petli. Najczestszy blad to off-by-one.
Typowe bledy ucznia i jak je prostowac
| Uczen mowi / robi | Co to znaczy | Jak poprawic |
|---|---|---|
| „Indeksuje od 0, czyli T[0]=4" | Miesza konwencje CKE (od 1) z Pythonem (od 0) | Pokaz wprost: w pseudokodzie CKE T[1] to pierwszy. W Pythonie t[0] to pierwszy. To dwie rozne konwencje, nie blad. |
| „i = i+1 jest niemozliwe" | Myli przypisanie z rownaniem matematycznym | Strzalka <- w pseudokodzie i = w Pythonie znacza „wez prawa strone i wsadz do lewej". To rozkaz, nie rownanie. |
Zapomina o i <- i + 1 w pseudokodzie |
Petla while wyglada na „magiczna" — nie widzi ze inkrement jest recznie pisany | Pokaz co sie stanie bez tej linii: petla nieskonczona. Tu jest roznica z for, gdzie inkrement jest automatyczny. |
| „Wynik to 39" (zsumowal wszystko) | Pominal warunek mod 2 = 1 |
Wroc do tabelki sledzenia, kolumna po kolumnie. Niech sam wskaze w ktorym wierszu dodaje, a w ktorym pomija. |
W Pythonie pisze for i in range(1, 9) a potem t[i] |
Probuje zachowac numeracje od 1, dostaje IndexError | Decyzja: albo trzymamy sie konwencji Python (range od 0, t[i]), albo robimy for x in t (bez indeksu, bo nie jest potrzebny). Drugie prostsze. |
Podzial lekcji 45 min
| Czas | Faza | Co robi prowadzacy |
|---|---|---|
| 0-5 | Rozgrzewka | Pytania diagnostyczne (modulo, indeksowanie, przypisanie). Krotkie odpowiedzi, bez wykladu. |
| 5-15 | Wspolne sledzenie | Pokaz pseudokod na tablicy. Rysujcie tabelke razem, kolumna po kolumnie. Uczen dyktuje wartosci, ty piszesz. Cel: wynik = 25. |
| 15-25 | Co robi algorytm + zlozonosc | Pytanie: „powiedz jednym zdaniem co liczymy". Potem: „ile razy wykona sie petla dla n=100? dla n=1000?". Wprowadz O(n) na konkretnych liczbach. |
| 25-35 | Modyfikacja | „Zmien tak, zeby sumowal parzyste". Uczen sam ma zauwazyc ze wystarczy zmienic = 1 na = 0. Pokaz ze wynik wyjdzie 14 (4+2+8). |
| 35-43 | Python | Razem przepiscie pseudokod na Pythona: lista, wynik = 0, for x in t, if x % 2 == 1, wynik = wynik + x, print. Uruchom w Replicie/IDLE — niech zobaczy 25. |
| 43-45 | Domowka | Zadanie: napisz wersje ktora liczy ile jest nieparzystych (nie sume, tylko ilosc). Cel: sprawdzic czy umie samodzielnie zmodyfikowac wzorzec. |
Momenty „aha!" — wykorzystaj je
wynik <- 0 na wynik = 0 i nagle widzi ze to mechaniczne tlumaczenie. Wykorzystaj: pokaz tabelke dwukolumnowa (pseudokod CKE | Python) z 5-6 typowymi przekladami. Daj ja jako sciagawke na nastepne lekcje — uczen zobaczy ze ma narzedzie do kazdego zadania CKE.
Czerwone flagi — przerwij lekcje i cofnij sie
- Uczen nie wie ile to
7 mod 2— wroc do modulo, bez tego nic dalej nie zadziala. - Uczen nie umie wypelnic tabelki sledzenia nawet z twoja pomoca — pseudokod jest za trudny, daj prostszy (np. tylko zliczanie elementow bez warunku).
- Uczen pisze Python „z glowy" bez patrzenia na pseudokod — zatrzymaj, kaz pisac linijka po linijce z pseudokodem obok. Cel lekcji to tlumaczenie 1:1, nie wymyslanie.
sum(), list comprehensions, filter, lambda — nawet jezeli uczen pyta. Na maturze CKE oczekuje petli for/while z licznikiem. Uczen ktory zna tylko one-linery polegnie na zadaniu typu „narysuj diagram przeplywu" albo „podaj wartosci posrednich zmiennych". Trzymamy sie podstawowych konstrukcji do konca tego cyklu.📝 Sprawdzian — Analiza algorytmu
Sprawdź swoją wiedzę z analizy pseudokodu. Masz 10 minut na 5 pytań.
1. Co zwróci algorytm, który ma warunek T[i] mod 3 = 0 i sumuje T[i]?
2. Jaka jest złożoność czasowa algorytmu, który przegląda tablicę n-elementową raz?
3. W pseudokodzie CKE, jak zapisujemy przypisanie?
4. Dla T = [5, 2, 8, 3], ile wynosi suma elementów spełniających T[i] mod 2 = 1?
5. Co oznacza zwróć wynik w pseudokodzie?
🔥 Rozgrzewka — Programowanie Python
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
with open("plik.txt", "r", encoding="utf-8") as f: — użyj encoding="utf-8" żeby polskie znaki się wyświetlały poprawnie!parts = linia.strip().split(";") — najpierw strip() usuwa \n, potem split(";") dzieli po średniku. Wynik: ["2024-01-01", "-5.2", "0.0"]float() od int()?int() zamienia na liczbę całkowitą (np. "42" → 42). float() zamienia na ułamek (np. "-5.2" → -5.2). Temperatura i opady to float!max(lista) i min(lista). Dla obiektów używaj key: max(dane, key=lambda x: x["temp"]). Pamiętaj: zwraca element, nie indeks!data.split("-")[1] zwróci "01". Albo data[5:7] (slice). Możesz też użyć modułu datetime dla bardziej zaawansowanych operacji.#02 — Programowanie Python — przetwarzanie pliku
Typ zadania: Część 2 — komputer · Czas: ~45 min · Python
Kontekst
Plik dane_pogoda.txt zawiera dane pogodowe z 365 dni (po jednym wierszu na dzień). Format: data;temperatura;opady;cisnienie
Pobierz plik z danymi: dane_pogoda.txt — zapisz go w folderze projektu obok swojego skryptu.
2024-01-01;-5.2;0.0;1013
2024-01-02;-3.8;2.5;1010
2024-01-03;-7.1;0.0;1018
...
2024-12-31;1.3;5.2;1005
Polecenia
- Podpunkt a) Znajdź dzień z najniższą temperaturą i dzień z najwyższą. Podaj daty i wartości.
- Podpunkt b) Oblicz średnią temperaturę dla każdego miesiąca. Wyniki zapisz do
wyniki_b.txt. - Podpunkt c) Ile dni z opadami (opady > 0)? Jaki procent wszystkich dni?
- Podpunkt d) Znajdź najdłuższą serię dni bez opadów. Podaj daty od-do i liczbę dni.
- Podpunkt e) Ile dni ciśnienie było powyżej 1015 hPa i jednocześnie temperatura była ujemna?
- Podpunkt f) Wyniki podpunktów c, d, e zapisz do
wyniki.txt.
Na lekcji: Zacznij od wczytania pliku i wydrukowania 3 pierwszych wierszy. Potem rozwiązuj podpunkt po podpunkcie.
#02 — Rozwiązanie krok po kroku
i w każdej liście opisuje ten sam dzień.lista_dat = []
lista_temp = []
lista_opadow = []
lista_cisnien = []
f = open("dane_pogoda.txt", "r", encoding="utf-8")
for linia in f:
linia = linia.strip() # usuwamy spacje i znak nowej linii
parts = linia.split(";") # dzielimy po srednikach
lista_dat.append(parts[0]) # "2024-01-15"
lista_temp.append(float(parts[1])) # -3.2
lista_opadow.append(float(parts[2])) # 0.0
lista_cisnien.append(int(parts[3])) # 1018
f.close()
print("Wczytano " + str(len(lista_dat)) + " dni")
print("Pierwszy dzien:", lista_dat[0], lista_temp[0], lista_opadow[0], lista_cisnien[0])
print("Ostatni dzien:", lista_dat[-1], lista_temp[-1], lista_opadow[-1], lista_cisnien[-1])
i łączy je razem — temp dnia i to lista_temp[i], data tego samego dnia to lista_dat[i].Wersja w funkcji — tak, jak wygląda to w projekcie Qt Creatora
Gdy te same dane ma wczytać okno, wczytywanie idzie do funkcji: raz napisane, wołane z dowolnego miejsca. Poza return na końcu i nazwą pliku w argumencie nic się nie zmienia.
def wczytaj_dane(nazwa_pliku):
lista_dat = []
lista_temp = []
lista_opadow = []
lista_cisnien = []
f = open(nazwa_pliku, "r", encoding="utf-8")
for linia in f:
linia = linia.strip() # usuwamy spacje i znak nowej linii
if linia != "": # pomijamy pusty wiersz
parts = linia.split(";") # dzielimy po srednikach
lista_dat.append(parts[0])
lista_temp.append(float(parts[1]))
lista_opadow.append(float(parts[2]))
lista_cisnien.append(int(parts[3]))
f.close()
return lista_dat, lista_temp, lista_opadow, lista_cisnien
daty, temperatury, opady, cisnienia = wczytaj_dane("dane_pogoda.txt")
print("Wczytano " + str(len(daty)) + " dni")
print("Pierwszy dzien:", daty[0], temperatury[0], opady[0], cisnienia[0])
return.
Typy się zgadzają (wszystko to listy liczb), więc Python nie ma jak tego wychwycić. Program rusza, liczy, zapisuje plik — tylko wyniki opisują co innego, niż myślisz:
# ZLE - listy deklarowane w jednej kolejnosci, zwracane w innej
return lista_dat, lista_opadow, lista_cisnien, lista_temp
daty, temperatury, opady, cisnienia = wczytaj_dane("dane_pogoda.txt")
# temperatury trzymaja teraz opady
# opady trzymaja cisnienie
# cisnienia trzymaja temperature
Rozpoznasz to po absurdzie w wyniku, nie po komunikacie błędu. Przy tym pliku zła kolejność daje „najniższą temperaturę" równą 0.0 (bo to w rzeczywistości minimum opadów) i styczniową średnią 2.49 zamiast -0.82. Zasada: ta sama kolejność w trzech miejscach — deklaracja list, return, i zmienne po lewej stronie przypisania. Data, temperatura, opady, ciśnienie.
if linia != "":przedsplit()— pusty wiersz na końcu pliku albo przypadkowy enter w środku dajeIndexError: list index out of rangeprzyparts[0].int(parts[3])dla ciśnienia zamiastfloat— ciśnienie w pliku jest całkowite,floatzadziała, ale w wynikach zobaczysz1013.0zamiast1013.- Nazwy funkcji piszemy z małej litery, z podkreśleniami:
wczytaj_dane, nieWczytajDane.WielkaLiterajest w Pythonie zarezerwowana dla klas — egzaminator za to punktu nie odbierze, ale w kodzie od razu widać, co jest klasą, a co funkcją.
min_tmp = lista_temp[0]
max_tmp = lista_temp[0]
data_min = lista_dat[0]
data_max = lista_dat[0]
for i in range(len(lista_temp)):
tmp = lista_temp[i]
if tmp < min_tmp:
min_tmp = tmp
data_min = lista_dat[i]
if tmp > max_tmp:
max_tmp = tmp
data_max = lista_dat[i]
print("Najzimniej: " + data_min + " -> " + str(min_tmp) + " stopni")
print("Najcieplej: " + data_max + " -> " + str(max_tmp) + " stopni")
min_tmp = 0 ani min_tmp = 1000. Zacznij od lista_temp[0] — wtedy zawsze działa, nawet gdy wszystkie temperatury są dodatnie lub bardzo duże.lista[0], lista[1]). Słownik to taki sam pojemnik, ale do wartości dostajemy się przez klucz — dowolny tekst lub liczbę. Zapis: slownik = {} tworzy pusty słownik. slownik["01"] = 5 wkłada wartość 5 pod kluczem "01". slownik["01"] odczytuje tę wartość. "01" in slownik sprawdza, czy klucz już istnieje. slownik.keys() zwraca wszystkie klucze. Tyle wystarczy do tego zadania."2024-01-15" biorąc znaki od pozycji 5 do 7 — to da "01". Budujemy dwa proste słowniki: jeden trzyma sumę temperatur dla miesiąca, drugi liczy ile dni mieliśmy. Na końcu druga pętla wypisuje średnie i zapisuje do pliku.suma_miesiac = {} # np. {"01": -45.6, "02": 12.3, ...}
ile_miesiac = {} # np. {"01": 31, "02": 29, ...}
for i in range(len(lista_dat)):
m = lista_dat[i][5:7] # "2024-01-15" -> "01"
tmp = lista_temp[i]
if m not in suma_miesiac:
suma_miesiac[m] = 0
ile_miesiac[m] = 0
suma_miesiac[m] = suma_miesiac[m] + tmp
ile_miesiac[m] = ile_miesiac[m] + 1
# zapis do pliku wyniki_b.txt
# UWAGA: tryb "w" NADPISUJE plik za kazdym uruchomieniem.
# Stare wyniki znikaja - jezeli chcesz dopisywac, uzyj "a" zamiast "w".
f = open("wyniki_b.txt", "w", encoding="utf-8")
lista_miesiecy = list(suma_miesiac.keys())
lista_miesiecy.sort() # sortowanie alfabetyczne "01", "02", ...
for m in lista_miesiecy:
srednia = suma_miesiac[m] / ile_miesiac[m]
srednia = round(srednia, 2)
linia = "Miesiac " + m + ": srednia = " + str(srednia) + " stopni"
print(linia)
f.write(linia + "\n")
f.close()
round(srednia, 2) zaokrągla do 2 miejsc po przecinku. Słowniki rozwiniemy w lekcji me03 — tu wystarczy zrozumieć, że klucz miesiąca ("01".."12") zastępuje numer pozycji w liście.ile = 0
for i in range(len(lista_opadow)):
if lista_opadow[i] > 0:
ile = ile + 1
wszystkich = len(lista_opadow)
procent = ile / wszystkich * 100
procent = round(procent, 1)
print("Dni z opadami: " + str(ile))
print("Procent: " + str(procent) + "%")
max_seria = 0
max_start = 0
biezaca = 0
biezacy_start = 0
for i in range(len(lista_opadow)):
if lista_opadow[i] == 0:
if biezaca == 0:
biezacy_start = i # zapamietaj poczatek nowej serii
biezaca = biezaca + 1
else:
if biezaca > max_seria:
max_seria = biezaca
max_start = biezacy_start
biezaca = 0
# WAZNE: sprawdz ostatnia serie PO petli
if biezaca > max_seria:
max_seria = biezaca
max_start = biezacy_start
data_od = lista_dat[max_start]
data_do = lista_dat[max_start + max_seria - 1]
print("Najdluzsza seria bez opadow: " + str(max_seria) + " dni")
print("Od " + data_od + " do " + data_do)
else — porównanie musi być POWTÓRZONE po pętli. To najczęstszy błąd w tym typie zadania.max_seria od razu wewnątrz gałęzi if lista_opadow[i] == 0 — po każdym zwiększeniu biezaca robisz if biezaca > max_seria: max_seria = biezaca. Wtedy nie trzeba sprawdzać po pętli, bo każdy dzień suchy od razu aktualizuje maksimum. Wersja powyżej (z aktualizacją w else) jest klasyczna i wymaga sprawdzenia po pętli — w cwiczeniu 6 zobaczysz wariant „w if", żeby porównać obie szkoły. Wybierz tę, którą szybciej zapamiętasz — obie dają ten sam wynik.and. Sprawdzamy ten sam indeks i w dwóch listach równoległych — to działa, bo pozycja i opisuje ten sam dzień we wszystkich listach.ile_e = 0
for i in range(len(lista_dat)):
if lista_cisnien[i] > 1015 and lista_temp[i] < 0:
ile_e = ile_e + 1
print("Dni z cisnieniem > 1015 i temperatura < 0: " + str(ile_e))
"w" (zapis), wypisujemy wyniki podpunktów a) c) d) e) — każdy w nowej linii. Pamiętaj o \n na końcu, inaczej wszystko skleja się w jedną linię.# Tryb "w" NADPISUJE plik. Stary wyniki.txt przestaje istniec w momencie open(...).
# Jezeli przy kolejnym uruchomieniu nie widzisz "poprzednich" wynikow - tak ma byc.
# Do dopisywania na koniec uzyj trybu "a" (append).
f = open("wyniki.txt", "w", encoding="utf-8")
f.write("a) Najzimniej: " + data_min + " -> " + str(min_tmp) + " stopni\n")
f.write("a) Najcieplej: " + data_max + " -> " + str(max_tmp) + " stopni\n")
f.write("c) Dni z opadami: " + str(ile) + " (" + str(procent) + "%)\n")
f.write("d) Najdluzsza seria bez opadow: " + str(max_seria) + " dni\n")
f.write(" Od " + data_od + " do " + data_do + "\n")
f.write("e) Cisnienie > 1015 i temp < 0: " + str(ile_e) + " dni\n")
f.close()
print("Wyniki zapisane do wyniki.txt")
wyniki.txt w notatniku. Powinny być 4 podpunkty (a, c, d, e). Średnie miesięczne (b) są w osobnym pliku wyniki_b.txt.Sama matura z informatyki nie wymaga interfejsu graficznego — tam wystarczy konsola i plik wynikowy. Ale na INF.04 okno w PyQt6 jest obowiązkowe, a Ty zdajesz oba egzaminy. Ten krok pokazuje najważniejszą rzecz w praktyce: logika się nie zmienia. Te same funkcje, te same pętle — zmienia się tylko to, dokąd trafia wynik.
| Wersja konsolowa | Wersja z oknem |
|---|---|
print(tekst) | self.listaWynikow.addItem(tekst) |
| program liczy i kończy się | program czeka na kliknięcie użytkownika |
| wynik leci od razu | funkcja zwraca listę napisów, okno je wyświetla |
Dlatego funkcja analizuj() niżej nie drukuje niczego — zbiera gotowe wiersze do listy i oddaje je na zewnątrz. To jest ta jedna zmiana, którą trzeba zrobić w głowie: oddzielić liczenie od pokazywania.
Struktura projektu — trzy pliki z szablonu Qt Creatora
Qt Creator zakłada projekt Qt for Python — Widgets (w drzewku widnieje jako „PySide Widgets Project" — to tylko nazwa szablonu, kod piszemy w całości w PyQt6). Dostajesz trzy pliki, każdy z jednym zadaniem:
| Plik | Co w nim jest | Czy dotykasz |
|---|---|---|
pyproject.toml | metadane projektu dla Qt Creatora | nie |
form.ui | wygląd okna — układasz go myszką w Designerze | tak, w Designerze |
mainwindow.py | logika zadania i podpięcie przycisków | tak, tu piszesz |
form.ui do Pythona: albo kompilujesz formularz raz komendą pyuic6 (opisane w module „9. PyQt6 (GUI)"), albo wczytujesz go w locie przez uic.loadUi — tak jak niżej. Druga droga jest wygodniejsza na lekcji: poprawiasz formularz w Designerze, zapisujesz, uruchamiasz — bez kroku kompilacji. Na egzaminie obie są poprawne.1. form.ui — cztery widżety i ich nazwy
W Designerze przeciągasz na okno cztery elementy i każdemu ustawiasz objectName w panelu po prawej. Ta nazwa to jedyny łącznik między formularzem a kodem — w Pythonie odwołujesz się dokładnie tak, jak nazwiesz widżet tutaj.
| Widżet | objectName | Do czego służy |
|---|---|---|
| QLabel | etykietaStatus | napis „Wczytano N…" |
| QPushButton | przyciskWczytaj | uruchamia obliczenia |
| QListWidget | listaWynikow | pokazuje wyniki zamiast print() |
| QPushButton | przyciskZapisz | zapisuje plik wynikowy |
Na koniec zaznacz okno i wybierz Lay out Vertically, żeby widżety układały się jeden pod drugim przy zmianie rozmiaru.
form.ui, który już masz w projekcie — nie twórz nowego. Otwórz go dwuklikiem w drzewku Qt Creatora; plik otworzy się w Designerze i tam przeciągasz widżety. Plik .ui to zwykły XML, więc gdybyś chciał zobaczyć, jak wygląda gotowy formularz w środku albo wkleić go zamiast klikać — to jest jego zawartość:<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>560</width>
<height>420</height>
</rect>
</property>
<property name="windowTitle">
<string>Analiza danych pogodowych</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="ukladGlowny">
<item>
<widget class="QLabel" name="etykietaStatus">
<property name="text">
<string>Nie wczytano jeszcze pliku</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="przyciskWczytaj">
<property name="text">
<string>Wczytaj dane_pogoda.txt</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="listaWynikow"/>
</item>
<item>
<widget class="QPushButton" name="przyciskZapisz">
<property name="text">
<string>Zapisz wyniki.txt</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
self." — i nie podpowie.
Przy uic.loadUi() widżety powstają dopiero w czasie działania programu. Edytor czyta wyłącznie kod Pythona, nie zagląda do form.ui, więc nie ma skąd wiedzieć, że za chwilę pojawi się przyciskWczytaj. Autouzupełnianie będzie puste, a kod i tak zadziała po uruchomieniu — piszesz nazwę z pamięci, litera w literę tak, jak brzmi objectName w Designerze.
Jeśli chcesz mieć podpowiadanie, skompiluj formularz do Pythona. W terminalu, w folderze projektu:
python3 -m PyQt6.uic.pyuic form.ui -o ui_form.py
Powstaje plik ui_form.py z klasą Ui_MainWindow — zwykły kod Pythona, który edytor potrafi przeczytać. Od tej chwili po self.ui. dostajesz pełną listę widżetów.
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow
from ui_form import Ui_MainWindow
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.przyciskWczytaj.clicked.connect(self.wczytaj)
def wczytaj(self):
self.ui.etykietaStatus.setText("Kliknieto Wczytaj")
self.ui.listaWynikow.clear()
self.ui.listaWynikow.addItem("pozycja testowa")
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = MainWindow()
widget.show()
sys.exit(app.exec())
Różnica, która najczęściej myli: przy uic.loadUi(...) widżety siedzą bezpośrednio na oknie — self.przyciskWczytaj. Po kompilacji pyuic6 siedzą w obiekcie formularza — self.ui.przyciskWczytaj. Wymieszanie obu zapisów daje AttributeError przy pierwszym kliknięciu.
uic.loadUi | pyuic6 + ui_form.py | |
|---|---|---|
| dostęp do widżetu | self.przyciskWczytaj | self.ui.przyciskWczytaj |
| podpowiadanie w edytorze | nie ma | jest |
| po zmianie formularza | nic nie robisz | kompilujesz ponownie |
| plików w projekcie | bez zmian | dochodzi ui_form.py (nie edytuje się go ręcznie) |
Obie drogi są poprawne na egzaminie. Przy nauce wygodniejsze jest loadUi (jeden plik mniej), przy większym formularzu — kompilacja, bo edytor pilnuje literówek za Ciebie.
2. mainwindow.py — logika i podpięcie
import sys
from pathlib import Path
from PyQt6 import uic
from PyQt6.QtWidgets import QApplication, QMainWindow, QMessageBox
FOLDER = Path(__file__).parent
# ---------- logika: te same obliczenia co w krokach 0-5 ----------
def wczytaj_dane(nazwa_pliku):
daty = []
temperatury = []
opady = []
cisnienia = []
f = open(FOLDER / nazwa_pliku, "r", encoding="utf-8")
for linia in f:
linia = linia.strip()
if linia != "":
parts = linia.split(";")
daty.append(parts[0])
temperatury.append(float(parts[1]))
opady.append(float(parts[2]))
cisnienia.append(int(parts[3]))
f.close()
return daty, temperatury, opady, cisnienia
def analizuj(daty, temperatury, opady, cisnienia):
wyniki = []
min_tmp = temperatury[0]
max_tmp = temperatury[0]
data_min = daty[0]
data_max = daty[0]
for i in range(len(temperatury)):
if temperatury[i] < min_tmp:
min_tmp = temperatury[i]
data_min = daty[i]
if temperatury[i] > max_tmp:
max_tmp = temperatury[i]
data_max = daty[i]
wyniki.append("a) najzimniej: " + data_min + " (" + str(min_tmp) + " st.)")
wyniki.append("a) najcieplej: " + data_max + " (" + str(max_tmp) + " st.)")
suma_miesiac = {}
ile_miesiac = {}
for i in range(len(daty)):
m = daty[i][5:7]
if m not in suma_miesiac:
suma_miesiac[m] = 0
ile_miesiac[m] = 0
suma_miesiac[m] = suma_miesiac[m] + temperatury[i]
ile_miesiac[m] = ile_miesiac[m] + 1
miesiace = list(suma_miesiac.keys())
miesiace.sort()
for i in range(len(miesiace)):
m = miesiace[i]
srednia = round(suma_miesiac[m] / ile_miesiac[m], 2)
wyniki.append("b) miesiac " + m + ": srednia " + str(srednia) + " st.")
ile_z_opadami = 0
for i in range(len(opady)):
if opady[i] > 0:
ile_z_opadami = ile_z_opadami + 1
procent = round(ile_z_opadami / len(opady) * 100, 1)
wyniki.append("c) dni z opadami: " + str(ile_z_opadami) + " (" + str(procent) + "%)")
najdluzsza = 0
biezaca = 0
for i in range(len(opady)):
if opady[i] == 0:
biezaca = biezaca + 1
if biezaca > najdluzsza:
najdluzsza = biezaca
else:
biezaca = 0
wyniki.append("d) najdluzsza seria bez opadow: " + str(najdluzsza) + " dni")
ile_wyz = 0
for i in range(len(cisnienia)):
if cisnienia[i] > 1015 and temperatury[i] < 0:
ile_wyz = ile_wyz + 1
wyniki.append("e) dni z cisnieniem > 1015 hPa i mrozem: " + str(ile_wyz))
return wyniki
# ---------- okno: widzety pochodza z form.ui ----------
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
uic.loadUi(FOLDER / "form.ui", self) # NAJPIERW formularz...
self.dane_wczytane = False # ...potem wlasne pola i sygnaly
self.przyciskWczytaj.clicked.connect(self.wczytaj)
self.przyciskZapisz.clicked.connect(self.zapisz)
def wczytaj(self):
self.daty, self.temperatury, self.opady, self.cisnienia = wczytaj_dane("dane_pogoda.txt")
self.dane_wczytane = True
self.etykietaStatus.setText("Wczytano " + str(len(self.daty)) + " dni")
self.listaWynikow.clear()
wyniki = analizuj(self.daty, self.temperatury, self.opady, self.cisnienia)
for i in range(len(wyniki)):
self.listaWynikow.addItem(wyniki[i])
def zapisz(self):
if self.dane_wczytane == False:
QMessageBox.warning(self, "Uwaga", "Najpierw wczytaj plik z danymi.")
return
wyniki = analizuj(self.daty, self.temperatury, self.opady, self.cisnienia)
f = open(FOLDER / "wyniki.txt", "w", encoding="utf-8")
for i in range(len(wyniki)):
f.write(wyniki[i] + "\n")
f.close()
QMessageBox.information(self, "Gotowe", "Zapisano wyniki.txt")
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = MainWindow()
widget.show()
sys.exit(app.exec())
uic.loadUi()musi być pierwsze. Przed nim żaden widżet z formularza jeszcze nie istnieje — odwołanie doself.listaWynikowlinijkę wyżej toAttributeError. Kolejność w__init__jest zawsze ta sama:super(),loadUi(), dopiero potem własne pola iconnect.- Samo
self.w linii to błąd składni. Jeśli zaczniesz pisaćself.i zostawisz niedokończone, Python nie uruchomi nawet pierwszej linii pliku — dostanieszSyntaxError: invalid syntaxi czerwony znacznik na tej linii. - Nazwa w kodzie musi być identyczna jak
objectName. Literówka nie wyjdzie przy starcie — program wstanie normalnie i wywali się dopiero przy kliknięciu, bo dopiero wtedy sięga po widżet. - Ścieżka do
form.ui. Samo'form.ui'działa tylko wtedy, gdy terminal stoi w folderze projektu. Uruchomienie z innego katalogu dajeFileNotFoundError. Dlatego w kodzie jestPath(__file__).parent— to zawsze folder, w którym leżymainwindow.py, niezależnie od tego, skąd odpalasz.
clicked.connect(self.metoda) — bez nawiasów przy nazwie metody, inaczej podłączysz wynik wywołania zamiast samej funkcji; oraz clear() przed ponownym wypełnieniem listy — bez tego drugie kliknięcie dokleja wyniki do poprzednich.a) najzimniej: 2024-01-30 (-6.0 st.)
a) najcieplej: 2024-07-15 (25.9 st.)
b) miesiac 01: srednia -0.82 st.
b) miesiac 02: srednia 0.63 st.
b) miesiac 03: srednia 3.8 st.
b) miesiac 04: srednia 10.63 st.
b) miesiac 05: srednia 15.98 st.
b) miesiac 06: srednia 19.79 st.
b) miesiac 07: srednia 21.0 st.
b) miesiac 08: srednia 17.91 st.
b) miesiac 09: srednia 13.58 st.
b) miesiac 10: srednia 7.87 st.
b) miesiac 11: srednia 4.18 st.
b) miesiac 12: srednia -0.15 st.
c) dni z opadami: 110 (30.1%)
d) najdluzsza seria bez opadow: 28 dni
e) dni z cisnieniem > 1015 hPa i mrozem: 24dane_pogoda.txt do folderu projektu, obok mainwindow.py, i uruchom zielonym przyciskiem w Qt Creatorze albo komendą python3 mainwindow.py. Klik Zapisz tworzy wyniki.txt z tą samą zawartością.Cwiczenia do samodzielnego rozwiazania
Kazde cwiczenie zaklada plik tekstowy w tym samym katalogu co skrypt. Rozwiazuj kolejno, po kazdym uruchom kod i sprawdz wynik. Uzywaj tylko: for, if, open, .split(), .strip(), int(), float(), len(), sum(), min(), max(), list i prostych zmiennych.
liczby.txt z kilkunastoma liczbami (po jednej w linii), plik oceny.txt z linijkami w formacie Imie;ocena oraz plik pomiary.txt z linijkami data;temperatura;opady. Te trzy pliki wystarcza na wszystkie cwiczenia.
liczby.txt i policz ile zawiera linii. Wypisz wynik na ekran.
Pokaz odpowiedz
licznik = 0
plik = open("liczby.txt", "r", encoding="utf-8")
for linia in plik:
licznik = licznik + 1
plik.close()
print("Liczba linii: " + str(licznik))
Mozna tez uzyc len(plik.readlines()), ale wersja z petla jest bardziej uniwersalna i dziala dla bardzo duzych plikow.
liczby.txt kazda linia zawiera jedna liczbe calkowita. Policz sume wszystkich liczb i wypisz wynik.
Pokaz odpowiedz
suma = 0
plik = open("liczby.txt", "r", encoding="utf-8")
for linia in plik:
linia = linia.strip()
liczba = int(linia)
suma = suma + liczba
plik.close()
print("Suma: " + str(suma))
Pamietaj o .strip() — bez tego na koncu linii zostaje znak konca wiersza i int() rzuci blad.
oceny.txt ma format Imie;ocena (np. Anna;5). Policz ile osob dostalo ocene wieksza lub rowna 4.
Pokaz odpowiedz
licznik = 0
plik = open("oceny.txt", "r", encoding="utf-8")
for linia in plik:
linia = linia.strip()
czesci = linia.split(";")
ocena = int(czesci[1])
if ocena >= 4:
licznik = licznik + 1
plik.close()
print("Ocen 4+: " + str(licznik))
czesci[0] to imie, czesci[1] to ocena jako tekst — trzeba zamienic na int przed porownaniem.
liczby.txt do listy o nazwie liczby, a potem policz srednia i wypisz wynik z dokladnoscia do dwoch miejsc po przecinku.
Pokaz odpowiedz
liczby = []
plik = open("liczby.txt", "r", encoding="utf-8")
for linia in plik:
linia = linia.strip()
liczby.append(float(linia))
plik.close()
srednia = sum(liczby) / len(liczby)
srednia = round(srednia, 2)
print("Srednia: " + str(srednia))
round(x, 2) zaokragla do dwoch miejsc. sum() i len() dzialaja na liscie wprost, bez petli.
pomiary.txt ma format data;temperatura;opady. Wczytaj do trzech list rownoleglych (daty, temperatury, opady), a potem znajdz najnizsza temperature i wypisz date, w ktorej zostala zmierzona.
Pokaz odpowiedz
daty = []
temperatury = []
opady = []
plik = open("pomiary.txt", "r", encoding="utf-8")
for linia in plik:
linia = linia.strip()
czesci = linia.split(";")
daty.append(czesci[0])
temperatury.append(float(czesci[1]))
opady.append(float(czesci[2]))
plik.close()
min_temp = temperatury[0]
data_min = daty[0]
for i in range(1, len(temperatury)):
if temperatury[i] < min_temp:
min_temp = temperatury[i]
data_min = daty[i]
print("Najnizsza temperatura: " + str(min_temp) + " w dniu " + data_min)
Klucz: indeks i dziala na wszystkich trzech listach jednoczesnie — daty[i] i temperatury[i] opisuja ten sam dzien. Tak samo zrobisz max, jesli zamienisz < na >.
> 0). Wypisz dlugosc tej serii. Uwaga: pamietaj o sprawdzeniu serii rowniez po zakonczeniu petli.
Pokaz odpowiedz
biezaca = 0
najdluzsza = 0
for i in range(len(temperatury)):
if temperatury[i] > 0:
biezaca = biezaca + 1
if biezaca > najdluzsza:
najdluzsza = biezaca
else:
biezaca = 0
print("Najdluzsza seria dni z temp > 0: " + str(najdluzsza))
To jest wariant alternatywny wspomniany w kroku 4 lekcji: aktualizacja najdluzsza dzieje sie wewnatrz if, wiec dziala takze gdy seria konczy plik — nie trzeba osobnego sprawdzenia po petli. Wersja z lekcji (aktualizacja w else) wymaga sprawdzenia po petli, bo ostatnia seria moze nie miec swojego "konca". Obie wersje sa rownowazne wynikowo — wybierz te, ktora latwiej ci zapamietac, ale rozumiej obie, bo na maturze trafisz na obie.
.txt.Wskazowki dla prowadzacego
Pytania diagnostyczne na start (5 min)
- "Mam plik tekstowy z 365 liniami. Jak go otworzyc i przeczytac linia po linii?" — oczekiwana odpowiedz:
open(...)+for linia in f. Jezeli uczen mowi "read()" i potem dzieli — to dziala, ale chcemy linia po linii. - "Linia wyglada tak:
2024-01-15;-3.2;0;1018. Jak rozdzielic to na 4 kawalki?" —.split(";"). Jezeli uczen mowi "po przecinku" — popraw, separator to srednik. - "Po split mam liste 4 stringow. Jak zamienic
'-3.2'na liczbe?" —float(). Czesty blad: uzycieint()na temperaturze. - "Mam 4 rownolegle listy. Co to znaczy ze sa rownolegle?" — ten sam indeks
iodnosi sie do tego samego dnia we wszystkich listach. To kluczowe pojecie. - "Jak znalezc najmniejsza wartosc w liscie liczb, ale BEZ uzycia funkcji
min()?" — petla zif tmp < min_tmp. To sprawdza czy uczen rozumie wzorzec, ktory potem wykorzystamy do znalezienia daty min/max.
Typowe bledy ucznia i jak je poprawic
| Co uczen robi/mowi | Co to znaczy | Jak poprawic |
|---|---|---|
Inicjalizuje min_tmp = 0 lub max_tmp = 0 |
Nie rozumie ze 0 to nie jest "neutralna" liczba. Jezeli wszystkie temp sa dodatnie, min wyjdzie 0 (blad). | Pokaz wzorzec: wez pierwsza wartosc z listy jako start, potem petla od indeksu 1. Albo min_tmp = lista_temp[0]. |
Zapomina o .strip() przed .split(";") |
Ostatni element listy ma \n na koncu, float() czasem zadziala ale przy stringu data dostaniemy "1018\n". |
Pokaz print(repr(parts[3])) — uczen zobaczy '1018\n' i zrozumie problem. |
| W petli na serie dni bez opadow zapomina sprawdzic po petli | Jezeli plik konczy sie seria suchych dni, ostatnia seria nigdy nie wpadnie do if biezaca > max_seria. |
Po petli dopisz drugie sprawdzenie if biezaca > max_seria:. Pokaz na przykladzie 5 dni suchych — bez tego dostanie 0. Albo naucz wariantu z aktualizacja max w gałezi if — wtedy post-check znika. |
Uzywa opady == 0 dla "dzien bez opadow" |
Jezeli opady to float (np. 0.0), porownanie zadziala, ale gdy w pliku jest "0.05" — uczen nie wie czy to "z opadami" czy nie. | Ustal konwencje: opady > 0 = dzien z opadami. Opisz to w komentarzu nad petla. |
| Liczy srednia miesieczna dzielac sume przez 30 | Nie rozumie ze luty ma 28/29 dni, a w pliku moga byc braki danych. | Pokaz wzorzec dwoch list rownoleglych: suma_miesiaca[m] i ile_dni[m], na koncu dzielenie. |
Bierze miesiac przez data.split("-")[1] i porownuje jako string |
Dziala, ale uczen czesto miesza string "01" z int 1. | Zostaw jako string "01".."12" — w slowniku/liscie rownoleglej to bez znaczenia, byle konsekwentnie. |
Podzial lekcji 45 minut
| Czas | Faza | Co robisz |
|---|---|---|
| 0-5 | Rozgrzewka / diagnoza | 5 pytan diagnostycznych przy tablicy. Pokaz jedna linie pliku — uczen ma opisac co widzi. |
| 5-15 | Wczytanie pliku (wspolnie) | Razem piszecie czesc wczytujaca: 4 puste listy, otwarcie pliku, petla, split, append. Po napisaniu print(len(lista_temp)) — sprawdzcie czy ladnie sie wczytalo. |
| 15-25 | Min/max temperatura (wspolnie) | Pokaz wzorzec: zmienna pomocnicza + indeks daty. Uczen sam pisze kawalek dla maksimum. To jest fundament — wszystkie pozniejsze podpunkty uzywaja tego samego patternu. |
| 25-40 | Uczen samodzielnie: opady + seria + warunek zlozony | Punkty c, d, e robia samodzielnie. Ty chodzisz, podpowiadasz. Najtrudniejsze: d (seria). Daj 5 min, jezeli stoja — wspolnie omawiacie schemat biezaca/max. |
| 40-45 | Zapis do pliku + podsumowanie | Pokaz open("wyniki.txt", "w", encoding="utf-8") + .write(). Zwroc uwage ze "w" nadpisuje — zademonstruj uruchamiajac dwa razy. Zadanie domowe: podpunkt b (srednia per miesiac do wyniki_b.txt) — wymaga dwoch slownikow po miesiacach, idealna powtorka i pomost do me03. |
Momenty "aha!"
data_min = lista_dat[i] w petli na min temp, kapuje ze indeks i to "wspolny adres" dla wszystkich list. To otwiera droge do mysenia o danych tabelarycznie. Wzmocnij to: narysuj na tablicy 4 kolumny z numerami wierszy z boku.
Moment 2 — pattern "biezaca/maksymalna" przy serii suchych dni. Gdy uczen sam wymysli ze potrzebuje dwoch zmiennych (jedna liczy aktualna seria, druga pamieta najdluzsza), i sam zauwazy ze "trzeba sprawdzic jeszcze raz po petli" — to jest moment przejscia z "kopiowania kodu" do "myslenia algorytmicznie". Zatrzymaj sie, doceniaj, daj 30 sekund ciszy zeby uczen poczul ze sam to wymyslil.
min(lista_temp) ani lista_temp.index(min(...)) dopoki uczen nie napisze recznej wersji. Wbudowane funkcje sa kuszace, ale celem lekcji jest zrozumienie petli z warunkiem. min() przyjdzie na nastepnej lekcji jako "skrot do tego co umiesz juz zrobic recznie".
📝 Sprawdzian — Programowanie Python
Sprawdź swoją wiedzę z przetwarzania plików w Pythonie. Masz 10 minut na 5 pytań.
1. Która konstrukcja PRAWIDŁOWO wczytuje plik z polskimi znakami?
2. Co zwróci "a;b;c".split(";")?
3. Jak poprawnie zamienić "-5.2" na liczbę?
4. Która funkcja znajdzie element z NAJMNIEJSZĄ wartością?
5. Jak wyciągnąć "01" z daty "2024-01-15"?
🔥 Rozgrzewka — Arkusz kalkulacyjny
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
A1 to adres względny — zmienia się przy kopiowaniu formuły. $A$1 to adres bezwzględny — NIE zmienia się. $A1 i A$1 to adresy mieszane.=LICZ.JEŻELI(B2:B100;">50") — warunek w cudzysłowie! Dla wielu warunków: =LICZ.WARUNKI(...).=WYSZUKAJ.PIONOWO(co;gdzie;która_kolumna;0). 0 = dokładne dopasowanie!=SUMA.JEŻELI(zakres_kryteriów;kryterium;zakres_sumy). Przykład: =SUMA.JEŻELI(A:A;"jabłka";B:B) — sumuje wartości z B, gdzie w A jest "jabłka".#03 — Arkusz kalkulacyjny
Typ zadania: Część 2 — komputer · Czas: ~30 min · Excel / LibreOffice Calc
Kontekst
Plik wyniki_egzamin.txt zawiera dane 200 uczniów z egzaminu. Format: imie;nazwisko;klasa;matematyka;informatyka;angielski
Anna;Kowalska;4A;72;85;68
Jan;Nowak;4B;55;92;71
Maria;Wiśniewska;4A;88;67;90
...
Punkty z każdego przedmiotu: 0–100.
Polecenia
- a) Zaimportuj dane do arkusza. W kolumnie G oblicz sumę punktów dla każdego ucznia. W kolumnie H oblicz średnią (2 m. po przecinku).
- b) Ile osób z klasy 4A zdało informatykę (≥50 pkt)? Użyj funkcji LICZ.WARUNKI.
- c) Znajdź najwyższy wynik z matematyki i użyj WYSZUKAJ.PIONOWO, aby znaleźć imię i nazwisko tej osoby.
- d) Oblicz średnią z informatyki osobno dla klasy 4A i 4B. Użyj ŚREDNIA.JEŻELI.
- e) Utwórz wykres kolumnowy porównujący średnie wyniki z 3 przedmiotów dla klas 4A i 4B.
Na lekcji: Zacznij od importu! Pokaż jak działa kreator tekstu → kolumny (separator: średnik).
#03 — Rozwiązanie krok po kroku
"Otworz Excel → Plik → Otworz → wybierz .txt → Kreator importu → separator srednik → zakoncz."
Po imporcie arkusz wyglada tak (dane od wiersza 1):
A B C D E F
1 Anna Kowalska 4A 72 85 68
2 Jan Nowak 4B 55 92 71
...
200 wierszy danych w zakresie A1:F200
Kolumny: A=imie, B=nazwisko, C=klasa, D=mat, E=inf, F=ang
G1: wpisz naglowek: Suma
G2: =SUMA(D2:F2)
→ zlap prawy dolny rog komorki i przeciagnij w dol do G201
H1: wpisz naglowek: Srednia
H2: =SREDNIA(D2:F2)
→ przeciagnij w dol do H201
Formula:
=LICZ.WARUNKI(C2:C201;"4A";E2:E201;">=50")
Jak czytac:
C2:C201;"4A" → w kolumnie klasa musi byc 4A
E2:E201;">=50" → w kolumnie informatyka musi byc >= 50
Wynik: liczba uczniow z 4A, ktorzy zdali informatyke.
">=50", nie samo >=50. Bez cudzyslowu Excel zwroci blad.Krok 1: Najwyzszy wynik z matematyki
J1: naglowek: Max mat
J2: =MAX(D2:D201)
Krok 2: Zbuduj tabelke pomocnicza w kolumnach K, L, M
K1: Mat L1: Imie M1: Nazwisko
K2: =D2 L2: =A2 M2: =B2
→ zaznacz K2:M2 i przeciagnij w dol do wiersza 201
Teraz tabelka K2:M201 ma matematyke jako PIERWSZA kolumne.
Krok 3: Wyszukaj imie i nazwisko ucznia z najwyzszym wynikiem
N1: Imie najlepszego
N2: =WYSZUKAJ.PIONOWO(J2;K2:M201;2;FALSZ)
O1: Nazwisko najlepszego
O2: =WYSZUKAJ.PIONOWO(J2;K2:M201;3;FALSZ)
=WYSZUKAJ.PIONOWO(czego_szukam; w_jakiej_tabeli; ktora_kolumne_zwrocic; FALSZ). Ostatni argument FALSZ oznacza dokladne dopasowanie (rownowaznie mozna wpisac 0 — obie wartosci dzialaja tak samo, ale w calej lekcji trzymamy FALSZ dla czytelnosci). Kolumna 2 to imie, kolumna 3 to nazwisko (liczac od pierwszej kolumny tabelki K).=MAX(D2:D201) w komorce, odczytaj wynik (np. 98), potem recznie przewin kolumne D i znajdz wiersz z ta wartoscia. Imie i nazwisko masz w tym samym wierszu."Dlaczego WYSZUKAJ.PIONOWO wymaga szukanej wartosci w pierwszej kolumnie tabeli?"
Odp: Bo funkcja przeszukuje wylacznie pierwsza kolumne tabeli i zwraca wartosc z n-tej kolumny tego samego wiersza. Nie potrafi szukac w kolumnie srodkowej — dlatego budujemy tabelke pomocnicza, w ktorej matematyka jest pierwsza.
Srednia informatyki dla klasy 4A:
=SREDNIA.JEZELI(C2:C201;"4A";E2:E201)
Srednia informatyki dla klasy 4B:
=SREDNIA.JEZELI(C2:C201;"4B";E2:E201)
Jak czytac:
=SREDNIA.JEZELI(gdzie_sprawdzic_warunek; warunek; z_czego_liczyc_srednia)
C2:C201 → kolumna klasa
"4A" → warunek: ma byc 4A
E2:E201 → bierzemy srednia z informatyki
Tabelka pomocnicza (np. w komorkach P1:S3):
P Q R S
1 Matematyka Informatyka Angielski
2 4A [formula] [formula] [formula]
3 4B [formula] [formula] [formula]
Wpisz formuly:
Q2: =SREDNIA.JEZELI(C2:C201;"4A";D2:D201) (mat 4A)
R2: =SREDNIA.JEZELI(C2:C201;"4A";E2:E201) (inf 4A)
S2: =SREDNIA.JEZELI(C2:C201;"4A";F2:F201) (ang 4A)
Q3: =SREDNIA.JEZELI(C2:C201;"4B";D2:D201) (mat 4B)
R3: =SREDNIA.JEZELI(C2:C201;"4B";E2:E201) (inf 4B)
S3: =SREDNIA.JEZELI(C2:C201;"4B";F2:F201) (ang 4B)
Tworzenie wykresu:
1. Zaznacz cala tabelke P1:S3 (razem z naglowkami!)
2. Wstaw → Wykres → Kolumnowy grupowany
3. Dodaj tytul wykresu, opisy obu osi i legende
Cwiczenia do samodzielnego rozwiazania
Wykonaj zadania po kolei. Kazde buduje na poprzednim. Otwieraj odpowiedz dopiero po wlasnej probie — nawet jeden nieudany strzal uczy wiecej niz przeczytanie gotowca.
Pokaz odpowiedz
C2: =SREDNIA(A2:A31)
C3: =MAX(A2:A31)
C4: =MIN(A2:A31)
To trzy najczesciej uzywane funkcje statystyczne w Excelu. Zapamietaj zakres A2:A31 — dwukropek oznacza "od ... do ...".
Pokaz odpowiedz
D2: =LICZ.JEZELI(B2:B31; 5)
D3: =LICZ.JEZELI(B2:B31; 1)
Skladnia: LICZ.JEZELI(zakres; warunek). Warunek to sama liczba 5 albo tekst w cudzyslowach ("=5" tez zadziala, ale zbedne).
Pokaz odpowiedz
E2: =SUMA.JEZELI(B2:B51; "napoje"; C2:C51)
Skladnia: SUMA.JEZELI(zakres_do_sprawdzenia; warunek; zakres_do_sumowania). Excel idzie po kolumnie B, znajduje "napoje", i z odpowiadajacego wiersza w C bierze cene do sumy.
Tekst "napoje" w cudzyslowach — bez nich Excel myslalby, ze to nazwa zakresu.
Pokaz odpowiedz
F2: =SREDNIA.JEZELI(B2:B51; "chleb"; C2:C51)
F3: =LICZ.JEZELI(C2:C51; ">10")
W F2 dziala analogicznie do SUMA.JEZELI — Excel uwzglednia tylko wiersze gdzie kategoria = "chleb" i liczy srednia z cen.
W F3 warunek ">10" musi byc w cudzyslowach — to caly tekst warunku, nie sama liczba.
Pokaz odpowiedz
D2: P003
E2: =WYSZUKAJ.PIONOWO(D2; A2:B6; 2; FALSZ)
Cztery argumenty po kolei:
D2— czego szukamy (kod P003)A2:B6— tabela do przeszukania (pierwsza kolumna A musi zawierac to czego szukamy)2— numer kolumny w tabeli z ktorej chcemy wynik (B to druga kolumna)FALSZ— dokladne dopasowanie (zawsze tak dla kodow/tekstow)
Wynik: 8. Zmien D2 na "P005" — wynik zmieni sie na 15. To wlasnie jest sila WYSZUKAJ.PIONOWO.
Pokaz odpowiedz
D2: =JEZELI(C2>5000; "wysoka"; "standardowa")
Skladnia: JEZELI(warunek; wartosc_gdy_TAK; wartosc_gdy_NIE).
Kopiowanie w dol: zaznacz D2, zlap maly kwadracik w prawym dolnym rogu komorki i przeciagnij do D21. Excel automatycznie zamieni C2 na C3, C4, C5... w kolejnych wierszach (adresowanie wzgledne — to dziala automatycznie, bez znakow $).
Gdybys chcial, zeby 5000 bylo wartoscia z konkretnej komorki F1, uzylbys adresu bezwzglednego: =JEZELI(C2>$F$1; "wysoka"; "standardowa") — znaki $ mowia Excelowi "nie zmieniaj F1 przy kopiowaniu".
#N/D!, #NAZWA? albo #ARG! — nie panikuj. To Excel mowi konkretnie co jest nie tak. #N/D! = nie znalazl wartosci (np. WYSZUKAJ.PIONOWO nie trafilo). #NAZWA? = literowka w nazwie funkcji. #ARG! = zly typ danych (np. probujesz dodac tekst do liczby). Czytaj komunikat, sprawdz argumenty, popraw.
Wskazowki dla prowadzacego
Pytania diagnostyczne (pierwsze 5 min)
- Czym rozni sie
=SUMA(D2:D200)od=D2+D3+...+D200? Sprawdza czy uczen rozumie sens zakresu i po co w ogole istnieja funkcje. - Co zwroci
=SREDNIA(D2:F2)? Sprawdza czy uczen czyta zakres poziomo vs pionowo. - Wyobraz sobie, ze w komorce H2 masz
=SREDNIA(D2:F2)/$I$1i przeciagasz w dol. Co zmieni sie w wierszu H3, a co nie? Sprawdza zrozumienie adresowania wzglednego (D2:F2 → D3:F3) i bezwzglednego ($I$1 zostaje). Punkt odniesienia dla wlasnych eksperymentow ucznia z dolarami. - Czym rozni sie LICZ.JEZELI od LICZ.WARUNKI? Sprawdza czy uczen wie kiedy potrzebny jest wiecej niz jeden warunek.
- Co zwraca WYSZUKAJ.PIONOWO gdy nie znajdzie wartosci? Sprawdza czy uczen rozumie blad
#N/D!i kolejnosc argumentow.
$ wchodza dopiero w Zadaniu 6 (porownanie do progu w $F$1) i w pytaniu diagnostycznym #3. Jezeli klasa pyta wczesniej — pokaz przyklad spoza glownego arkusza (patrz "Moment aha! #1" ponizej).Typowe bledy ucznia
| Co uczen mowi/robi | Co to znaczy | Jak poprawic |
|---|---|---|
Pisze =SUMA(D2;D200) ze srednikiem zamiast dwukropkiem |
Myli "zakres" (D2:D200) z "lista argumentow" (D2;D200) | Pokaz roznice na 3 komorkach: =SUMA(A1:A3) sumuje trzy, =SUMA(A1;A3) tylko pierwsza i trzecia |
Po skopiowaniu formuly w dol wyniki sa dziwne lub #ARG! |
Brak $ przy zakresie ktory powinien byc staly — Excel przesunal odwolanie |
Naucz F4 do przelaczania trybu adresowania. Powtarzaj: "zakres staly = dolary" |
W LICZ.WARUNKI wpisuje >=50 bez cudzyslowow |
Nie wie ze operator porownania to tekst dla funkcji | Regula: jesli warunek zawiera >, <, = lub gwiazdki — w cudzyslowach |
WYSZUKAJ.PIONOWO zwraca #N/D! chociaz uczen wie ze wartosc istnieje |
Pominal czwarty argument lub dal PRAWDA zamiast FALSZ |
Zelazna regula: czwarty argument zawsze FALSZ (lub rownowaznie 0) chyba ze dane sa posortowane (rzadko) |
| Probuje znalezc imie ucznia z MAX matematyki przez WYSZUKAJ.PIONOWO w kolumnie D | Nie wie ze WYSZUKAJ.PIONOWO szuka tylko w pierwszej kolumnie zakresu | Pokaz trick: skopiuj kolumne D do kolumny pomocniczej K, potem zakres K:M i szukaj w K, zwracaj z L/M |
Podzial 45 minut
| Czas | Aktywnosc | Co robi prowadzacy |
|---|---|---|
| 0-5 min | Pytania diagnostyczne na glos | Szybkie 5 pytan z sekcji wyzej, bez oceniania — mapowanie poziomu klasy |
| 5-15 min | Rozgrzewka: SUMA, SREDNIA, MAX na malym zakresie (10 wierszy) | Pokaz na zywo, uczniowie powtarzaja w swoich plikach. Tu mozesz wbic adresowanie $ na bocznym przykladzie (patrz Moment aha! #1) |
| 15-30 min | Wspolne rozwiazanie a) b) d) — kolumna sumy, LICZ.WARUNKI, SREDNIA.JEZELI | Dyktuj formuly krok po kroku. Po kazdej — uczen wpisuje, sprawdza wynik, dopiero potem nastepna |
| 30-40 min | Cwiczenie c) WYSZUKAJ.PIONOWO + wykres | Pokaz trick z kolumna pomocnicza K (kopia matematyki). Wykres: zbuduj tabelke 2x3, zaznacz, Wstaw > Wykres kolumnowy, pokaz przycisk "Przelacz wiersz/kolumne" |
| 40-45 min | Podsumowanie + zadanie domowe | Wybierz 2 uczniow do pokazania ekranu. Zadanie: dodaj kolumne "zdal/nie zdal" przez JEZELI |
=D2/J1, skopiuj w dol — w K3 bedzie =D3/J2 i wynik bedzie absurdalny (zakres J "uciekl"). Potem zmien K2 na =D2/$J$1, skopiuj jeszcze raz — teraz J1 zostaje stale. Uczen widzi roznice na wlasne oczy. Dolary przestaja byc abstrakcja.
Sygnaly ze klasa nie nadaza
- Wiecej niz 3 uczniow pyta "a co to dwukropek" — wroc do rozgrzewki, dopiero potem dalej
- Pojawia sie
#N/D!u wiekszosci — zatrzymaj sie, omow 4. argument WYSZUKAJ.PIONOWO na tablicy - Uczniowie kopiuja formule mysza zamiast za rog komorki — pokaz double-click na rogu (autofill do konca danych)
📝 Sprawdzian — Arkusz kalkulacyjny
Sprawdź swoją wiedzę z arkuszy kalkulacyjnych. Masz 10 minut na 5 pytań.
1. Jak zapisać adres bezwzględny dla komórki B5?
2. Która formuła zlicza komórki z wartością większą niż 100?
3. W WYSZUKAJ.PIONOWO, co oznacza ostatni argument "0"?
4. Co MUSI zawierać poprawny wykres na maturze?
5. Która funkcja oblicza średnią dla komórek spełniających warunek?
🔥 Rozgrzewka — Bazy danych SQL
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT. Pamiętaj: WHERE przed GROUP BY, HAVING po GROUP BY!WHERE filtruje wiersze PRZED grupowaniem. HAVING filtruje grupy PO grupowaniu. Z HAVING używasz funkcji agregujących (COUNT, SUM, AVG).INNER JOIN — tylko pasujące wiersze z obu tabel. LEFT JOIN — wszystko z lewej + pasujące z prawej. Na maturze najczęściej INNER JOIN.ORDER BY cena DESC. Domyślnie jest ASC (rosnąco). Możesz sortować po wielu kolumnach: ORDER BY kategoria ASC, cena DESC.SELECT kategoria, COUNT(*) FROM produkty GROUP BY kategoria. COUNT(*) zlicza wszystkie wiersze, COUNT(kolumna) — tylko niepuste.#04 — Bazy danych SQL
Typ zadania: Część 1 (SQL na papierze) + Część 2 (komputer) · Czas: ~40 min
Kontekst
Baza danych biblioteki zawiera 3 tabele:
-- Tabela: ksiazki
-- id INTEGER PRIMARY KEY
-- tytul TEXT
-- autor_id INTEGER (FK → autorzy.id)
-- rok_wydania INTEGER
-- gatunek TEXT
-- rating REAL
-- Tabela: autorzy
-- id INTEGER PRIMARY KEY
-- imie TEXT
-- nazwisko TEXT
-- kraj TEXT
-- Tabela: wypozyczenia
-- id INTEGER PRIMARY KEY
-- ksiazka_id INTEGER (FK → ksiazki.id)
-- data_wyp TEXT
-- data_zwrotu TEXT (NULL = niezwrócona)
Polecenia
- a) Wyświetl tytuły książek i nazwiska autorów, posortowane alfabetycznie po nazwisku.
- b) Ile książek napisał każdy autor? Wyświetl imię, nazwisko i liczbę książek. Tylko autorzy z ≥ 3 książkami.
- c) Wyświetl tytuły książek, które nigdy nie zostały wypożyczone.
- d) Ile książek z gatunku 'kryminał' zostało wypożyczonych w 2024 roku?
- e) Wyświetl tytuł i autora książki z najwyższym ratingiem.
- f) Stwórz widok (VIEW) pokazujący dla każdego gatunku: liczbę książek, średni rating i liczbę wypożyczeń.
#04 — Rozwiązanie krok po kroku
autor_id w tabeli ksiazki wskazuje na id w tabeli autorzy. Tak samo ksiazka_id w wypozyczenia wskazuje na id w ksiazki.autorzy (id) --1:N--> ksiazki (autor_id)
ksiazki (id) --1:N--> wypozyczenia (ksiazka_id)
Relacje:
- Jeden autor ma wiele ksiazek
- Jedna ksiazka ma wiele wypozyczen
id z jednej tabeli i cos_id z drugiej.JOIN po autor_id.-- laczymy ksiazki z autorami po kluczu obcym, sortujemy po nazwisku
SELECT ksiazki.tytul, autorzy.imie, autorzy.nazwisko
FROM ksiazki
JOIN autorzy ON ksiazki.autor_id = autorzy.id
ORDER BY autorzy.nazwisko;
-
SELECT wybiera 3 kolumny do wyswietlenia-
FROM ksiazki tabela glowna-
JOIN autorzy ON ... dolaczamy autorow po kluczu-
ORDER BY nazwisko sortujemy alfabetycznie po nazwiskuHAVING.-- grupowanie po id autora (klucz glowny wystarczy), filtr na grupach: minimum 3 ksiazki
SELECT autorzy.imie, autorzy.nazwisko, COUNT(*) AS ile_ksiazek
FROM autorzy
JOIN ksiazki ON autorzy.id = ksiazki.autor_id
GROUP BY autorzy.id
HAVING COUNT(*) >= 3
ORDER BY ile_ksiazek DESC;
WHERE filtruje pojedyncze wiersze ZANIM zgrupujemy. HAVING filtruje gotowe grupy PO grupowaniu. Tu chcemy odrzucic autorow z mniej niz 3 ksiazkami, to filtrowanie grup, wiec HAVING.GROUP BY autorzy.id? W SQLite id to klucz glowny tabeli autorzy, wiec jednoznacznie identyfikuje wiersz. Kolumny imie i nazwisko sa od niego zalezne i SQLite je dopuszcza w SELECT bez wymieniania w GROUP BY. To najprostsza forma.ksiazka_id wszystkich ksiazek, ktore kiedykolwiek wypozyczono. Potem zewnetrzne zapytanie wybiera te ksiazki, ktorych id NIE MA na tej liscie.-- podzapytanie daje liste id ksiazek z wypozyczen,
-- zewnetrzne wybiera te ktorych tam nie ma
SELECT tytul
FROM ksiazki
WHERE id NOT IN (
SELECT ksiazka_id
FROM wypozyczenia
);
SELECT.ksiazka_id w tabeli wypozyczenia nigdy nie jest NULL (kazde wypozyczenie odnosi sie do konkretnej ksiazki). Gdyby byly tam NULL-e, NOT IN zwrocilby pusty wynik, bo NULL "psuje" porownanie. W bezpiecznej wersji dopisalibysmy WHERE ksiazka_id IS NOT NULL w podzapytaniu.wypozyczenia z ksiazki zeby dostac sie do gatunku. W WHERE dwa warunki polaczone AND: gatunek to kryminal i data wypozyczenia zaczyna sie od "2024".-- JOIN po ksiazka_id, filtr po gatunku i roku wypozyczenia
SELECT COUNT(*) AS ile_wypozyczen
FROM wypozyczenia
JOIN ksiazki ON wypozyczenia.ksiazka_id = ksiazki.id
WHERE ksiazki.gatunek = 'kryminal'
AND wypozyczenia.data_wyp LIKE '2024%';
LIKE '2024%'? Daty sa w formacie YYYY-MM-DD, wiec kazda data z 2024 roku zaczyna sie od "2024". Znak % oznacza "cokolwiek dalej".-- podzapytanie zwraca jedna liczbe (MAX rating),
-- zewnetrzne SELECT bierze ksiazke ktora ma dokladnie taki rating
SELECT ksiazki.tytul, autorzy.imie, autorzy.nazwisko, ksiazki.rating
FROM ksiazki
JOIN autorzy ON ksiazki.autor_id = autorzy.id
WHERE ksiazki.rating = (SELECT MAX(rating) FROM ksiazki);
ORDER BY rating DESC LIMIT 1? Bo gdyby dwie ksiazki mialy ten sam najwyzszy rating, LIMIT 1 pokazalby tylko jedna. Podzapytanie z MAX pokaze wszystkie remisujace.ORDER BY kolumna DESC LIMIT 1. To wzorzec do zapamietania, przyda sie w cwiczeniach.Zapytanie 1: liczba ksiazek i sredni rating per gatunek (tylko z tabeli ksiazki)
-- grupujemy ksiazki po gatunku, liczymy ile ich jest i jaka jest srednia ratingu
SELECT
gatunek,
COUNT(*) AS ile_ksiazek,
ROUND(AVG(rating), 2) AS sredni_rating
FROM ksiazki
GROUP BY gatunek
ORDER BY gatunek;
Zapytanie 2: liczba wypozyczen per gatunek (laczenie z wypozyczenia)
-- LEFT JOIN zeby uwzglednic gatunki bez ani jednego wypozyczenia
SELECT
ksiazki.gatunek,
COUNT(wypozyczenia.id) AS ile_wypozyczen
FROM ksiazki
LEFT JOIN wypozyczenia ON ksiazki.id = wypozyczenia.ksiazka_id
GROUP BY ksiazki.gatunek
ORDER BY ile_wypozyczen DESC;
-
COUNT(*) liczy wszystkie wiersze w grupie-
AVG(rating) srednia z ratingow-
ROUND(..., 2) zaokragla do 2 miejsc po przecinku-
COUNT(wypozyczenia.id) liczy tylko te wypozyczenia, gdzie id nie jest NULL (po LEFT JOIN gatunek bez wypozyczen ma tam NULL, wiec liczy sie jako 0)Cwiczenia do samodzielnego rozwiazania
Pracujesz na tej samej bazie biblioteki: autorzy(id, imie, nazwisko, kraj), ksiazki(id, tytul, autor_id, gatunek, rating), wypozyczenia(id, ksiazka_id, data_wyp, data_zwr). Kazde zapytanie zapisz, uruchom w sqlite3 i sprawdz wynik.
Pokaz odpowiedz
-- filtr po kraju
SELECT imie, nazwisko
FROM autorzy
WHERE kraj = 'Polska';
Pokaz odpowiedz
-- WHERE + ORDER BY malejaco
SELECT tytul, rating
FROM ksiazki
WHERE gatunek = 'fantasy'
ORDER BY rating DESC;
Pokaz odpowiedz
-- grupowanie po gatunku, liczenie wierszy
SELECT gatunek, COUNT(*) AS ile
FROM ksiazki
GROUP BY gatunek;
Pokaz odpowiedz
-- laczymy ksiazki z autorami po autor_id = autorzy.id
SELECT ksiazki.tytul, autorzy.kraj
FROM ksiazki
JOIN autorzy ON ksiazki.autor_id = autorzy.id;
Pokaz odpowiedz
-- JOIN + GROUP BY po autorze + HAVING na agregacie
SELECT autorzy.nazwisko, AVG(ksiazki.rating) AS sredni
FROM autorzy
JOIN ksiazki ON ksiazki.autor_id = autorzy.id
GROUP BY autorzy.id
HAVING AVG(ksiazki.rating) > 4.0;
Pokaz odpowiedz
-- wzorzec "top 1": grupujemy po ksiazce, sortujemy malejaco, bierzemy pierwsza
-- (taki sam pattern jak w kroku 5 "Wzorzec top 1")
SELECT ksiazki.tytul, COUNT(*) AS ile_wyp
FROM ksiazki
JOIN wypozyczenia ON wypozyczenia.ksiazka_id = ksiazki.id
GROUP BY ksiazki.id
ORDER BY ile_wyp DESC
LIMIT 1;
SELECT * FROM tabela LIMIT 5; zeby zobaczyc dane.
Wskazowki dla prowadzacego
Pytania diagnostyczne (5 min na start)
Zadaj te pytania zanim wejdziesz w cwiczenia. Odpowiedzi pokaza ci czy uczen ma fundament, czy musisz cofnac sie do podstaw SELECT-a.
- "Co zwroci
SELECT * FROM ksiazki WHERE rating > 4?" sprawdza czy rozumie filtrowanie wierszy. - "Po co jest klucz obcy
autor_idw tabeli ksiazki?" sprawdza model relacyjny. Jesli nie umie odpowiedziec, narysuj dwie tabele na kartce i pociagnij strzalke. - "Jaka jest roznica miedzy
COUNT(*)aCOUNT(autor_id)?" wykrywa luke z NULL-ami i agregacjami. - "Co robi GROUP BY i kiedy go uzywasz?" kluczowe dla zadan b, d, f. Jesli uczen mowi "grupuje dane", dopytaj "po czym konkretnie i co dostajesz na wyjsciu".
- "Czym rozni sie INNER JOIN od LEFT JOIN?" jesli odpowie poprawnie, mozesz przyspieszyc zadanie a; jesli nie, poswiec 5 min na rysunek dwoch kol (zbiory).
Typowe bledy ucznia i jak je prostowac
| Co uczen mowi/pisze | Co to znaczy | Jak poprawic |
|---|---|---|
WHERE COUNT(*) >= 3 |
Nie rozroznia WHERE i HAVING. | "WHERE dziala na wierszach przed grupowaniem, HAVING na grupach po. Agregat (COUNT, SUM, AVG) nie istnieje przed GROUP BY." |
| JOIN bez warunku ON | Robi iloczyn kartezjanski, nie wie o tym. | Pokaz na 3 autorach i 5 ksiazkach: bez ON dostaniesz 15 wierszy. Z ON tylko poprawne pary. |
| SELECT z kolumna i agregatem bez GROUP BY | Myli poziom: "chce nazwisko i ilosc ksiazek tego autora". | Zasada: kazda kolumna spoza agregatu musi byc w GROUP BY (lub funkcyjnie zalezna od klucza w GROUP BY, jak w SQLite). Powtorz na trzech przykladach. |
| NOT IN z podzapytaniem zwracajacym NULL | Wynik pusty, nie wie dlaczego. | "NOT IN porownuje z kazdym elementem listy. Jesli choc jeden to NULL, calosc jest NULL = false." Tu mozna dodac WHERE ksiazka_id IS NOT NULL w podzapytaniu (jak w info-box w kroku 3). |
| "Wezme MAX(rating) i tytul jednym SELECT-em" | Nie widzi ze potrzeba dwoch krokow: najpierw znajdz wartosc, potem znajdz wiersz. | Wprowadz wzorzec WHERE rating = (SELECT MAX(rating) FROM ksiazki). Wytlumacz ze podzapytanie zwraca jedna liczbe, ktora wstawia sie w miejsce wartosci. |
| "Top 1": uzywa MAX zamiast ORDER BY + LIMIT | Nie zna wzorca "posortuj malejaco i wez pierwszy" gdy zlicza wystapienia. | Pokaz wzorzec: GROUP BY x ORDER BY COUNT(*) DESC LIMIT 1. To inny przypadek niz MAX z kolumny, bo tutaj wartosc do porownania powstaje dopiero przez zliczenie. |
Podzial lekcji 45 minut
| Czas | Co robisz | Cel |
|---|---|---|
| 0-5 min | Rozgrzewka: 5 pytan diagnostycznych na glos. | Zmapuj luki, zdecyduj tempo. |
| 5-15 min | Wspolne zadanie a (JOIN) na tablicy/ekranie. Narysuj dwie tabele, pokaz fizycznie laczenie po autor_id. | Utrwal mechanike JOIN. |
| 15-30 min | Zadania b i d wspolnie (GROUP BY + HAVING, GROUP BY + WHERE z data). | Wbij roznice WHERE vs HAVING. |
| 30-42 min | Uczen samodzielnie: c, e, f. Ty chodzisz i pomagasz. | Sprawdz czy potrafi sam. |
| 42-45 min | Podsumowanie: jedno zdanie kazde, "WHERE to...", "HAVING to...", "JOIN to...", "podzapytanie to...". | Cement w pamieci. |
WHERE rating > 4 (najpierw odfiltruj slabe ksiazki, potem grupuj), raz z HAVING AVG(rating) > 4 (grupuj wszystkie, potem zostaw grupy z wysoka srednia). Inne wyniki, ta sama tabela. Uczen zobaczy ze to dwa rozne pytania biznesowe.
SELECT MAX(rating) FROM ksiazki (dostajesz 9.8). Potem wpisz recznie WHERE rating = 9.8. Dziala. Potem powiedz: "Zamiast wpisywac 9.8 recznie, wstaw tam to pierwsze zapytanie w nawiasach." Uczen widzi ze podzapytanie to po prostu sposob na uniknicie hardkodowania wartosci.
📝 Sprawdzian — Bazy danych SQL
Sprawdź swoją wiedzę z SQL. Masz 10 minut na 5 pytań.
1. Jaka jest prawidłowa kolejność klauzul w SELECT?
2. Kiedy używamy HAVING zamiast WHERE?
3. Co zwróci COUNT(*) dla tabeli z 10 wierszami (w tym 2 z NULL w kolumnie X)?
4. Jak posortować wyniki od najwyższej ceny?
5. Co robi INNER JOIN?
🔥 Rozgrzewka — Kody produktów
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
kod = "5901234"?kod[0] to "5" — ale jako napis, nie liczba. Żeby liczyć na tym arytmetykę: int(kod[0]). Ostatni znak: kod[len(kod)-1] albo krócej kod[-1]."907"?suma = 0, potem for i in range(len(kod)): suma = suma + int(kod[i]). Wynik: 9 + 0 + 7 = 16.% (modulo) i po co jest w cyfrze kontrolnej?% daje resztę z dzielenia: 47 % 10 to 7. Cyfra kontrolna odpowiada na pytanie „ile brakuje do pełnej dziesiątki", więc modulo 10 jest sercem całego algorytmu.i ze znakiem len(kod)-1-i w pętli. Jedna niezgodna para przekreśla cały napis. Skrót kod == kod[::-1] też działa, ale pętlę łatwiej obronić przy omawianiu rozwiązania.if x not in ile: ile[x] = 0, a potem ile[x] = ile[x] + 1. Klucz to element, wartość to liczba wystąpień.#05 — Programowanie Python — kody produktów i cyfra kontrolna
Typ zadania: Część 2 — komputer · Czas: ~45 min · Python
Kontekst
Hurtownia dostała plik kody.txt z 500 kodami produktów — jeden kod w każdym wierszu. Poprawny kod to EAN-13: dokładnie 13 cyfr, w którym ostatnia cyfra (kontrolna) jest wyliczana z dwunastu poprzednich. Część kodów przyszła uszkodzona — mają złą długość albo nie zgadza się cyfra kontrolna.
5881399869875
9730911417079
3645568151886
7294055761693
1689768469787
...
Jak wylicza się cyfrę kontrolną EAN-13
Bierzesz 12 pierwszych cyfr. Cyfry na pozycjach nieparzystych (1., 3., 5. …) mnożysz przez 1, na parzystych (2., 4., 6. …) przez 3. Sumujesz. Cyfra kontrolna to tyle, ile tej sumie brakuje do pełnej dziesiątki:
kod: 5 9 0 1 2 3 4 5 6 7 8 9 ?
wagi: 1 3 1 3 1 3 1 3 1 3 1 3
iloczyny: 5 +27+ 0 + 3 + 2 + 9 + 4 +15 + 6 +21 + 8 +27 = 127
127 % 10 = 7 -> brakuje 3 -> cyfra kontrolna = 3
Polecenia
- Podpunkt a) Ile kodów ma poprawną długość (13 cyfr), a ile ma inną długość?
- Podpunkt b) Ile kodów ma sumę cyfr podzielną przez 7? Podaj też kod o największej sumie cyfr i tę sumę.
- Podpunkt c) Wśród kodów 13-cyfrowych: ile ma poprawną cyfrę kontrolną, a ile błędną?
- Podpunkt d) Ile kodów to palindromy (czytane od tyłu dają ten sam ciąg cyfr)? Wypisz je.
- Podpunkt e) Czy któryś kod powtarza się w pliku? Podaj powtórzone kody i liczbę wystąpień.
- Podpunkt f) Wyniki wszystkich podpunktów zapisz do pliku
raport.txt.
Skąd wziąć plik z danymi
Najszybciej: pobierz gotowy kody.txt i zapisz w folderze projektu.
Albo wygeneruj sam — uruchom raz poniższy skrypt w pustym folderze. Ziarno losowania jest ustalone (seed(2026)), więc dostaniesz bit w bit ten sam plik co powyżej, a więc i te same odpowiedzi.
import random
random.seed(2026)
def cyfra_kontrolna(dwanascie):
suma = 0
for i in range(12):
c = int(dwanascie[i])
if i % 2 == 0:
suma = suma + c
else:
suma = suma + 3 * c
return str((10 - suma % 10) % 10)
kody = []
for n in range(500):
los = random.randint(1, 100)
if los <= 6: # 6% - zla dlugosc
dlugosc = 12 + 2 * random.randint(0, 1) # 12 albo 14 cyfr
kod = ""
for i in range(dlugosc):
kod = kod + str(random.randint(0, 9))
else:
baza = ""
for i in range(12):
baza = baza + str(random.randint(0, 9))
k = cyfra_kontrolna(baza)
if random.randint(1, 100) <= 18: # 18% - zepsuta cyfra kontrolna
k = str((int(k) + random.randint(1, 9)) % 10)
kod = baza + k
kody.append(kod)
kody[42] = "9876556556789"
kody[137] = "1234554554321"
kody[268] = "2002002002002"
kody[455] = "1010110110101"
kody[17] = kody[300]
kody[421] = kody[300]
kody[88] = kody[199]
f = open("kody.txt", "w", encoding="utf-8")
for i in range(len(kody)):
f.write(kody[i] + "\n")
f.close()
print("zapisano " + str(len(kody)) + " kodow")
Na lekcji: zacznij od podpunktu c) na kartce — policz cyfrę kontrolną ręcznie dla jednego kodu z pliku. Dopiero gdy uczeń rozumie algorytm na papierze, przepisujecie go na pętlę. Podpunkty a), b), d) są rozgrzewką pod niego, e) domyka temat słownika-licznika.
#05 — Rozwiązanie krok po kroku
int(kod), kod zaczynający się od zera straciłby to zero i cyfra kontrolna przestałaby się zgadzać.kody = []
f = open("kody.txt", "r", encoding="utf-8")
for linia in f:
linia = linia.strip() # usuwamy znak nowej linii
if linia != "": # pomijamy pusty wiersz na koncu pliku
kody.append(linia)
f.close()
print("Wczytano " + str(len(kody)) + " kodow")
print("Pierwszy kod: " + kody[0])
print("Ostatni kod: " + kody[len(kody) - 1])
Wczytano 500 kodow, pierwszy kod to 5881399869875. Jeśli masz inną liczbę — plik wygenerował się inaczej, uruchom generator jeszcze raz w pustym folderze.len(kod) zwraca liczbę znaków napisu — dla kodu to po prostu liczba cyfr.poprawna_dlugosc = 0
zla_dlugosc = 0
for i in range(len(kody)):
if len(kody[i]) == 13:
poprawna_dlugosc = poprawna_dlugosc + 1
else:
zla_dlugosc = zla_dlugosc + 1
print("a) 13 cyfr: " + str(poprawna_dlugosc))
print(" inna dlugosc: " + str(zla_dlugosc))
ile_podzielnych = 0
najwieksza_suma = 0
kod_rekordzisty = ""
for i in range(len(kody)):
kod = kody[i]
suma = 0
for j in range(len(kod)):
suma = suma + int(kod[j]) # int(), bo kod[j] to napis
if suma % 7 == 0:
ile_podzielnych = ile_podzielnych + 1
if suma > najwieksza_suma:
najwieksza_suma = suma
kod_rekordzisty = kod
print("b) suma cyfr podzielna przez 7: " + str(ile_podzielnych))
print(" najwieksza suma cyfr: " + str(najwieksza_suma) + " (kod " + kod_rekordzisty + ")")
7849996179788 — suma cyfr 92.suma = suma + kod[j] bez int() nie wywali błędu od razu — Python skleja napisy. Awaria przychodzi dopiero przy suma % 7. Zawsze int() na granicy między napisem a arytmetyką.i % 2 == 0 to pozycje 1., 3., 5. … (Python liczy od zera) — waga 1, pozostałe — waga 3.def cyfra_kontrolna(kod):
suma = 0
for i in range(12): # tylko 12 pierwszych cyfr
c = int(kod[i])
if i % 2 == 0:
suma = suma + c # pozycje 1, 3, 5, ... waga 1
else:
suma = suma + 3 * c # pozycje 2, 4, 6, ... waga 3
return (10 - suma % 10) % 10
poprawne = 0
bledne = 0
for i in range(len(kody)):
kod = kody[i]
if len(kod) == 13: # tylko kody wlasciwej dlugosci
if cyfra_kontrolna(kod) == int(kod[12]):
poprawne = poprawne + 1
else:
bledne = bledne + 1
print("c) poprawna cyfra kontrolna: " + str(poprawne))
print(" bledna cyfra kontrolna: " + str(bledne))
% 10: gdy suma kończy się zerem, 10 - 0 daje 10 — a cyfra kontrolna musi być jedną cyfrą. Drugie % 10 zamienia to 10 z powrotem na 0. Bez tego mniej więcej co dziesiąty poprawny kod zostanie odrzucony jako błędny.5881399869875, policz ręcznie sumę ważoną z 12 pierwszych cyfr i porównaj wynik z ostatnią cyfrą. Jeśli ręcznie i programowo wychodzi to samo — algorytm masz opanowany.czy_palindrom = True i zbijamy ją na False.palindromy = []
for i in range(len(kody)):
kod = kody[i]
czy_palindrom = True
for j in range(len(kod)):
if kod[j] != kod[len(kod) - 1 - j]:
czy_palindrom = False
if czy_palindrom:
palindromy.append(kod)
print("d) palindromow: " + str(len(palindromy)))
for i in range(len(palindromy)):
print(" " + palindromy[i])
9876556556789, 1234554554321, 2002002002002, 1010110110101. Żaden z nich nie ma poprawnej cyfry kontrolnej, co ładnie pokazuje, że „równy" kod i „poprawny" kod to dwie różne rzeczy.range(len(kod) // 2). Przy 500 kodach różnicy w czasie nie zobaczysz, ale warto o tym powiedzieć przy omawianiu.ile_razy = {}
for i in range(len(kody)):
kod = kody[i]
if kod not in ile_razy:
ile_razy[kod] = 0
ile_razy[kod] = ile_razy[kod] + 1
powtorzone = []
for kod in ile_razy:
if ile_razy[kod] > 1:
powtorzone.append(kod)
powtorzone.sort()
print("e) powtorzonych kodow: " + str(len(powtorzone)))
for i in range(len(powtorzone)):
print(" " + powtorzone[i] + " wystepuje " + str(ile_razy[powtorzone[i]]) + " razy")
print(" unikalnych kodow: " + str(len(ile_razy)))
4321445653382 (3 razy) i 5199559392988 (2 razy). Unikalnych kodów jest 497 przy 500 wierszach w pliku.open(..., "w"), seria write(), jeden close(). Każdy write() wymaga własnego "\n" — w odróżnieniu od print(), który nową linię dokleja sam.f = open("raport.txt", "w", encoding="utf-8")
f.write("RAPORT - kody produktow\n")
f.write("Kodow w pliku: " + str(len(kody)) + "\n")
f.write("a) 13 cyfr: " + str(poprawna_dlugosc) + ", inna dlugosc: " + str(zla_dlugosc) + "\n")
f.write("b) suma cyfr podzielna przez 7: " + str(ile_podzielnych) + "\n")
f.write("b) najwieksza suma cyfr: " + str(najwieksza_suma) + " (kod " + kod_rekordzisty + ")\n")
f.write("c) poprawna cyfra kontrolna: " + str(poprawne) + ", bledna: " + str(bledne) + "\n")
f.write("d) palindromy: " + str(len(palindromy)) + "\n")
for i in range(len(palindromy)):
f.write(" " + palindromy[i] + "\n")
f.write("e) powtorzone kody: " + str(len(powtorzone)) + "\n")
for i in range(len(powtorzone)):
f.write(" " + powtorzone[i] + " x" + str(ile_razy[powtorzone[i]]) + "\n")
f.close()
print("Zapisano raport.txt")
"w" nadpisuje plik przy każdym uruchomieniu — poprzedni raport znika. Gdybyś chciał dopisywać kolejne uruchomienia, użyj "a". Na maturze prawie zawsze chodzi o "w": sprawdzający czyta jeden, aktualny wynik.rozwiazanie.py w tym samym folderze co kody.txt i uruchom.# --- krok 0: wczytanie ---
kody = []
f = open("kody.txt", "r", encoding="utf-8")
for linia in f:
linia = linia.strip()
if linia != "":
kody.append(linia)
f.close()
print("Wczytano " + str(len(kody)) + " kodow")
# --- a) dlugosc kodow ---
poprawna_dlugosc = 0
zla_dlugosc = 0
for i in range(len(kody)):
if len(kody[i]) == 13:
poprawna_dlugosc = poprawna_dlugosc + 1
else:
zla_dlugosc = zla_dlugosc + 1
print("a) 13 cyfr: " + str(poprawna_dlugosc) + ", inna dlugosc: " + str(zla_dlugosc))
# --- b) suma cyfr ---
ile_podzielnych = 0
najwieksza_suma = 0
kod_rekordzisty = ""
for i in range(len(kody)):
kod = kody[i]
suma = 0
for j in range(len(kod)):
suma = suma + int(kod[j])
if suma % 7 == 0:
ile_podzielnych = ile_podzielnych + 1
if suma > najwieksza_suma:
najwieksza_suma = suma
kod_rekordzisty = kod
print("b) suma cyfr podzielna przez 7: " + str(ile_podzielnych))
print(" najwieksza suma cyfr: " + str(najwieksza_suma) + " (kod " + kod_rekordzisty + ")")
# --- c) cyfra kontrolna ---
def cyfra_kontrolna(kod):
suma = 0
for i in range(12):
c = int(kod[i])
if i % 2 == 0:
suma = suma + c
else:
suma = suma + 3 * c
return (10 - suma % 10) % 10
poprawne = 0
bledne = 0
for i in range(len(kody)):
kod = kody[i]
if len(kod) == 13:
if cyfra_kontrolna(kod) == int(kod[12]):
poprawne = poprawne + 1
else:
bledne = bledne + 1
print("c) poprawna cyfra kontrolna: " + str(poprawne) + ", bledna: " + str(bledne))
# --- d) palindromy ---
palindromy = []
for i in range(len(kody)):
kod = kody[i]
czy_palindrom = True
for j in range(len(kod)):
if kod[j] != kod[len(kod) - 1 - j]:
czy_palindrom = False
if czy_palindrom:
palindromy.append(kod)
print("d) palindromow: " + str(len(palindromy)))
for i in range(len(palindromy)):
print(" " + palindromy[i])
# --- e) duplikaty ---
ile_razy = {}
for i in range(len(kody)):
kod = kody[i]
if kod not in ile_razy:
ile_razy[kod] = 0
ile_razy[kod] = ile_razy[kod] + 1
powtorzone = []
for kod in ile_razy:
if ile_razy[kod] > 1:
powtorzone.append(kod)
powtorzone.sort()
print("e) powtorzonych kodow: " + str(len(powtorzone)))
for i in range(len(powtorzone)):
print(" " + powtorzone[i] + " wystepuje " + str(ile_razy[powtorzone[i]]) + " razy")
# --- f) raport do pliku ---
f = open("raport.txt", "w", encoding="utf-8")
f.write("RAPORT - kody produktow\n")
f.write("Kodow w pliku: " + str(len(kody)) + "\n")
f.write("a) 13 cyfr: " + str(poprawna_dlugosc) + ", inna dlugosc: " + str(zla_dlugosc) + "\n")
f.write("b) suma cyfr podzielna przez 7: " + str(ile_podzielnych) + "\n")
f.write("b) najwieksza suma cyfr: " + str(najwieksza_suma) + " (kod " + kod_rekordzisty + ")\n")
f.write("c) poprawna cyfra kontrolna: " + str(poprawne) + ", bledna: " + str(bledne) + "\n")
f.write("d) palindromy: " + str(len(palindromy)) + "\n")
for i in range(len(palindromy)):
f.write(" " + palindromy[i] + "\n")
f.write("e) powtorzone kody: " + str(len(powtorzone)) + "\n")
for i in range(len(powtorzone)):
f.write(" " + powtorzone[i] + " x" + str(ile_razy[powtorzone[i]]) + "\n")
f.close()
print("Zapisano raport.txt")
Wczytano 500 kodow
a) 13 cyfr: 476, inna dlugosc: 24
b) suma cyfr podzielna przez 7: 74
najwieksza suma cyfr: 92 (kod 7849996179788)
c) poprawna cyfra kontrolna: 364, bledna: 112
d) palindromow: 4
9876556556789
1234554554321
2002002002002
1010110110101
e) powtorzonych kodow: 2
4321445653382 wystepuje 3 razy
5199559392988 wystepuje 2 razy
Zapisano raport.txt
Sama matura z informatyki nie wymaga interfejsu graficznego — tam wystarczy konsola i plik wynikowy. Ale na INF.04 okno w PyQt6 jest obowiązkowe, a Ty zdajesz oba egzaminy. Ten krok pokazuje najważniejszą rzecz w praktyce: logika się nie zmienia. Te same funkcje, te same pętle — zmienia się tylko to, dokąd trafia wynik.
| Wersja konsolowa | Wersja z oknem |
|---|---|
print(tekst) | self.listaWynikow.addItem(tekst) |
| program liczy i kończy się | program czeka na kliknięcie użytkownika |
| wynik leci od razu | funkcja zwraca listę napisów, okno je wyświetla |
Dlatego funkcja analizuj() niżej nie drukuje niczego — zbiera gotowe wiersze do listy i oddaje je na zewnątrz. To jest ta jedna zmiana, którą trzeba zrobić w głowie: oddzielić liczenie od pokazywania.
Struktura projektu — trzy pliki z szablonu Qt Creatora
Qt Creator zakłada projekt Qt for Python — Widgets (w drzewku widnieje jako „PySide Widgets Project" — to tylko nazwa szablonu, kod piszemy w całości w PyQt6). Dostajesz trzy pliki, każdy z jednym zadaniem:
| Plik | Co w nim jest | Czy dotykasz |
|---|---|---|
pyproject.toml | metadane projektu dla Qt Creatora | nie |
form.ui | wygląd okna — układasz go myszką w Designerze | tak, w Designerze |
mainwindow.py | logika zadania i podpięcie przycisków | tak, tu piszesz |
form.ui do Pythona: albo kompilujesz formularz raz komendą pyuic6 (opisane w module „9. PyQt6 (GUI)"), albo wczytujesz go w locie przez uic.loadUi — tak jak niżej. Druga droga jest wygodniejsza na lekcji: poprawiasz formularz w Designerze, zapisujesz, uruchamiasz — bez kroku kompilacji. Na egzaminie obie są poprawne.1. form.ui — cztery widżety i ich nazwy
W Designerze przeciągasz na okno cztery elementy i każdemu ustawiasz objectName w panelu po prawej. Ta nazwa to jedyny łącznik między formularzem a kodem — w Pythonie odwołujesz się dokładnie tak, jak nazwiesz widżet tutaj.
| Widżet | objectName | Do czego służy |
|---|---|---|
| QLabel | etykietaStatus | napis „Wczytano N…" |
| QPushButton | przyciskWczytaj | uruchamia obliczenia |
| QListWidget | listaWynikow | pokazuje wyniki zamiast print() |
| QPushButton | przyciskZapisz | zapisuje plik wynikowy |
Na koniec zaznacz okno i wybierz Lay out Vertically, żeby widżety układały się jeden pod drugim przy zmianie rozmiaru.
form.ui, który już masz w projekcie — nie twórz nowego. Otwórz go dwuklikiem w drzewku Qt Creatora; plik otworzy się w Designerze i tam przeciągasz widżety. Plik .ui to zwykły XML, więc gdybyś chciał zobaczyć, jak wygląda gotowy formularz w środku albo wkleić go zamiast klikać — to jest jego zawartość:<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>560</width>
<height>420</height>
</rect>
</property>
<property name="windowTitle">
<string>Analiza kodow produktow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="ukladGlowny">
<item>
<widget class="QLabel" name="etykietaStatus">
<property name="text">
<string>Nie wczytano jeszcze pliku</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="przyciskWczytaj">
<property name="text">
<string>Wczytaj kody.txt</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="listaWynikow"/>
</item>
<item>
<widget class="QPushButton" name="przyciskZapisz">
<property name="text">
<string>Zapisz raport.txt</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
self." — i nie podpowie.
Przy uic.loadUi() widżety powstają dopiero w czasie działania programu. Edytor czyta wyłącznie kod Pythona, nie zagląda do form.ui, więc nie ma skąd wiedzieć, że za chwilę pojawi się przyciskWczytaj. Autouzupełnianie będzie puste, a kod i tak zadziała po uruchomieniu — piszesz nazwę z pamięci, litera w literę tak, jak brzmi objectName w Designerze.
Jeśli chcesz mieć podpowiadanie, skompiluj formularz do Pythona. W terminalu, w folderze projektu:
python3 -m PyQt6.uic.pyuic form.ui -o ui_form.py
Powstaje plik ui_form.py z klasą Ui_MainWindow — zwykły kod Pythona, który edytor potrafi przeczytać. Od tej chwili po self.ui. dostajesz pełną listę widżetów.
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow
from ui_form import Ui_MainWindow
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.przyciskWczytaj.clicked.connect(self.wczytaj)
def wczytaj(self):
self.ui.etykietaStatus.setText("Kliknieto Wczytaj")
self.ui.listaWynikow.clear()
self.ui.listaWynikow.addItem("pozycja testowa")
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = MainWindow()
widget.show()
sys.exit(app.exec())
Różnica, która najczęściej myli: przy uic.loadUi(...) widżety siedzą bezpośrednio na oknie — self.przyciskWczytaj. Po kompilacji pyuic6 siedzą w obiekcie formularza — self.ui.przyciskWczytaj. Wymieszanie obu zapisów daje AttributeError przy pierwszym kliknięciu.
uic.loadUi | pyuic6 + ui_form.py | |
|---|---|---|
| dostęp do widżetu | self.przyciskWczytaj | self.ui.przyciskWczytaj |
| podpowiadanie w edytorze | nie ma | jest |
| po zmianie formularza | nic nie robisz | kompilujesz ponownie |
| plików w projekcie | bez zmian | dochodzi ui_form.py (nie edytuje się go ręcznie) |
Obie drogi są poprawne na egzaminie. Przy nauce wygodniejsze jest loadUi (jeden plik mniej), przy większym formularzu — kompilacja, bo edytor pilnuje literówek za Ciebie.
2. mainwindow.py — logika i podpięcie
import sys
from pathlib import Path
from PyQt6 import uic
from PyQt6.QtWidgets import QApplication, QMainWindow, QMessageBox
FOLDER = Path(__file__).parent
def wczytaj_kody(nazwa_pliku):
kody = []
f = open(FOLDER / nazwa_pliku, "r", encoding="utf-8")
for linia in f:
linia = linia.strip()
if linia != "":
kody.append(linia)
f.close()
return kody
def cyfra_kontrolna(kod):
suma = 0
for i in range(12):
c = int(kod[i])
if i % 2 == 0:
suma = suma + c
else:
suma = suma + 3 * c
return (10 - suma % 10) % 10
def analizuj(kody):
wyniki = []
poprawna_dlugosc = 0
zla_dlugosc = 0
for i in range(len(kody)):
if len(kody[i]) == 13:
poprawna_dlugosc = poprawna_dlugosc + 1
else:
zla_dlugosc = zla_dlugosc + 1
wyniki.append("a) 13 cyfr: " + str(poprawna_dlugosc) + ", inna dlugosc: " + str(zla_dlugosc))
ile_podzielnych = 0
najwieksza_suma = 0
kod_rekordzisty = ""
for i in range(len(kody)):
kod = kody[i]
suma = 0
for j in range(len(kod)):
suma = suma + int(kod[j])
if suma % 7 == 0:
ile_podzielnych = ile_podzielnych + 1
if suma > najwieksza_suma:
najwieksza_suma = suma
kod_rekordzisty = kod
wyniki.append("b) suma cyfr podzielna przez 7: " + str(ile_podzielnych))
wyniki.append("b) najwieksza suma cyfr: " + str(najwieksza_suma) + " (kod " + kod_rekordzisty + ")")
poprawne = 0
bledne = 0
for i in range(len(kody)):
kod = kody[i]
if len(kod) == 13:
if cyfra_kontrolna(kod) == int(kod[12]):
poprawne = poprawne + 1
else:
bledne = bledne + 1
wyniki.append("c) poprawna cyfra kontrolna: " + str(poprawne) + ", bledna: " + str(bledne))
palindromy = []
for i in range(len(kody)):
kod = kody[i]
czy_palindrom = True
for j in range(len(kod)):
if kod[j] != kod[len(kod) - 1 - j]:
czy_palindrom = False
if czy_palindrom:
palindromy.append(kod)
wyniki.append("d) palindromow: " + str(len(palindromy)))
for i in range(len(palindromy)):
wyniki.append(" " + palindromy[i])
ile_razy = {}
for i in range(len(kody)):
kod = kody[i]
if kod not in ile_razy:
ile_razy[kod] = 0
ile_razy[kod] = ile_razy[kod] + 1
powtorzone = []
for kod in ile_razy:
if ile_razy[kod] > 1:
powtorzone.append(kod)
powtorzone.sort()
wyniki.append("e) powtorzonych kodow: " + str(len(powtorzone)))
for i in range(len(powtorzone)):
wyniki.append(" " + powtorzone[i] + " x" + str(ile_razy[powtorzone[i]]))
return wyniki
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
uic.loadUi(FOLDER / "form_kody.ui", self)
self.kody = []
self.przyciskWczytaj.clicked.connect(self.wczytaj)
self.przyciskZapisz.clicked.connect(self.zapisz)
def wczytaj(self):
self.kody = wczytaj_kody("kody.txt")
self.etykietaStatus.setText("Wczytano " + str(len(self.kody)) + " kodow")
self.listaWynikow.clear()
wyniki = analizuj(self.kody)
for i in range(len(wyniki)):
self.listaWynikow.addItem(wyniki[i])
def zapisz(self):
if len(self.kody) == 0:
QMessageBox.warning(self, "Uwaga", "Najpierw wczytaj plik z kodami.")
return
wyniki = analizuj(self.kody)
f = open(FOLDER / "raport.txt", "w", encoding="utf-8")
for i in range(len(wyniki)):
f.write(wyniki[i] + "\n")
f.close()
QMessageBox.information(self, "Gotowe", "Zapisano raport.txt")
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = MainWindow()
widget.show()
sys.exit(app.exec())
uic.loadUi()musi być pierwsze. Przed nim żaden widżet z formularza jeszcze nie istnieje — odwołanie doself.listaWynikowlinijkę wyżej toAttributeError. Kolejność w__init__jest zawsze ta sama:super(),loadUi(), dopiero potem własne pola iconnect.- Samo
self.w linii to błąd składni. Jeśli zaczniesz pisaćself.i zostawisz niedokończone, Python nie uruchomi nawet pierwszej linii pliku — dostanieszSyntaxError: invalid syntaxi czerwony znacznik na tej linii. - Nazwa w kodzie musi być identyczna jak
objectName. Literówka nie wyjdzie przy starcie — program wstanie normalnie i wywali się dopiero przy kliknięciu, bo dopiero wtedy sięga po widżet. - Ścieżka do
form.ui. Samo'form.ui'działa tylko wtedy, gdy terminal stoi w folderze projektu. Uruchomienie z innego katalogu dajeFileNotFoundError. Dlatego w kodzie jestPath(__file__).parent— to zawsze folder, w którym leżymainwindow.py, niezależnie od tego, skąd odpalasz.
clicked.connect(self.metoda) — bez nawiasów przy nazwie metody, inaczej podłączysz wynik wywołania zamiast samej funkcji; oraz clear() przed ponownym wypełnieniem listy — bez tego drugie kliknięcie dokleja wyniki do poprzednich.raport.txt identyczny jak w kroku 6.kody.txt do folderu projektu, obok mainwindow.py, i uruchom. W formularzu zmieniasz tylko trzy napisy — tytuł okna i teksty obu przycisków. objectName zostają te same, więc kod pozostaje bez zmian.Opis rozwiązania — co trzeba umieć, żeby napisać to samemu
| Podpunkt | Mechanizm | Gdzie jeszcze wraca na maturze |
|---|---|---|
| a) | len() na napisie + licznik w pętli | walidacja danych wejściowych w każdym zadaniu z plikiem |
| b) | pętla w pętli, int(kod[j]), operator % | suma cyfr, podzielność, liczby pierwsze |
| c) | własna funkcja, wagi zależne od pozycji, modulo | PESEL, NIP, ISBN — CKE lubi cyfry kontrolne |
| d) | indeks „od końca" len - 1 - i, flaga logiczna | palindromy, porównywanie napisów, prefiksosufiks |
| e) | słownik jako licznik wystąpień | najczęstszy element, duplikaty, histogram |
| f) | open("w") + write() + close() | każde zadanie kończące się „wyniki zapisz do pliku" |
int i utrata zer wiodących, (2) brak zewnętrznego % 10 w cyfrze kontrolnej, (3) liczenie cyfry kontrolnej dla kodów o złej długości — kod[12] na kodzie 12-cyfrowym wywala IndexError i program przerywa działanie w połowie.📝 Sprawdzian — Kody produktów
Sprawdź, czy algorytm cyfry kontrolnej i praca na napisach siedzą. Masz 10 minut na 5 pytań.
1. Dlaczego kod produktu trzymamy jako napis, a nie jako int?
2. Co zwróci (10 - 130 % 10) % 10?
3. W kodzie 13-cyfrowym, pod którym indeksem siedzi cyfra kontrolna?
4. Z którym znakiem porównujesz kod[j], sprawdzając palindrom?
5. Po co w słowniku-liczniku linia if kod not in ile_razy: ile_razy[kod] = 0?
Sprawdzian — teoria i SQL (zestaw 1)
10 pytań · 10 minut · próg zaliczenia 60%. Materiał: systemy liczbowe, logika, sieci, kompresja, bazy danych.
SPRAWDZIAN - 10 pytań, 10 minut, próg 60%. Odeślij jedną linią, np.:
1b 2c 3a 4d 5b 6c 7a 8d 9b 10c
1. Ile wynosi 101101(2) w systemie dziesiętnym?
a) 41 b) 44 c) 45 d) 53
2. Jaki zakres liczb całkowitych obejmuje 8-bitowy kod U2?
a) -127..127 b) -128..127 c) -128..128 d) 0..255
3. Ile bitów ma adres IPv6?
a) 32 b) 48 c) 64 d) 128
4. Ile adresów hostów daje podsieć z maską /26?
a) 62 b) 64 c) 126 d) 30
5. Wyrażenie NOT (A OR B) jest równoważne:
a) NOT A OR NOT B b) NOT A AND NOT B c) A AND B d) A XOR B
6. Które zdanie o kompresji RLE jest prawdziwe?
a) Jest stratna, bo usuwa powtarzające się znaki
b) Skraca każde możliwe wejście
c) Jest bezstratna, ale dla danych bez powtórzeń może wydłużyć zapis
d) Działa tylko na obrazach, nie na tekstach
7. Nieskompresowany obraz 640 x 480, 24 bity na piksel, zajmuje:
a) 921 600 B b) 307 200 B c) 7 372 800 B d) 1 843 200 B
8. Która klauzula filtruje wyniki PO grupowaniu?
a) WHERE b) ORDER BY c) LIMIT d) HAVING
9. Chcesz wypisać pacjentów bez ani jednej wizyty. Która konstrukcja to zrobi?
a) INNER JOIN wizyty z warunkiem COUNT(*) = 0
b) LEFT JOIN wizyty ... WHERE wizyty.id IS NULL
c) SELECT ... FROM wizyty WHERE pacjent_id IS NULL
d) GROUP BY pacjent_id HAVING COUNT(*) = 0
10. Data jest tekstem '2026-04-17'. Co wyciągnie z niej '2026-04'?
a) SUBSTR(data, 1, 4) b) SUBSTR(data, 6, 2)
c) SUBSTR(data, 1, 7) d) LEFT(data, 8)
Klucz — do sprawdzania odpowiedzi z czatu
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|
| c | b | d | a | b | c | a | d | b | c |
Pytania 1, 2 i 7 liczy się na kartce w kilkanaście sekund — błąd w nich oznacza brak automatyzmu, nie brak wiedzy. Formularz poniżej zostawiam do samodzielnego sprawdzenia, gdy uczeń dostanie kiedyś dostęp do aplikacji; dziś używasz wersji z czatu.
1011012 w systemie dziesiętnym?/26?NOT (A OR B) jest równoważne:'2026-04-17'. Które wyrażenie wyciągnie z niej '2026-04'?📝 Ściąga — Matura z informatyki
Kluczowe wzory, formuły i szablony na maturę z informatyki. Wydrukuj i przejrzyj przed egzaminem!
🔢 Systemy liczbowe
DEC→BIN: dziel przez 2, reszty od dołu
BIN→DEC: suma wag (128,64,32,16,8,4,2,1)
BIN→HEX: grupy po 4 bity
BIN→OCT: grupy po 3 bity
Python: bin(), oct(), hex(), int(s,base)
🧮 Logika Boole'a
De Morgan: ¬(A∧B)=¬A∨¬B
¬(A∨B)=¬A∧¬B
A∧1=A A∧0=0 A∧A=A A∧¬A=0
A∨0=A A∨1=1 A∨A=A A∨¬A=1
Python: and, or, not, &, |, ^
📊 Złożoność
O(1) — stała (dostęp T[i])
O(log n) — binary search
O(n) — liniowe szukanie
O(n log n)— merge sort
O(n²) — bubble/selection sort
O(2ⁿ) — podzbiory
🐍 Python — pliki
with open("f.txt","r",encoding="utf-8") as f:
for l in f:
parts = l.strip().split(";")
# Zapis:
with open("w.txt","w",encoding="utf-8") as f:
f.write(f"wynik: {x}\n")
🐍 Python — listy
min(L), max(L), sum(L), len(L)
sorted(L), sorted(L, reverse=True)
[x for x in L if warunek]
sum(1 for x in L if warunek)
set(L) # unikalne
🐍 Python — stringi
.split(sep) .strip() .upper()
.lower() .count(c) .replace(a,b)
.startswith(s) .endswith(s)
.isdigit() .isalpha() .islower()
f"text {zmienna:.2f}"
📋 Excel — kluczowe
=JEŻELI(war;"tak";"nie")
=LICZ.JEŻELI(zakr;war)
=LICZ.WARUNKI(z1;w1;z2;w2)
=SUMA.JEŻELI(z_war;war;z_sum)
=ŚREDNIA.JEŻELI(z_war;war;z_śr)
=WYSZUKAJ.PIONOWO(szu;tab;kol;0)
📋 Excel — adresy
A1 → względny (przesuwa się)
$A$1 → bezwzgl. (stały)
$A1 → kol stała, wiersz ruchomy
A$1 → kol ruchoma, wiersz stały
Skrót: F4 przełącza tryby
🗃️ SQL — SELECT
SELECT col FROM tab
WHERE war
GROUP BY col HAVING war
ORDER BY col ASC|DESC
LIMIT n;
🗃️ SQL — JOIN
INNER JOIN t2 ON t1.id=t2.fk
LEFT JOIN t2 ON t1.id=t2.fk
-- INNER: tylko pasujące
-- LEFT: wszyscy z lewej+NULL
🗃️ SQL — agregaty
COUNT(*), COUNT(col)
SUM(col), AVG(col)
MIN(col), MAX(col)
ROUND(val, 2)
DISTINCT col
🌐 Sieci — IP
Hosty = 2^(32-maska) - 2
/24→254h /25→126h /26→62h /27→30h
Adres sieci: IP AND maska
Broadcast: IP OR NOT(maska)
Prywatne: 10.x, 172.16-31.x, 192.168.x
💾 Informacja
N wartości → ⌈log₂(N)⌉ bitów
1KB=1024B 1MB=1024KB 1GB=1024MB
Obraz = szer×wys×głębia/8 bajtów
Dźwięk = freq×bity×kanały×czas/8
ASCII: '0'=48 'A'=65 'a'=97
⏱️ Strategia egzaminu
210 min — podział:
Cz.1 papier: ~50-60 min
Cz.2 program: ~50 min
Cz.2 Excel: ~30 min
Cz.2 SQL: ~40 min
Rezerwa: ~30 min
ZAWSZE zacznij od łatwiejszego!
🐛 Typowe błędy na maturze z informatyki
10 najczęstszych błędów, które kosztują punkty. Przejrzyj przed egzaminem!
❌ BŁĄD: Reszty z dzielenia czytane od GÓRY zamiast od DOŁU
156 ÷ 2 → reszta 0 ← to NIE jest pierwsza cyfra wyniku!
✅ POPRAWNIE: Reszty czytamy od DOŁU do GÓRY
Ostatnia reszta = najbardziej znaczący bit
❌ Pseudokod CKE: T[1..n] — indeksy od 1!
Python: T[0..n-1] — indeksy od 0!
Częsty błąd: Przepisanie pseudokodu 1:1 do Pythona
Pseudokod: T[1] → Python: T[0]
Pseudokod: T[n] → Python: T[n-1]
Pseudokod: i=1..n → Python: range(n) lub range(0,n)
# Pseudokod CKE:
# x div y = dzielenie całkowite
# x mod y = reszta
# Python:
x // y # div (nie / !)
x % y # mod
# ❌ BŁĄD: 17 / 5 = 3.4 (float!)
# ✅ POPRAWNIE: 17 // 5 = 3 (int)
# ❌ BŁĄD:
for linia in plik:
parts = linia.split(";")
# parts[-1] zawiera "wartość\n" — z nową linią!
# ✅ POPRAWNIE:
for linia in plik:
parts = linia.strip().split(";")
# czyste wartości
-- ❌ BŁĄD:
SELECT kategoria, COUNT(*)
FROM produkty
WHERE COUNT(*) > 5 -- NIE DZIAŁA!
GROUP BY kategoria;
-- ✅ POPRAWNIE:
SELECT kategoria, COUNT(*)
FROM produkty
GROUP BY kategoria
HAVING COUNT(*) > 5; -- HAVING po GROUP BY
❌ BŁĄD: Kopiujesz formułę w dół i tabela się przesuwa:
=WYSZUKAJ.PIONOWO(A2;G2:H100;2;0)
→ po skopiowaniu: =WYSZUKAJ.PIONOWO(A3;G3:H101;2;0) ← ZŁE!
✅ POPRAWNIE: Zablokuj tabelę dolarem:
=WYSZUKAJ.PIONOWO(A2;$G$2:$H$100;2;0)
→ A2 się przesuwa (dobrze), tabela stała
# Szukanie najdłuższej serii (np. bez opadów)
# ❌ BŁĄD — brak sprawdzenia po pętli:
for d in dane:
if warunek:
biezaca += 1
else:
if biezaca > max: max = biezaca
biezaca = 0
# Jeśli seria jest na KOŃCU — nigdy nie wejdzie w else!
# ✅ POPRAWNIE — sprawdź po pętli:
for d in dane:
...
# dodaj TO po pętli:
if biezaca > max: max = biezaca
# ❌ BŁĄD (Windows domyślnie cp1250):
with open("dane.txt") as f: # może nie odczytać ąęół
# ✅ POPRAWNIE:
with open("dane.txt", encoding="utf-8") as f:
# lub:
with open("dane.txt", encoding="cp1250") as f:
# Sprawdź kodowanie pliku w notatniku!
-- ❌ Ile RÓŻNYCH gatunków?
SELECT COUNT(gatunek) FROM ksiazki;
-- Liczy WSZYSTKIE wiersze (z powtórzeniami!)
-- ✅ POPRAWNIE:
SELECT COUNT(DISTINCT gatunek) FROM ksiazki;
-- ❌ Ile uczniów wypożyczyło książkę?
SELECT COUNT(*) FROM wypozyczenia; -- liczy WYPOŻYCZENIA, nie uczniów!
-- ✅ POPRAWNIE:
SELECT COUNT(DISTINCT uczen_id) FROM wypozyczenia;
❌ BŁĄD: 1 KB = 1000 B
✅ POPRAWNIE: 1 KB = 1024 B = 2¹⁰ B
❌ BŁĄD: Obraz 1920×1080 × 24 bit = 49 766 400 bajtów
✅ POPRAWNIE: 1920 × 1080 × 24 / 8 = 6 220 800 B (÷8!)
❌ BŁĄD: Zapomnienie o odejmowaniu 2 hostów
/24 → 256 hostów — NIE! → 256 - 2 = 254 hostów
(adres sieci + broadcast nie mogą być hostami)