📝 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.

Podział kanałów Twój ekran — teoria i wzorcowy kod, dopiero po próbie ucznia. Czat Meet — treści zadań i odpowiedzi ucznia; to jedyny ślad, który mu zostaje. Ekran ucznia — Android Studio przy zadaniach 12.4, 12.5 i mini-aplikacji; patrzysz, jak pisze, nie dyktujesz mu klawiszy.
Gradle sync — nie pozwól, żeby zjadł lekcję Pierwsze otwarcie projektu ciągnie zależności i potrafi paść dwa razy, zanim przejdzie. Jeśli sync jeszcze mieli, nie czekacie — wchodzicie w blok tablic, bo do niego komputer nie jest potrzebny. Gdy sync wywala się dalej: File → Invalidate Caches → Invalidate and Restart, a przy braku sieci File → Settings → Build Tools → Gradle → Offline mode odznaczone. Kod można pisać przy nieudanym syncu — edytor podkreśli R.id. na czerwono, to zniknie po udanym buildzie.

Rozkład minut

MinutyCo robimyKto co pokazujeMateriał
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 zadaniaKanał zwrotnyDlaczego tak
„Co wypisze", wartości, indeksyczat, jedna linia na zadanieZapis zostaje uczniowi jako materiał do powtórki wieczorem przed sprawdzianem
Fragmenty kodu (11.2, 11.4, 11.5, 12.2)czat — wklejony kodWidzisz 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 ekranLiczy się też to, jak szuka błędu w Logcacie, a tego z gotowego kodu nie zobaczysz
Sprawdzian, część zamknięta1b 2c 3a … jedną liniąSprawdzasz w kilkanaście sekund, uczeń nie widzi klucza

Pięć pytań na diagnozę (blok 0–8)

  1. Co wstawi do XML-a pozycja Plain Text z palety? (EditText — nazwa w palecie to nie nazwa klasy)
  2. Od jakiego indeksu zaczyna się tablica i jaki ma ostatni? (0 oraz length - 1)
  3. length czy length() — kiedy które? (tablica bez nawiasów, String z nawiasami)
  4. Kiedy CheckBox, a kiedy RadioButton? (wiele odpowiedzi vs dokładnie jedna)
  5. Po co RadioButtonom RadioGroup? (bez niej da się zaznaczyć wszystkie naraz)
  6. Co zwraca getCheckedRadioButtonId(), gdy nic nie wybrano? (-1, nie null)

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() z length tydzień 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

DataMateriał, na którym pracowaliśmyStan
05.09Zadanie 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.09GUI PyQt6 na strukturze form.ui — kroki w ćwiczeniach #02 i #05, pułapka z kolejnością w returnotwarte: czy zbudował formularz i podłączył sygnały
27.09Android pod szkolny sprawdzian: tablice (6 zadań), CheckBox i RadioButton (5 zadań), mini-aplikacja, sprawdzian próbny na 20 pktdziś

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.

Ten zakres nie schodzi już z planu Wrzesień miał być miesiącem sortowań (moduł M5) z kamieniem milowym „arkusz próbny #1 ≥ 40%". Sesja 19.09 poszła na GUI, 27.09 na przygotowanie do szkolnego sprawdzianu z Androida. Październik to według planu rocznego drzewa i struktury danych — bez złożoności i bez umiejętności prześledzenia algorytmu na kartce ten moduł nie ma na czym stanąć. Przy 90 minutach tnij raczej głębokość omówienia niż liczbę tematów: każdy z czterech bloków musi dziś wystąpić.

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

MinutyCo robimyKto co pokazujeMateriał
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

BlokCo tnijCzego nie tnij
SortowaniaPrzez wstawianie (4.3) omów ustnie na przykładzie kart w ręce, bez pełnego rozpisywaniaBą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 tabeliPorównanie liczby operacji dla n = 1000 — bez tego klasy złożoności zostają abstrakcją
PythonUczeń pisze szkielet, resztę kończy w domuLicznik 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ą

KrokCo robisz
1Pobierasz plik: sortowania.ipynb
2Wchodzisz na colab.research.google.com → Plik → Prześlij notatnik → wskazujesz pobrany plik
3Udostępnij (prawy górny róg) → Każdy z linkiem → uprawnienie Edytor → Kopiuj link
4Link wklejasz uczniowi na czat Meeta. Ten sam notatnik służy przez całą sesję — uczeń niczego nie instaluje i nie zakłada konta
Notatnik zostaje uczniowi po lekcji Z jego wpisanymi odpowiedziami i uruchomionymi sprawdzeniami. To jest jego notatka do powtórki przed maturą — czego kartka w kamerze nigdy nie dawała. Przed następną sesją robisz kopię (Plik → Zapisz kopię na Dysku), żeby zacząć od czystego notatnika.

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 sesjiZadanieKotwica do dopisania
startdane i sprawdzarki — uczeń uruchamia jako pierwsze#scrollTo=setup
5–404.1 — bąbelkowe#scrollTo=zad41
4.2 — przez wybór#scrollTo=zad42
4.3 — przez wstawianie#scrollTo=zad43
55–684.4 — złożoność#scrollTo=zad44
40–554.5 — wyszukiwanie binarne#scrollTo=zad45
68–874.6 — implementacja#scrollTo=zad46
do domu4.7 — bąbelkowe z flagą#scrollTo=zad47
Dwie rzeczy, które psują ten układ

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

TematGdzieKiedy wraca
Teoria za 6% arkusza: systemy liczbowe, logika, sieci, kompresja (21 zadań)Matura → Teoria, moduły 1, 2, 9, 10następna sesja albo praca własna z arkusza PDF
SQL na bazie przychodnia (12 zapytań + 2 domowe)Matura → Teoria → Bazy danych i SQLpo sortowaniach, przed arkuszem próbnym
Android: tablice, CheckBox, RadioButton, ćwiczenie #06Android → Teoria 11, 12 i ćwiczenie 6zrobione 27.09 — materiał do powtórki po stronie ucznia
GUI PyQt6 na strukturze form.ui (z 19.09)Python → ćwiczenia #02 i #05, kroki GUIzdjęte z planu sesji — uczeń kończy sam, sprawdzamy przy najbliższym zadaniu z GUI

Poprzednie sesje

DataMateriałStan
19.09GUI PyQt6 na strukturze form.uizdjęte z planu sesji — praca własna ucznia
27.09Android pod szkolny sprawdzian: tablice, CheckBox, RadioButton, ćwiczenie #06 na 20 pkt, sprawdzian próbnyzrobione
4.10Sortowania, 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.

Co uczeń już ma 15.11 dostał mailem arkusz PDF i 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

MinutyCo robimyKto co pokazujeMateriał
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

BlokCo tnijCzego nie tnij
Część INagłówek dokumentacji do domuSprawdzarkę: to ona pokazuje wymaganie o liczbie elementów, którego nie widać na ekranie
Część IISprawdzarka i zrzuty do domu, sam ją uruchomiKrok 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. Nigdy len, nigdy sum.
  • 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

PlikDla kogoGdzie
Arkusz PDF i pliki3.zipuczeń, wysłane mailem 15.11arkusze.pl
sprawdz_tablice.pyuczeń, część Imoretutor.domoresoft.com/sprawdz_tablice.py
sprawdz_galeria.jsuczeń, część II, konsola przeglądarkimoretutor.domoresoft.com/sprawdz_galeria.js
Wzorcowy main.pyTy, po próbie uczniaPython → ćwiczenie #08 → Gotowy kod
Wzorcowy App.jsxTy, po próbie uczniaReact → ćwiczenie #1 → Gotowy kod

Poprzednie sesje

DataMateriałStan
4.10Matura: sortowania, wyszukiwanie binarne, złożonośćprzebieg niezapisany w dzienniku
8.11INF.04 czerwiec 2026: generator haseł i menedżer haseł w Reactczas „na czas” niezapisany w dzienniku
15.11INF.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.

Zmiana planu rocznego Według planu matury listopad to moduł M7 (techniki algorytmiczne), a październikowy M6 (drzewa, stos, kolejka) nie został zrobiony. 8.11 zapadła decyzja, że ta sesja idzie na INF.04. Matura wraca od M6. Przesunięcie jest zapisane w dzienniku sesji.

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

MinutyCo robimyKto co pokazujeMateriał
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

BlokCo tnijCzego nie tnij
Czytanie arkuszaLista wymagań ustnie zamiast w plikuPorównanie z tabelą egzaminatora: to ona uczy czytać arkusz
DokumentacjaZip i egzamin.txt idą do domuNagłówek według listingu 1 i jeden zrzut całego ekranu. To osobny rezultat oceniany przez egzaminatora
Na czasCał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, nie print. 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

PlikDla kogoGdzie
Web1.7z (hasło *LoGiny&H@sla8)uczeń, przed sesjąmoretutor.domoresoft.com/Web1.7z
sprawdz_hasla.pyuczeń, przed sesjąmoretutor.domoresoft.com/sprawdz_hasla.py
Arkusz PDFTy, treść na czatarkusze.pl
Wzorcowy main.pyTy, po próbie uczniaPython → ćwiczenie #07 → Gotowy kod
Część II: menedżer haseł w ReactTy i uczeń, po części IReact → ćwiczenie #07
sprawdz_web.jsuczeń, wkleja do konsoli przeglądarkimoretutor.domoresoft.com/sprawdz_web.js

Poprzednie sesje

DataMateriałStan
27.09Android pod szkolny sprawdzian: tablice, CheckBox, RadioButton, ćwiczenie #06 na 20 pktzrobione
4.10Matura: sortowania, wyszukiwanie binarne, złożoność (7 zadań, notatnik Colab)przebieg niezapisany w dzienniku
8.11INF.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

0/8

#02 — Lista zadań (Todo)

Ćwiczenie · controlled input · dodawanie/usuwanie · toggle · select

0/7

#03 — Tracker wydatków

Ćwiczenie · .reduce() · number input · wyszukiwarka · Bootstrap card

0/8

#04 — Dziennik ocen

Ćwiczenie · edycja elementu · .sort() · .find() · dynamic select

0/8

Python — egzaminy / ćwiczenia

#01 — Rejestr produktów

Ćwiczenie własne · open/split · SQLite · PyQt6 GUI

0/8

#02 — Dziennik ucznia

Oceny · średnia · filtr przedmiot · SQLite · PyQt6

0/7

#03 — Wypożyczalnia książek

Katalog · wypożyczenie · UPDATE · SQLite · PyQt6

0/7

#04 — Stacja pogodowa

Pomiary · min/max/avg · daty · SQLite · PyQt6

0/8

#05 — Logowanie z nawigacją menu

QStackedWidget · QMenu · navbar · open() · sprawdzanie danych · PyQt6 GUI

0/6

#08 — Klasa z tablicą

INF.04 styczeń 2025, część I · pola prywatne · wyszukiwanie · średnia · sprawdzarka 29 wymagań

0/8

#09 — Loteria

INF.04 czerwiec 2025, część I · 6 z 49 bez powtórzeń · lista list · zliczanie · bez zmiennych globalnych · sprawdzarka 16 wymagań

0/8

Matura — egzaminy / ćwiczenia

#01 — Analiza algorytmu

Pseudokod · śledzenie zmiennych · wynik algorytmu · złożoność

0/6

#02 — Programowanie Python

Plik tekstowy · przetwarzanie danych · statystyki · wynik do pliku

0/8

#03 — Arkusz kalkulacyjny

Formuły · JEŻELI · LICZ.JEŻELI · WYSZUKAJ.PIONOWO · wykresy

0/6

#04 — Bazy danych SQL

SELECT · JOIN · GROUP BY · HAVING · podzapytania · CREATE VIEW

0/7

#05 — Kody produktów

Napisy · cyfra kontrolna EAN-13 · palindromy · duplikaty · PyQt6 GUI

0/9

📖 Baza wiedzy — React

ModułTematKluczowe pojęcia
1Co to React?Biblioteka UI, Virtual DOM, komponenty
2ŚrodowiskoNode.js, Vite, npm, struktura projektu
3JSXclassName, klamry {}, ternary, Fragment
4KomponentyFunkcja → JSX, export/import, PascalCase
5PropsDane rodzic→dziecko, destrukturyzacja, read-only
6StateuseState, setX, re-render, controlled input
7EventyonClick, onChange, onSubmit, e.preventDefault()
8Tablice.map(), key, .filter(), .find(), .sort(), spread
9Warunkowy renderingternary, &&, wczesny return, klasy warunkowe
10useEffectEfekty uboczne, zależności, cleanup, fetch
11FormularzeControlled inputs, select, checkbox, walidacja
12Struktura projektuImport/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łTematKluczowe pojęcia
1Podstawy PythonInterpreter, typy, zmienne, input/print, f-string
2Instrukcje warunkoweif/elif/else, and/or/not, in, zagnieżdżone
3Pętlefor, while, range(), break/continue, enumerate
4Funkcjedef, return, parametry domyślne, lambda
5StringiMetody, f-string, slicing, split/join
6Listyappend/sort/pop, comprehension, slicing
7Słowniki i krotkidict, tuple, set, .items(), unpacking
8Plikiwith open(), read/write, CSV, split(";")
9PyQt6QApplication, QLabel, QLineEdit, QPushButton, QListWidget, QGridLayout
10SQLiteconnect, 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łTematKluczowe pojęcia
1Systemy liczboweBIN, OCT, DEC, HEX, konwersje, arytmetyka
2Logika i algebra Boole'aAND, OR, NOT, XOR, tablice prawdy, bramki
3Algorytmy i pseudokodŚledzenie, zmienne, warunek stopu, złożoność
4Sortowanie i wyszukiwanieBubble, selection, insertion, binary search
5RekurencjaSilnia, Fibonacci, wieże Hanoi, drzewo wywołań
6Python — pliki i daneopen(), split(), listy, przetwarzanie tekstowe
7Arkusz kalkulacyjnyJEŻELI, LICZ.JEŻELI, SUMA.JEŻELI, WYSZUKAJ.PIONOWO
8Bazy danych i SQLSELECT, JOIN, GROUP BY, HAVING, podzapytania
9Sieci komputeroweAdresacja IP, maski, podsieci, DNS, protokoły
10Teoria informacjiKodowanie, 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?

DefinicjaReact to biblioteka JavaScript stworzona przez Meta (Facebook) w 2013 roku. Służy do budowania interfejsów użytkownika. React to biblioteka, nie framework — zajmuje się tylko warstwą widoku.

Stary sposób vs React

Czysty JS
<div id="licznik">0</div>
<button onclick="zwieksz()">Kliknij</button>
<script>
  let wartosc = 0;
  function zwieksz() {
    wartosc++;
    document.getElementById('licznik').textContent = wartosc;
  }
</script>
React
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
AnalogiaArkusz Excel: zmiana jednej komórki nie wymaga przeliczania wszystkiego.

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

1 — Jeden element główny
// ❌ return ( <h1>A</h1> <p>B</p> );
// ✅ return ( <> <h1>A</h1> <p>B</p> </> );
2 — Zamknięte tagi
// ❌ <br>    ✅ <br />
3 — className (nie class)

class jest słowem kluczowym JS → w JSX: className.

4 — camelCase
HTMLJSX
onclickonClick
forhtmlFor
5 — style jako obiekt
<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>;
}
Props są TYLKO DO ODCZYTU!Dane płyną rodzic → dziecko. Do zmieniania: 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

EventOpisTypowe użycie
onClickKliknięciePrzyciski, karty
onChangeZmiana wartościInput, select
onSubmitWysłanie formularzaForm
onKeyDownNaciśnięcie klawiszaEnter do zatwierdzenia
onFocus / onBlurFocus / utrataWalidacja pól
onMouseEnter / onMouseLeaveHoverTooltipy

📌 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>
  );
}
🔑 key musi być unikatowy w obrębie listy. Najlepszy: 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
⚠️ Uważaj z 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

ZapisKiedy 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
}, []);
🔁 Cleanup odpala się, gdy komponent się odmontowuje LUB przed ponownym uruchomieniem efektu.

📌 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

PlikRola
index.jsMontuje <App/> do drzewa DOM
App.jsxGłówny komponent — tu składasz całą aplikację
index.htmlJedyny HTML — <div id="root">
package.jsonLista 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

KomendaCo robi
npm create vite@latestTworzy nowy projekt
npm installInstaluje zależności
npm run devUruchamia serwer dev
npm run buildBuduje produkcję
npm run previewPodglą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
);
💡 W React state traktujemy jako immutable — nigdy nie modyfikuj bezpośrednio, zawsze twórz kopię!

📌 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 routeraZ routerem
Jedna strona, wszystko w App.jsOsobne "podstrony": /home, /about, /contact
Ukrywasz/pokazujesz komponenty stanemURL zmienia się automatycznie
Przycisk "Wstecz" nie działaPrzycisk "Wstecz" działa normalnie
Nie można udostępnić linka do konkretnej stronyKaż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 czy .jsx? Oba działają w Vite — JSX można pisać w plikach .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>
);
Opakowujemy <App /> w <BrowserRouter> — to włącza routing w całej aplikacji.

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>
  );
}
ElementCo 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>
  );
}
Dlaczego Link a nie <a>? Zwykły <a href="..."> przeładowuje całą stronę. <Link to="..."> zmienia URL bez przeładowania — to właśnie SPA!

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>
  );
}
ElementCo robi
useEffect(() => {...}, [])Odpala fetch raz — przy wejściu na stronę
useState(true) dla loadingPokazuje "Ładowanie..." dopóki dane nie przyjdą
.then(res => res.json())Zamienia odpowiedź HTTP na obiekt JS
setLoading(false)Ukrywa "Ładowanie..." i pokazuje listę

Podsumowanie

ImportCo robi
BrowserRouterOpakowuje aplikację — włącza routing
RoutesKontener na trasy
RouteJedna trasa: path + element
LinkNawigacja bez przeładowania strony
NavLinkLink 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.

BibliotekaCo robi
reactSam React (już mamy)
react-router-domRouting (poprzednia lekcja)
bootstrapGotowe style CSS
axiosWysyłanie zapytań HTTP do API ← dziś
moment / dayjsPraca z datami
chart.jsWykresy

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';
Skąd wiem co biblioteka robi? Każda biblioteka ma stronę dokumentacji. Wyszukaj w Google "axios npm" — pierwsza odpowiedź to dokumentacja z przykładami.

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:

LiteraOperacjaHTTP MethodCo robi
CCreatePOSTTworzy nowy zasób
RReadGETCzyta dane (lista lub jeden)
UUpdatePUT / PATCHAktualizuje zasób
DDeleteDELETEUsuwa zasób

axios vs fetch — czemu axios?

Cechafetch (wbudowany)axios (biblioteka)
Krócej2 kroki: fetch().then(r=>r.json())1 krok: axios.get() — od razu daje dane
JSON automatycznieRęcznie res.json()Automatycznie
Obsługa błędów404 NIE rzuca błędu (musisz sprawdzić)404/500 automatycznie rzuca błąd
POST/PUTTrzeba ręcznie ustawić headers + JSON.stringifyWystarczy 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));
    });
}
URL musi zawierać ID konkretnego zasobu: /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);
  }
}
async/await to nowsza, czytelniejsza składnia. 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ęcieCo warto pamiętać
npm installPobiera bibliotekę i zapisuje w package.json
import x from 'lib'Włącza bibliotekę w pliku
API"Punkt kontaktu" z serwerem przez HTTP
JSONFormat danych (wygląda jak obiekt JS)
CRUD4 operacje: Create / Read / Update / Delete
axios.get/post/put/delete4 metody — po jednej na każdą operację
response.dataTu są dane z serwera (axios automatycznie parsuje JSON)
Loading statePokaż użytkownikowi że trwa ładowanie
try / catchObsł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:

  1. Przy starcie (useEffect z []) pobiera listę użytkowników z API i zapisuje w stanie
  2. Renderuje przycisk "Dodaj" oraz listę imion użytkowników
  3. Po kliknięciu "Dodaj" wysyła POST do API z nowym userem ("Jan Kowalski")
  4. 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

Zadanie

Wykonaj mini-stronę z nawigacją używając React Router:

  1. 3 podstrony: Strona główna (/), O nas (/about), Kontakt (/contact)
  2. Menu nawigacyjne widoczne na każdej stronie
  3. Aktywny link podświetlony innym kolorem (NavLink)
  4. Strona 404 dla nieistniejących adresów
  5. Na stronie Kontakt — formularz z polami: imię, email, wiadomość
  6. useEffect — zmiana tytułu karty przeglądarki na każdej podstronie
  7. Na stronie "O nas" — pobranie danych z API (fetch + useEffect)
  8. Bootstrap do stylowania

Mini-strona — Budowa

☐ Krok 1: Utwórz projekt i zainstaluj paczki
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';
☐ Krok 2: Opakowaj App w BrowserRouter (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>
);
☐ Krok 3: Utwórz folder pages/ z 4 komponentami

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;
useEffect robi 2 rzeczy: (1) zmienia tytuł karty w przeglądarce, (2) pobiera dane z API. Oba odpalają się raz — przy wejściu na stronę "O nas".

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;
☐ Krok 4: Navbar z NavLink

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;
NavLink + isActive — Bootstrap klasa active podświetla aktywny link. NavLink dostarcza info czy dany link jest aktualnie aktywny.
☐ Krok 5: Strona Kontakt z formularzem + useEffect

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;
Łączy kilka tematów: useState, formularze, warunkowy rendering, computed property names [e.target.name].
☐ Krok 6: App.js — Routes + Navbar
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;
Navbar jest PO ZA Routes — dlatego jest widoczny na każdej podstronie. Tylko to co jest wewnątrz Routes zmienia się przy nawigacji.
☐ Krok 7: Uruchom i przetestuj
npm run dev

Sprawdź:

TestOczekiwany wynik
Otwórz http://localhost:5173Strona 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ślijZielony alert "Dziękujemy"
Wpisz w URL /xyzStrona 404
Przycisk "Wstecz" w przeglądarceWraca do poprzedniej strony
Aktywny link w menuPodś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ć

Sprawdzian z lekcji 05:00
1
Jak zainstalować React Router?
2
Dlaczego używamy <Link> zamiast <a>?
3
Co robi path="*" w <Route>?
4
Czym NavLink różni się od Link?
5
Gdzie umieszczamy <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

Zadanie

Wykonaj aplikację React do zarządzania hodowlą świnek morskich. Aplikacja używa:

  1. React Router + Navbar — 5 podstron z nawigacją
  2. axios — komunikacja z backendem REST API
  3. PHP + MySQL (XAMPP) — realny backend i baza, nie mock
  4. Dwie tabele MySQL: rasy i swinki (relacja rasy_id → rasy.id)
  5. CRUD na świnkach: lista / dodaj / edytuj / usuń
  6. Read-only lista ras (z drugiej tabeli)
  7. Bootstrap do stylowania

Schemat bazy danych (z hodowla.sql)

Tabela rasyTabela 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)

URLKomponentCo robi
/HomePowitanie + statystyki (ile świnek, ile ras, średnia cena)
/swinkiListaSwinekREAD — tabela świnek z nazwą rasy + Edytuj/Usuń
/swinki/dodajDodajSwinkeCREATE — formularz nowej świnki
/swinki/:id/edytujEdytujSwinkeUPDATE — formularz z wypełnionymi danymi
/rasyRasyREAD — lista ras (read-only)
*NotFound404

Czego się nauczysz

PojęcieCo robi
Architektura 3-warstwowaFrontend (React) → Backend (PHP) → Baza (MySQL)
PHP + PDOBezpieczne łączenie się z MySQL i obsługa metod HTTP
CORSHeader 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 + RouterCRUD rozłożony na osobne strony Reacta
useNavigateProgramowa zmiana strony (po zapisie wraca na listę)
useParamsCzytanie 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

☐ Krok 1: Utwórz projekt i zainstaluj paczki
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>
);
Trzy biblioteki: axios (HTTP), bootstrap (style), react-router-dom (routing). Backend = PHP+MySQL z XAMPP (nie potrzebujemy json-server).
☐ Krok 2: Backend PHP w XAMPP — 4 pliki łączące React z MySQL

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
Ścieżka htdocs zależy od systemu: Windows XAMPP 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());
Bez .htaccess! Świadomie NIE używamy mod_rewrite. React będzie wołał endpointy wprost: 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.

Czemu PHP a nie React? Przeglądarka NIE umie języka MySQL. PHP jest tłumaczem między HTTP (od Reacta) a SQL (do bazy). Plus dane logowania do bazy zostają na serwerze, nigdy nie trafiają do przeglądarki.
CORS — header 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.
☐ Krok 3: Navbar z linkami

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).
☐ Krok 4: App.js — Routes + 5 stron

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.
☐ Krok 5: Strona Home — statystyki hodowli

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.
☐ Krok 6: Strona ListaSwinek — READ + DELETE + JOIN z rasami

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;
Promise.all — pobiera świnki I rasy równolegle. Czeka aż obie odpowiedzi przyjdą i dopiero ustawia stan.
"JOIN" w React — funkcja nazwaRasy(rasy_id) szuka rasy o danym id przez .find(). To samo co SQL JOIN, tylko po stronie frontu.
☐ Krok 7: Strona DodajSwinke — formularz z select ras (CREATE)

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

  • useState trzyma obiekt dane z 6 polami formularza (imie, rasy_id, data_ur, miot, opis, cena)
  • useEffect pobiera listę ras (rasy.php) do wypełnienia <select>
  • handleChange — jeden handler na wszystkie pola (po name); dla rasy_id i cena robi Number(value)
  • handleSubmit — axios.post do swinki.php, potem navigate('/swinki')
Number(value) — <input> i <select> zawsze dają string. Dla rasy_id i cena trzeba zamienić na liczbę, inaczej „JOIN" w liście się rozjedzie (4 ≠ "4").
useNavigate — po sukcesie POST wywołujemy navigate('/swinki') — programowo wracamy na listę. Tak samo działa „Anuluj".
☐ Krok 8: Strona EdytujSwinke — useParams + GET + PUT (UPDATE)

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 startnajpierw GET swinki.php?id=X wypełnia formularz
axios.postaxios.put z tym samym id
—useParams() czyta :id z URL
—if (!dane) return Ładowanie dopóki nie przyjdą dane
Trzy hooki naraz:
  • useParams() — czyta :id z URL (zawsze string)
  • useEffect([id]) — pobiera dane świnki PO ID przy wejściu na stronę
  • useNavigate() — po PUT wraca na listę
Wzorzec edycji: Lista linkuje do /swinki/5/edytuj. EdytujSwinke czyta id=5, pobiera GET swinki.php?id=5, wypełnia formularz. Po zapisie PUT swinki.php?id=5.
☐ Krok 9: Strona Rasy + 404

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;
☐ Krok 10: Test integracji — phpMyAdmin obok Reacta

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 ReactNetworkphpMyAdmin (po refresh)
Otwórz /GET /swinki + GET /rasy—
Kliknij "Świnki"GET /swinki + GET /rasyTabela tych samych świnek
Dodaj świnkę "Pinki"POST /swinki (200)Nowy wiersz w tabeli swinki
Edytuj cenę świnkiPUT /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
"Aha!" moment: Dane są realne, persystentne, dostępne dla każdej aplikacji która zna login do bazy (React, PHP, Python, Java...). Backend PHP+MySQL to centralne źródło prawdy.

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 — funkcje getSwinki(), 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
Dlaczego potrzebujemy PHP? Przeglądarka NIE umie języka MySQL. To dwa różne protokoły. PHP jest tłumaczem między HTTP (od Reacta) a SQL (do bazy). Plus — dane logowania do bazy zostają na serwerze, nigdy nie trafiają do przeglądarki.

Wymagania (XAMPP — uczeń już ma)

  • Apache uruchomiony (XAMPP Control Panel → Start)
  • MySQL uruchomiony (XAMPP Control Panel → Start)
  • Baza hodowla z tabelami rasy i swinki (już jest)
  • Folder Apache: C:\xampp\htdocs\ — tu trafią pliki PHP
☐ Krok 1: Utwórz folder dla API

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
Sprawdź czy Apache widzi folder: wejdź na http://localhost/hodowla-api/ w przeglądarce. Jeśli pokaże listę plików (lub 403) — Apache działa.
☐ Krok 2: db.php — połączenie z MySQL przez PDO
<?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;
}
Co tu się dzieje:
  • 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)
☐ Krok 3: swinki.php — pełen CRUD (4 metody HTTP)
<?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']);
}
SQL injection? NIE — dzięki prepare() + execute([...]). Wartości są przekazywane jako parametry, nie wklejane do tekstu zapytania. To jest bezpieczne.
php://input — czyta surowe ciało żądania (gdzie axios wysyła JSON dla POST/PUT). json_decode(..., true) zamienia na tablicę asocjacyjną.
☐ Krok 4: rasy.php — read-only lista ras
<?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.

☐ Krok 5: Dlaczego NIE używamy .htaccess (świadoma decyzja)

Można by zrobić "ładne URL-e" (/swinki/5) przez Apache mod_rewrite i plik .htaccess. Świadomie tego nie robimy.

PodejścieURLZależność
.htaccess + mod_rewrite/swinki/5Wymaga AllowOverride All + mod_rewrite + restart Apache. Różni się Windows/macOS/MAMP. Kruche.
Query string (nasze)/swinki.php?id=5Zero konfiguracji. Działa na każdym serwerze, zawsze.
Praktyka inżynierska: URL rewrite to częste źródło "u mnie nie działa" — zależy od wersji i ustawień hostingu. Query string (?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.

☐ Krok 6: Test backendu w przeglądarce (bez Reacta!)

Zanim podpinamy React — sprawdź że PHP samo działa. Otwórz w przeglądarce:

URLCo powinno pokazać
http://localhost/hodowla-api/rasy.phpJSON z listą wszystkich ras
http://localhost/hodowla-api/swinki.phpJSON z wszystkimi świnkami
http://localhost/hodowla-api/swinki.php?id=1JSON jednej świnki (Crejzy)
Jeśli widzisz JSON — backend działa! Już teraz te dane są naprawdę z MySQL (nie z db.json). Otwórz phpMyAdmin równolegle żeby zobaczyć źródło.

Jeśli widzisz błąd: sprawdź czy Apache + MySQL działają (XAMPP Control Panel), czy plik db.php ma poprawne dane logowania.

☐ Krok 7: Migracja React — zmień JEDNĄ linię

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
To jest moc separacji warstw! Cały React (5 stron, 6 komponentów) bez żadnej zmiany działa teraz na realnej bazie MySQL. Frontend nie wie i nie musi wiedzieć skąd są dane.
☐ Krok 8: Test integracji — phpMyAdmin obok Reacta

Otwórz dwa okna obok siebie:

  • Lewe: React na http://localhost:3000/swinki
  • Prawe: phpMyAdmin → hodowla.swinki
Akcja w ReactCo zobaczysz w phpMyAdmin (po odświeżeniu)
Dodaj świnkę "Pinki"Nowy wiersz w tabeli swinki
Edytuj cenę świnkiZaktualizowana wartość w kolumnie cena
Usuń świnkęWiersz znika z tabeli
Odśwież stronę ReactDane zostają — to NIE jest pamięć przeglądarki, to MySQL!
Dodaj wiersz w phpMyAdminPo refreshu Reacta — nowa świnka pojawia się w UI
To jest "aha!" moment: dane są realne, persystentne, dostępne dla każdej aplikacji która zna login do bazy (PHP, Python, Java, inny React...). Backend (PHP+MySQL) to centralne źródło prawdy.

Typowe błędy i jak je naprawić

BłądPrzyczynaNaprawa
CORS error w F12 ConsolePHP nie wysłał headera Access-Control-Allow-OriginSprawdź czy header('Access-Control-Allow-Origin: *') jest na początku PHP
404 Not FoundZły URL — brak .php albo zła ścieżka folderuSprawdź ż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ówW 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łoXAMPP → Start MySQL, sprawdź $user/$pass w db.php
500 Internal Server ErrorBłąd składni PHP lub w SQLOtwórz C:\xampp\apache\logs\error.log — jest tam dokładny komunikat
POST/PUT zwraca null w PHPBrak php://input + json_decodeaxios wysyła JSON w body — czytamy: json_decode(file_get_contents('php://input'), true)
Polskie znaki = krzakiBrak charset=utf8mb4Sprawdź 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ć

Sprawdzian z lekcji 05:00
1
Czemu React nie może łączyć się bezpośrednio z bazą MySQL?
2
Jak w React zrobić "JOIN" — pokazać nazwę rasy obok świnki, gdy mamy swinka.rasy_id i tablicę rasy?
3
Co robi hook useParams() z react-router-dom dla URL /swinki/5/edytuj i trasy /swinki/:id/edytuj?
4
Po sukcesie POST chcemy programowo wrócić na listę świnek. Jak to zrobić?
5
W formularzu mamy <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

Arkusz CKE · INF.04-01-26.06-SG · egzamin zawodowy, czerwiec 2026Część II arkusza (aplikacja webowa) i część III w zakresie, który jej dotyczy. Wymagania przepisane z arkusza bez zmian.

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)

StanFormularzPodsumowanie pod przyciskiem
1. Startpola puste, „Wygeneruj hasło” zaznaczone, pole hasła szareAdres: / Login: / Hasło automatycznie generowane
2. Zapisz z zaznaczonym polem wyborupoczta.pl, Ola, pole hasła szareAdres: poczta.pl / Login: Ola / Hasło automatycznie generowane
3. Zapisz z odznaczonym polem wyborupoczta.pl, Ola, hasło w kropkachAdres: 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 arkuszaW kodzie
Jeden komponent, dwa blokiApp.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 polamihtmlFor="adres" i id="adres", tak samo login i hasło
Podpowiedzi w polachplaceholder="Adres strony WWW", placeholder="Twój login"
Pole wyboru domyślnie zaznaczoneuseState(true) + checked={generuj}
Pole hasła ukrywa znakitype="password"
Zaznaczone, czyli hasło nieaktywnedisabled={generuj}
Niebieski przycisk „Zapisz”className="btn btn-primary m-2"
Podsumowanie wypełniane po zatwierdzeniuosobny stan zapisane, ustawiany tylko w zapisz()
Akapity: p-2, pierwszy dodatkowo text-primary leadtrzy <p> pod formularzem
Trzeci akapit zależny od pola wyboru{zapisane.generuj ? ... : ...}
web.zip, zmodyfikowane pliki, zrzuty web1–web3krok 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.

1
Projekt Vite i Bootstrap
▼

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.

Vite, a nie create-react-app 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.
2
Wyczyść szablon Vite
▼

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>,
)
Pułapka: 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ą.

3
Szkielet komponentu i stan
▼
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.

Dlaczego dwa zestawy danych Arkusz mówi: „elementy są wypełniane po zatwierdzeniu formularza przyciskiem”. Jeśli akapit pokazuje {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).

4
Formularz z szablon.txt przerobiony na JSX
▼

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 szablonieW JSXDlaczego
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-primaryniebieski 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.
5
Pole wyboru, które blokuje hasło
▼
  <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>
Pułapka: 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.

6
Zapisz i blok drugi: trzy akapity
▼

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>
WymaganieKlasa Bootstrap (tabela 1 arkusza)
margines wewnętrzny 0,5 remp-2 (p = padding, 2 = .5rem)
tekst niebieskitext-primary
tekst wiodącylead
Pułapka: brak 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.
7
Sprawdzarka w konsoli przeglądarki: 21 wymagań
▼
Materiał własny MoreTutorNa egzaminie jej nie ma. Sprawdzarka klika formularz tak, jak zrobiłby to egzaminator.
  1. Aplikacja otwarta w przeglądarce (npm run dev).
  2. F12 (macOS: Cmd + Opt + I), zakładka Console.
  3. 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).

8
Zrzuty web1–web3, web.zip i pliki źródłowe
▼

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:

PlikStan
web1.pngstan początkowy, zaraz po otwarciu
web2.pngwpisane dane, pole wyboru zaznaczone, po kliknięciu „Zapisz”
web3.pngpole wyboru odznaczone, wpisane hasło, po kliknięciu „Zapisz”

Folder webowa:

  • web.zip, czyli spakowany cały folder projektu, łącznie z node_modules, bo arkusz mówi „archiwum całego folderu projektu”. Przy Vite to około 70 MB, a pakowanie trwa poniżej minuty. Najpierw zatrzymaj npm run dev (Ctrl + C).
  • kopie zmodyfikowanych plików: App.jsx i main.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)
Kolejność oddania na egzaminie Zrzuty robisz zaraz po skończeniu każdej części, a nie na końcu. Jeśli aplikacja webowa się nie skompiluje, arkusz wymaga zrzutu z listą błędów. Zrzut zrobiony wcześniej, z działającą częścią I, już leży w folderze.

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 do zapisane.
  • Checkbox steruje dwiema rzeczami: disabled pola hasła teraz i treścią trzeciego akapitu po zapisie.

Menedżer haseł — Sprawdzian

5 pytań · 5 minut · minimum 60%, żeby zdać

Sprawdzian z lekcji 05:00
1
Jak w JSX powiązać etykietę z polem o id="login"?
2
Która linia poprawnie zapisuje stan checkboxa?
3
Margines wewnętrzny 0,5 rem w Bootstrapie to:
4
Akapit pokazuje {adres} zamiast {zapisane.adres}. Co jest nie tak?
5
Po kliknięciu „Zapisz” podsumowanie miga i wraca do pustego. Najbardziej prawdopodobna przyczyna?

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

Arkusz CKE · INF.04-01-25.01-SG · egzamin zawodowy, styczeń 2025Część II arkusza (aplikacja webowa) i część III w zakresie, który jej dotyczy. Wymagania przepisane z arkusza bez zmian.

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

Część II. Aplikacja webowa

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/assets albo egzamin/public/assets)
  • dane.txt zawiera 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.zip z całym folderem projektu i zmodyfikowane pliki źródłowe

Jak ma wyglądać (ilustracje z arkusza)

ObrazStanCo widać
2starttrzy 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”
3wyłączone „Zwierzęta”3 kwiaty i 2 samochody
4włączone tylko „Samochody”, kilka kliknięć pod GarbusemAudi „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 arkuszaW kodzieJak łatwo to stracić
Dane z dane.txt jako tablicaconst ZDJECIA = [ ... ] i useState(ZDJECIA)przepisywanie ręczne zamiast kopiowania
Trzy switche domyślnie włączoneuseState(true) × 3, checked + onChangee.target.value zamiast e.target.checked
Switche według tabeli 1form-check form-switch form-check-inlinebrak form-check-inline, przełączniki jeden pod drugim
Pętla i warunekzdjecia.map(...) z czyWidoczne(...) &&12 bloków przepisanych ręcznie
Bloki obok siebieclassName="d-flex flex-wrap"bloki jeden pod drugim
Margines 5 px, zaokrąglone rogiApp.css: img { margin: 5px; border-radius: 8px; }padding zamiast margin
Pobrania zapisane w tablicysetZdjecia(zdjecia.map(...)) z nowym obiektemmutacja downloads++, ekran się nie odświeża
Zrzuty web1, web2, ... i web.zipkrok 8brak 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.

1
Archiwum, projekt Vite i Bootstrap
▼
  1. Rozpakuj pliki3.zip hasłem K@tegorie). Hasło kończy się nawiasem zamykającym, który łatwo pominąć.
  2. W folderze webowa utwórz projekt o nazwie egzamin, bo tak nazywa go arkusz:
npm create vite@latest egzamin -- --template react
cd egzamin
npm install
npm install bootstrap
npm run dev
Vite, a nie create-react-app 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.
2
Obrazy do public/assets i czysty szablon
▼
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.

Pułapka: 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.
3
Dane z dane.txt i stan
▼

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;
Dlaczego 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.
4
Trzy przełączniki z tabeli 1
▼

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 1W JSXDlaczego
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-inlinejeden rząd, jak na obrazie 2 (opis w tabeli 1)
Pułapka: 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.
5
Bloki zdjęć: pętla i warunek
▼

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;
}
Dlaczego to spełnia „działa dla innej liczby zdjęć” Nigdzie w kodzie nie ma liczby 12 ani nazwy konkretnego pliku. Dopisanie trzynastego obiektu do tablicy dokłada trzynasty blok bez innych zmian.
Pułapka: brak 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.
6
Przycisk „Pobierz”: nowa tablica, nie mutacja
▼
  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.

Pułapka: 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.
Pułapka: onClick={pobierz(zdjecie.id)} Biała strona i w konsoli „Too many re-renders”. Bez () => funkcja wykonuje się w trakcie rysowania.
7
Sprawdzarka w konsoli przeglądarki: 24 wymagania
▼
Materiał własny MoreTutorNa egzaminie jej nie ma. Sprawdzarka klika przełączniki i przyciski tak, jak zrobiłby to egzaminator.
  1. Aplikacja otwarta w przeglądarce (npm run dev), okno szerokie na co najmniej dwa zdjęcia.
  2. F12 (macOS: Cmd + Opt + I), zakładka Console.
  3. 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).

8
Zrzuty web1–web4, web.zip i pliki źródłowe
▼

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:

PlikStan
web1.pngstan początkowy, wszystkie przełączniki włączone
web2.pngwyłączone „Zwierzęta”
web3.pngwłączone tylko „Samochody”
web4.pngten 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.
  • map rysuje blok dla każdego zdjęcia, a czyWidoczne(...) && pomija zdjęcia z wyłączonej kategorii.
  • pobierz buduje 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ć

Sprawdzian z lekcji 05:00
1
Która linia poprawnie zapisuje stan przełącznika?
2
Obrazy leżą w public/assets. Jaki src ma zdjęcie?
3
Po kliknięciu „Pobierz” liczba na ekranie stoi, a po przełączeniu switcha nagle skacze. Przyczyna?
4
Która klasa ustawia trzy przełączniki w jednym rzędzie?
5
Biała strona i w konsoli „Too many re-renders”. Która linia jest winna?

🔥 Rozgrzewka — Lista zadań

Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.

przypomnienie
Co zwraca wywołanie useState(wartośćPoczątkowa)?
Hook 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ą.
koncepcja
Jak obsłużyć wysłanie formularza w React, żeby strona się nie przeładowała?
Do elementu <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.
kod
Jak poprawnie dodać nowy element do tablicy w stanie?
Nigdy nie modyfikujemy tablicy bezpośrednio (np. 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]).
koncepcja
Co to jest „kontrolowany input" (controlled input)?
Kontrolowany input to pole formularza, którego wartość jest przechowywana w stanie React. Ustawiamy 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.
kod
Jak działa zdarzenie onChange na elemencie <input>?
Zdarzenie 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)

controlled input (text) select + onChange dodawanie do tablicy usuwanie z tablicy toggle boolean w obiekcie warunkowe klasy CSS Date.now() jako ID Bootstrap table + badge useState (tablica) .map() w JSX .filter() spread operator

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

ProblemPodpowiedź
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

WymaganieDetaleStatus
Jeden komponentApp.jsx✅
Nagłówek h1„Lista zadań"✅
Formularz dodawaniainput (text) + select (priorytet) + button „Dodaj"✅
Controlled inputvalue={nowyTytul} + onChange✅
Controlled selectvalue={nowyPriorytet} + onChange + Number()✅
Dane początkowe4 obiekty {id, tytul, priorytet, zrobione}✅
Tabela Bootstraptable table-striped, 4 kolumny✅
Badge priorytetbg-success (niski), bg-warning (średni), bg-danger (wysoki)✅
Dodawanie zadania[...zadania, nowe] + Date.now() jako id✅
WalidacjaNie dodaje pustego (trim === '')✅
Toggle zrobione.map() + spread + !z.zrobione✅
Usuwanie zadania.filter(z => z.id !== id)✅
CSS — przekreślenietext-decoration: line-through + opacity: 0.5✅
Warunkowe klasyclassName={z.zrobione ? 'zrobione' : ''}✅
Licznik„Zrobione: X z Y" — filter().length✅
Znaczące nazwyzadania, nowyTytul, dodaj, toggle, usun✅

#02 — Budowa krok po kroku

0/7 kroków
0
Nowy projekt (lub istniejący)
▼

„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
1
Szkielet + dane początkowe
▼

„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."

2
Formularz — controlled input + select
▼

„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.

Sprawdź: Wpisujesz tekst → widać. Zmieniasz select → nic się nie psuje. Button jeszcze nie działa.
3
Funkcja dodaj() — spread + nowy obiekt
▼

„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]"

Sprawdź: Wpisz tekst → kliknij Dodaj → nowe zadanie pojawia się (pod nagłówkiem, jeszcze nie ma tabeli).
4
Tabela Bootstrap + .map()
▼

„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.

5
Toggle + Usuń — dwie funkcje
▼

„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."

Sprawdź: ✓ toggleuje na Zrobione/Do zrobienia. ✕ usuwa wiersz.
6
CSS + licznik
▼

„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ć

Sprawdzian z lekcji 05:00
1
Co zwraca wywołanie useState("")?
2
Dlaczego w obsłudze formularza wywołujemy e.preventDefault()?
3
Jak poprawnie dodać nowy element do tablicy w stanie React?
4
Czym jest „controlled input" (kontrolowany input) w React?
5
Jak usunąć element o danym id z tablicy w stanie?

🔥 Rozgrzewka — Tracker wydatków

Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.

koncepcja
Czym jest 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.
przypomnienie
Jak działa 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.
kod
Co robi metoda .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.
kod
Do czego służy .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.
koncepcja
Jak sprawić, żeby useEffect uruchomił się tylko raz, przy pierwszym renderze?
Wystarczy przekazać pustą tablicę zależności: 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)

.reduce() — suma input type="number" parseFloat() .toFixed(2) wyszukiwarka (.includes) .toLowerCase() Bootstrap card Bootstrap alert (warunkowy) useState (tablica) controlled input/select dodawanie/usuwanie .map() / .filter() Bootstrap table / badge

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

ProblemPodpowiedź
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

WymaganieDetaleStatus
Jeden komponentApp.jsx✅
Nagłówek h1„Tracker wydatków"✅
Dane początkowe5 obiektów {id, nazwa, kwota, kategoria}✅
Formularz dodawaniainput text + input number + select + button✅
Controlled input (text)value={nowaNazwa} + onChange✅
Controlled input (number)value={nowaKwota} + onChange + parseFloat()✅
Controlled selectvalue={nowaKategoria} + onChange✅
WalidacjaNie dodaje pustej nazwy ani kwoty ≤ 0✅
Wyszukiwarka.toLowerCase().includes() — case-insensitive✅
Tabela Bootstraptable table-hover, 4 kolumny✅
Badge kategoria4 kolory — jedzenie/transport/rozrywka/edukacja✅
Usuwanie.filter(w => w.id !== id)✅
Karta podsumowaniaBootstrap card — suma, ilość, średnia✅
.reduce() sumawydatki.reduce((s, w) => s + w.kwota, 0)✅
.toFixed(2)Kwoty z 2 miejscami po przecinku + „zł"✅
Alert warunkowysuma > 1000 → alert-warning✅
Znaczące nazwywydatki, nowaNazwa, dodaj, usun, szukaj, suma✅

#03 — Budowa krok po kroku

0/8 kroków
0
Nowy projekt (lub istniejący)
▼

„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
1
Szkielet + dane początkowe
▼

„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.

2
Formularz — text + number + select
▼

„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.

3
Funkcja dodaj() + parseFloat + walidacja
▼

„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()!"

Sprawdź: Wpisz „Kanapki" + 25.50 + jedzenie → Dodaj. Czyści pola? Dodaje do tablicy?
4
Tabela + badge kategorii + usuwanie
▼

„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}.

5
Wyszukiwarka — .filter() + .includes()
▼

„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())"

6
Podsumowanie — .reduce() + Bootstrap card
▼

„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)"

Kroks (akumulator)w.kwotawynik
start0——
10127.50127.50
2127.5099.00226.50
3226.5043.00269.50
4269.5065.00334.50
5334.5089.00423.50

„Dlaczego wydatki.length > 0 przy średniej?"

Odp: Dzielenie przez 0! Jeśli tablica pusta → NaN. Zabezpieczamy.

7
Alert budżetowy + CSS
▼

„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."

Gotowe! Tracker działa: dodawanie, usuwanie, wyszukiwarka, podsumowanie (.reduce), alert warunkowy.

#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ć

Sprawdzian z lekcji 05:00
1
Co oznacza pusty array [] jako drugi argument useEffect?
2
Jak poprawnie zapisać tablicę do localStorage?
3
Co robi metoda .reduce((suma, el) => suma + el.kwota, 0) na tablicy wydatków?
4
Który zapis poprawnie renderuje element warunkowo w JSX?
5
Kiedy uruchomi się useEffect z zależnością [wydatki]?

🔥 Rozgrzewka — Dziennik ocen

Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.

kod
Jak działa metoda .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ę.
kod
Jak usunąć duplikaty z tablicy za pomocą 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))].
kod
Jak posortować tablicę obiektów, np. po nazwisku?
Używamy .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(...).
koncepcja
Jak edytować istniejący element w tablicy w stanie React?
Używamy .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.
koncepcja
Czym różni się .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

Czwarte ćwiczenie! Tym razem nie tylko dodajesz i usuwasz — nauczysz się edytować istniejące elementy, sortować tabelę i wyciągać unikalne wartości z tablicy.

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

  1. Wyświetlanie ocen w tabeli z kolumnami: Uczeń, Przedmiot, Ocena (badge kolorowy), Data, Akcje
  2. Dodawanie nowej oceny — formularz: imię ucznia (text), przedmiot (text), ocena (select 1-6)
  3. Edycja istniejącej oceny — kliknięcie ✎ wypełnia formularz, przycisk zmienia tekst na „Zapisz"
  4. Usuwanie oceny przyciskiem ✕
  5. Sortowanie — kliknięcie w nagłówek kolumny sortuje rosnąco/malejąco (▲/▼)
  6. Filtrowanie po przedmiocie — select z dynamicznie generowanymi opcjami
  7. Karta podsumowania — liczba ocen, średnia (.toFixed(2)), liczba przedmiotów
  8. Alert ostrzegawczy gdy średnia < 3.0

Przykłady Bootstrap do użycia

ElementKlasy Bootstrap
Tabelatable table-striped
Badge ocenybadge bg-danger / bg-warning / bg-success / bg-primary
Karta podsumowaniacard, card-body, card-title
Alertalert alert-danger
Przycisk dodajbtn btn-primary
Przycisk zapisz (edycja)btn btn-warning
Przycisk anulujbtn btn-secondary

#04 — Wprowadzenie do lekcji

Siema Maks! 👋 Dzisiaj robimy Dziennik ocen — to będzie najtrudniejsze jak dotąd, bo nauczysz się edytować istniejące elementy w tablicy, sortować tabelę klikając w nagłówki i wyciągać unikalne wartości z danych.

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

Największa nowość: edycja istniejącego obiektu. Do tej pory tylko dodawałeś i usuwałeś — teraz nauczysz się zmieniać dane „w miejscu" za pomocą .find() i trybu edycji.
Nowa umiejętnośćDo czego
.find()Znajduje obiekt w tablicy po id — żeby wypełnić formularz edycji
edytowanyId statePrzechowuje 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 przyciskuTen 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
})
💡 Nigdy nie sortuj oryginału! Zawsze [...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
Pytania na rozgrzewkę:
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ń

Sprawdzaj tę listę po każdym kroku. Jak wszystko zaznaczysz — znaczy, że apka jest gotowa! 🎯
#WymaganieKrok✓
1Projekt Vite + Bootstrap działa0☐
2Dane początkowe (6 obiektów) zdefiniowane poza komponentem1☐
3useState dla oceny[], nowyUczen, nowyPrzedmiot, nowaOcena1☐
4Formularz — 2× input text + select 1-6 + przycisk2☐
5Controlled inputs — value + onChange2☐
6dodaj() — walidacja pustych pól3☐
7dodaj() — nowy obiekt z Date.now() id + toISOString date3☐
8dodaj() — spread [...oceny, nowy] + czyszczenie formularza3☐
9Tabela z .map() — uczeń, przedmiot, ocena (badge), data, akcje4☐
10kolorOceny() — badge czerwony/żółty/zielony/niebieski4☐
11usun(id) — .filter() + clear edit state4☐
12edytuj(id) — .find() wypełnia formularz5☐
13edytowanyId state — null = dodawanie, id = edycja5☐
14dodaj() obsługuje oba tryby (add / save edit)5☐
15Przycisk „Dodaj" / „Zapisz" + „Anuluj" w trybie edycji5☐
16zmienSort() — sortKolumna + sortRosnaco state6☐
17Klikalne nagłówki tabeli z ▲/▼6☐
18[...przefiltrowane].sort() — kopia!6☐
19Filtr po przedmiocie — dynamiczny select z [...new Set()]7☐
20Karta podsumowania — ilość, średnia toFixed(2), ile przedmiotów7☐
21Alert ostrzegawczy gdy średnia < 3.07☐

#04 — Budowa krok po kroku

0 / 8

Standardowy start — tworzysz projekt Vite i dodajesz Bootstrap. Znasz to już z trzech poprzednich ćwiczeń.
npm create vite@latest dziennik -- --template react
cd dziennik
npm install
npm install bootstrap
Otwierasz 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
💡 W tym ćwiczeniu nie potrzebujesz App.css — wszystko stylujemy klasami Bootstrap! Możesz usunąć import CSS.
Sprawdź: widzisz „Dziennik ocen" w przeglądarce po npm run dev?

Dane początkowe definiujesz poza komponentem — tak jak w #03. Czemu? Bo to stałe dane startowe, nie zmieniają się i nie potrzebują re-renderowania.
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' },
]
Teraz state wewnątrz 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>
  )
}
Pytanie: dlaczego nowaOcena inicjalizujemy na 3, a nie na ''?
Bo ocena to number! Select zwróci number, więc wartość domyślna też musi być number. Gdybyśmy dali '' to porównanie by się psuło.

Formularz ma 3 pola: tekst (uczeń), tekst (przedmiot) i select (ocena 1-6). Wszystko w jednym wierszu dzięki Bootstrap 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>
💡 Zwróć uwagę na Number(e.target.value) w onChange selecta! Bez Number() dostalibyśmy string „3" zamiast number 3.
Spróbuj wpisać coś w pola — wartości powinny się zmieniać (sprawdź React DevTools).

Funkcja 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)
}
Nowość! Co robi 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ę!
💡 Walidacja .trim() === '' sprawdza, czy pole nie jest puste (ani same spacje). Nie walidujemy oceny — select wymusza wartość 1-6.
Dodaj ocenę i sprawdź: oceny.length powinno zwiększyć się o 1.

Teraz wyświetlamy dane w tabeli Bootstrap. Ocenę pokazujemy jako kolorowy badge — czerwony dla niskich, zielony dla wysokich.

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>
💡 Backticki w className={`badge ${kolorOceny(o.ocena)}`} to template literal — łączysz stałą klasę „badge" ze zmienną klasą koloru.
Sprawdź: tabela wyświetla 6 ocen? Badge oceny 2 jest czerwony, 3 żółty, 4-5 zielony? Usuwanie działa?

To jest najważniejszy krok #04 — dodajesz możliwość edytowania istniejących ocen. Potrzebujesz nowego state i dwóch zmian: w formularzu i w funkcji dodaj().

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)
}
Co robi .find() tutaj?
Szuka w tablicy 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)
}
Kluczowa linia: 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>
Przetestuj pełny cykl: kliknij ✎ → formularz się wypełni → zmień ocenę → kliknij „Zapisz" → dane się zaktualizują?
Sprawdź też: kliknij ✎ → potem „Anuluj" → formularz się czyści i przycisk wraca na „Dodaj"?

Druga wielka nowość! Klikasz w nagłówek kolumny → tabela sortuje się po tej kolumnie. Klikniesz jeszcze raz → zmieni kierunek (rosnąco ↔ malejąco).

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)
  }
}
💡 Gdy klikasz tę samą kolumnę — odwracamy kierunek. Gdy inną — ustawiamy nową kolumnę i resetujemy na rosnąco.

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
})
Co robi a[sortKolumna]?
To bracket notation! Gdy 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>
💡 Ternary w ternary: sortKolumna === 'uczen' ? (sortRosnaco ? '▲' : '▼') : '' → jeśli ta kolumna jest aktywna → pokaż strzałkę (góra/dół), jeśli nie → nic.
Kliknij „Uczeń" → widać ▲? Kliknij jeszcze raz → ▼? Kliknij „Ocena" → sortuje po ocenie rosnąco?

Ostatni krok! Dodajemy: filtr po przedmiocie (z dynamicznym selectem), kartę podsumowania i alert gdy średnia jest za niska.

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
Widzisz pipeline? 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>
Dynamiczny select! Opcje generują się automatycznie z 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.
Finalne testy:
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

Poniżej kompletny kod aplikacji Dziennik ocen. W tym ćwiczeniu nie potrzebujesz pliku App.css — wszystko stylujemy klasami Bootstrap.

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
To najdłuższa apka z całej serii — ale każdy klocek znasz z wcześniejszych ćwiczeń! Nowe są: .find(), .sort(), [...new Set()] i wzorzec edycji z edytowanyId. Brawo! 🎉

📝 Sprawdzian — Dziennik ocen

5 pytań · 5 minut · minimum 60% żeby zdać

Sprawdzian z lekcji 05:00
1
Czym różni się .find() od .filter()?
2
Dlaczego przed .sort() stosujemy operator spread [...tablica]?
3
Jak uzyskać tablicę unikalnych przedmiotów z tablicy ocen?
4
Jak poprawnie edytować jeden element w tablicy stanu za pomocą .map()?
5
Dlaczego w React nie wolno bezpośrednio modyfikować (mutować) stanu?

📝 Ściąga React

Nowy projekt

npm create vite@latest app -- --template react
cd app; npm install

Import 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

❌ class zamiast className
// ❌ <div class="box">  ✅ <div className="box">
❌ for zamiast htmlFor
// ❌ <label for="x">  ✅ <label htmlFor="x">
❌ onClick={func()} — wywołanie
// ❌ onClick={pobierz(id)}       → od razu
// ✅ onClick={() => pobierz(id)} → po kliknięciu
❌ Mutacja stanu (++, push, splice)
// ❌ z.downloads++          ✅ { ...z, downloads: z.downloads + 1 }
// ❌ arr.push(x)            ✅ [...arr, x]
// ❌ arr.splice(i, 1)       ✅ arr.filter(el => el.id !== id)
❌ Brak key w .map()
// ❌ {l.map(el => <div>...</div>)}
// ✅ {l.map(el => <div key={el.id}>...</div>)}
❌ Obrazy 404

Muszą być w public/assets/. Ścieżka: /assets/obraz1.jpg.

❌ Brak importu Bootstrap
import 'bootstrap/dist/css/bootstrap.css'
❌ Niezamknięty tag
// ❌ <img src="x">  ✅ <img src="x" />
❌ checked bez onChange
// ❌ <input checked={x} />
// ✅ <input checked={x} onChange={e => setX(e.target.checked)} />
❌ e.target.value to string
// 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

CechaAngularReact
TypPełny frameworkBiblioteka UI
JęzykTypeScript (wymagany)JS/TS (opcjonalny)
SzablonyHTML + dyrektywyJSX
Two-way bindingWbudowany [(ngModel)]Ręczny (value + onChange)
RoutingWbudowanyZewnętrzny (react-router)
DI (Dependency Injection)WbudowanyBrak (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

KomendaOpis
ng new nazwaNowy projekt
ng serveSerwer deweloperski
ng generate component nazwaGeneruj komponent
ng generate service nazwaGeneruj serwis
ng buildBuild produkcyjny
ng testUruchom 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

ReactAngular
value={x} onChange={e => setX(e.target.value)}[(ngModel)]="x"
💡 Two-way binding w Angular = 1 linia kodu. W React potrzebujesz useState + onChange.

📌 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

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

WalidatorUżycie
requiredPole wymagane
emailFormat 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

ReactAngular
useEffect + fetchngOnInit + HttpClient.subscribe
PromiseObservable (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ć

Sprawdzian z lekcji 10:00
1
Jak iterować po tablicy w Angular?
2
Jak warunkowo renderować element?
3
Property binding dla atrybutu src:
4
Event binding dla kliknięcia:
5
Klasy warunkowe w Angular:

🔥 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ć

Sprawdzian z lekcji 10:00
1
Two-way binding dla inputa:
2
Jaki moduł dla ngModel?
3
Niemutujące dodanie do tablicy:
4
Obsługa submit formularza:
5
Filtrowanie tablicy:

🔥 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ć

Sprawdzian z lekcji 10:00
1
Sumowanie tablicy obiektów:
2
Pipe formatowania waluty:
3
Getter w klasie TypeScript:
4
Select z ngModel binding:
5
Max z tablicy liczb:

🔥 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ć

Sprawdzian z lekcji 10:00
1
Generowanie serwisu CLI:
2
Dekorator serwisu singleton:
3
Wstrzykiwanie serwisu:
4
Sortowanie alfabetyczne stringów:
5
Znajdowanie elementu w tablicy:

📋 Ściąga Angular

Nowy projekt

ng new nazwa
ng serve

Generowanie

ng g c nazwa    # component
ng g s nazwa    # service
ng g m nazwa    # module

Interpolacja

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

❌ Brak FormsModule
Can't bind to 'ngModel' since it isn't a known property
→ Dodaj FormsModule do imports w module
❌ *ngFor bez let
// ❌ *ngFor="x of arr"
// ✅ *ngFor="let x of arr"
❌ Brak deklaracji komponentu
Component X is not part of any NgModule
→ Dodaj do declarations w @NgModule
❌ Mutacja danych
// ❌ this.arr.push(x)
// ✅ this.arr = [...this.arr, x]
❌ Brak @Injectable
No provider for XService
→ Dodaj @Injectable({providedIn:'root'})
❌ Literówka w selektorze
// ❌ <app-Header> (wielkość liter!)
// ✅ <app-header>
❌ Brak async pipe dla Observable
// ❌ *ngFor="let x of data$"
// ✅ *ngFor="let x of data$ | async"
❌ trackBy - zła sygnatura
// ✅ 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

TechnologiaTypJęzykZastosowanie
JavaFXDesktopJavaAplikacje okienkowe Windows/Mac/Linux
SwingDesktopJavaStarsze aplikacje desktop (nadal na egzaminie)
ReactWebJavaScriptStrony i aplikacje webowe
AndroidMobileJava/KotlinAplikacje na telefony

Dlaczego JavaFX na egzaminie?

INF.04 — Część praktyczna

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ęcieOpis
StageOkno aplikacji (główne lub dialog)
SceneZawartość okna (kontrolki + layout)
NodeDowolny element UI (Button, Label, VBox...)
ControllerKlasa z logiką obsługi zdarzeń
FXMLPlik 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

Zadanie: Wymień 3 główne elementy architektury JavaFX (Stage, Scene, ?). Co jest odpowiednikiem "komponentu" z React?
Pokaż rozwiązanie

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

  1. File → New Project
  2. Wybierz JavaFX → JavaFX FXML Application
  3. Podaj nazwę projektu (np. Kalkulator)
  4. 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

  1. Kliknij prawym na plik .fxml
  2. Wybierz Open in Scene Builder
  3. Przeciągaj kontrolki z lewego panelu na scenę

Główne sekcje Scene Buildera

SekcjaOpis
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

Zadanie: Wymień 4 główne sekcje Scene Buildera i opisz do czego służą.
Pokaż rozwiązanie
  1. Library — paleta kontrolek (Button, Label, TextField...)
  2. Hierarchy — drzewo elementów, pokazuje strukturę zagnieżdżenia
  3. Content — podgląd projektowanego interfejsu
  4. 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

PlikRola
MojProjekt.javaPunkt wejścia, ładuje FXML i pokazuje okno
FXMLDocument.fxmlDefinicja UI w XML (kontrolki, layout)
FXMLDocumentController.javaObsł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>
Ważne! Atrybut fx:controller musi wskazywać pełną nazwę klasy controllera (z pakietem).

📝 Zadanie do teorii

Zadanie: Co oznacza atrybut fx:controller w pliku FXML? Co się stanie, jeśli go pominiesz?
Pokaż rozwiązanie

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

LayoutRozmieszczenieZastosowanie
VBoxPionowo (w kolumnie)Formularz, lista opcji
HBoxPoziomo (w wierszu)Pasek narzędzi, przyciski obok siebie
GridPaneSiatka (wiersze × kolumny)Kalkulator, formularze
BorderPane5 stref (top, left, center, right, bottom)Główny layout aplikacji
AnchorPanePozycjonowanie względem krawędziDokładne pozycjonowanie
StackPaneWarstwy (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

Zadanie: Jaki layout wybierzesz dla kalkulatora (16 przycisków 0-9 i operacje)? Uzasadnij.
Pokaż rozwiązanie

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śćMetodaOpis
TekstsetText() / getText()Napis na przycisku
WyłączonysetDisable(true/false)Szary, nieklikany
WidocznysetVisible(true/false)Ukryj/pokaż
StylsetStyle("-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;"/>
Porównanie z React:

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

Zadanie: Napisz kod Java, który po kliknięciu przycisku zmieni tekst Label na "Witaj [imię]!" gdzie imię pobierzesz z TextField.
Pokaż rozwiązanie
@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

Zadanie: Jaka jest różnica między getText() a getSelectedText()? Kiedy użyjesz każdej?
Pokaż rozwiązanie
  • getText() — zwraca CAŁY tekst z pola
  • getSelectedText() — 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"
Ważne! RadioButtony muszą być w tej samej ToggleGroup, żeby działało "wybierz jeden".

📝 Zadanie do teorii

Zadanie: Masz formularz zamówienia pizzy: rozmiar (mała/średnia/duża) i dodatki (ser, szynka, ananas). Które kontrolki użyjesz i dlaczego?
Pokaż rozwiązanie
  • 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

CechaComboBoxChoiceBox
EdytowalnyTak (można wpisać)Nie
PlaceholderpromptTextBrak
WydajnośćLepsza dla dużych listDla małych list

📝 Zadanie do teorii

Zadanie: Jak dodać nową opcję do ComboBox w trakcie działania programu?
Pokaż rozwiązanie
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ń

ZdarzenieKontrolkaKiedy
onActionButton, TextField (Enter)Kliknięcie / Enter
onKeyPressedWszystkieNaciśnięcie klawisza
onMouseClickedWszystkieKliknięcie myszą
setOnActionComboBox, ChoiceBoxZmiana 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

Zadanie: Masz 10 przycisków cyfr (0-9) w kalkulatorze. Czy lepiej utworzyć 10 metod czy jedną? Jak to zaimplementujesz?
Pokaż rozwiązanie

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(...)
Częste błędy:
  • Brak @FXML przed polem/metodą → NullPointerException
  • Literówka w fx:id vs nazwa pola → NullPointerException
  • Brak # przed nazwą metody w onAction

📝 Zadanie do teorii

Zadanie: Co oznacza adnotacja @FXML? Co się stanie, jeśli ją pominiesz?
Pokaż rozwiązanie

@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

  1. Jaki layout najlepiej użyć do siatki przycisków kalkulatora?
  2. Jak pobrać tekst z TextField?
  3. Co oznacza @FXML?
  4. Jak obsłużyć błąd konwersji String na double?
Odpowiedzi
  1. GridPane — siatka wierszy i kolumn
  2. txtPole.getText()
  3. Adnotacja łącząca pole/metodę z elementem FXML
  4. try-catch dla NumberFormatException

📋 Kalkulator — Instrukcja

„To Twoje egzaminowe zadanie. Przeczytaj uważnie wymagania."

Treść zadania

Zadanie egzaminowe INF.04

Wykonaj aplikację okienkową "Kalkulator" spełniającą wymagania:

  1. Interfejs zawiera: pole wyświetlacza, przyciski cyfr 0-9, przyciski operacji (+, -, *, /), przycisk "=" i "C"
  2. Kliknięcie cyfry dopisuje ją do wyświetlacza
  3. Kliknięcie operacji zapamiętuje liczbę i operację
  4. Kliknięcie "=" wykonuje obliczenie i wyświetla wynik
  5. Kliknięcie "C" czyści wyświetlacz
  6. Obsłuż dzielenie przez zero (komunikat błędu)

Wymagane komponenty

Kontrolkafx:idOpis
TextFieldtxtWyswietlaczWyświetlacz (tylko odczyt)
Button ×10btn0-btn9Cyfry
Button ×4btnDodaj, btnOdejmij, btnMnoz, btnDzielOperacje
ButtonbtnRownaOblicz wynik
ButtonbtnCzyscWyczyść

🏗️ Kalkulator — Budowa UI

„Zaczynamy od Scene Buildera. Będziemy budować interfejs krok po kroku."

Krok 1: Utwórz projekt

  1. File → New Project → JavaFX FXML Application
  2. Nazwa: Kalkulator
  3. Otwórz FXMLDocument.fxml w Scene Builder

Krok 2: Struktura layoutu

  1. Usuń domyślny AnchorPane, dodaj VBox
  2. W VBox dodaj TextField (wyświetlacz)
  3. 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

TestOczekiwany 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
CWyś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

  1. Jak dodać element do ListView?
  2. Jak pobrać zaznaczony element?
  3. Jak usunąć element z listy?
Odpowiedzi
  1. listView.getItems().add("element")
  2. listView.getSelectionModel().getSelectedItem()
  3. listView.getItems().remove(obiekt) lub .remove(index)

📋 Lista zadań — Instrukcja

„Klasyczna aplikacja Todo — idealna do nauki CRUD w JavaFX."

Treść zadania

Zadanie

Wykonaj aplikację "Lista zadań do zrobienia":

  1. Pole tekstowe do wpisania nowego zadania
  2. Przycisk "Dodaj" — dodaje zadanie do listy
  3. ListView wyświetlający wszystkie zadania
  4. Przycisk "Usuń" — usuwa zaznaczone zadanie
  5. Przycisk "Wyczyść wszystko" — usuwa wszystkie zadania
  6. 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

TestOczekiwany wynik
Dodaj "Test""Test" pojawia się na liście
Dodaj pusteAlert z błędem
Zaznacz i usuńElement znika z listy
Usuń bez zaznaczeniaAlert z błędem
Wyczyść wszystkoLista pusta

🔥 Rejestr produktów — Rozgrzewka

„To zadanie wymaga TableView — tabeli z kolumnami. Powtórzmy jak działa."

Mini Quiz

  1. Jak powiązać kolumnę TableView z polem obiektu?
  2. Jaka klasa reprezentuje wiersz w tabeli?
  3. Jak dodać wiersz do tabeli?
Odpowiedzi
  1. kolumna.setCellValueFactory(new PropertyValueFactory<>("nazwaPolaWKlasie"))
  2. Własna klasa (np. Produkt) z polami i getterami
  3. tableView.getItems().add(new Produkt(...))

📋 Rejestr produktów — Instrukcja

Treść zadania

Zadanie

Wykonaj aplikację "Rejestr produktów":

  1. Pola: nazwa (TextField), cena (TextField), ilość (TextField)
  2. TableView z kolumnami: Nazwa, Cena, Ilość, Wartość (cena×ilość)
  3. Przycisk "Dodaj" — dodaje produkt do tabeli
  4. Przycisk "Usuń" — usuwa zaznaczony produkt
  5. Label pokazujący sumę wartości wszystkich produktów
  6. 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

TestOczekiwany wynik
Dodaj "Jabłko", 2.50, 10Wiersz w tabeli, Wartość=25.00
Dodaj "Mleko", 3.00, 5Drugi wiersz, Suma=40.00 zł
Dodaj z tekstem w cenieAlert z błędem
Usuń zaznaczonyWiersz znika, suma się aktualizuje

🔥 Dziennik ocen — Rozgrzewka

„To zadanie łączy ComboBox, TableView i obliczenia. Powtórzmy ComboBox."

Mini Quiz

  1. Jak dodać opcje do ComboBox w metodzie initialize?
  2. Jak pobrać wybraną wartość z ComboBox?
  3. Jak obliczyć średnią z listy liczb?
Odpowiedzi
  1. cmbPrzedmiot.getItems().addAll("Matematyka", "Fizyka")
  2. cmbPrzedmiot.getValue()
  3. Suma wszystkich / ilość elementów

📋 Dziennik ocen — Instrukcja

Treść zadania

Zadanie

Wykonaj aplikację "Dziennik ocen ucznia":

  1. ComboBox z przedmiotami (Matematyka, Fizyka, Informatyka, Polski, Angielski)
  2. Pole tekstowe na imię ucznia
  3. ComboBox lub RadioButton na ocenę (1-6)
  4. TableView: Uczeń, Przedmiot, Ocena
  5. Przycisk "Dodaj ocenę"
  6. Label ze średnią wszystkich ocen
  7. 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

TestOczekiwany wynik
Dodaj "Jan", Matematyka, 5Wiersz w tabeli
Dodaj "Anna", Fizyka, 4Drugi wiersz, średnia=4.50
Filtruj: MatematykaTylko oceny z matematyki
Filtruj: WszystkieWszystkie oceny widoczne
Dodaj bez przedmiotuAlert z błędem

📜 JavaFX — Ściąga

Podstawowe kontrolki

KontrolkaTworzeniePobieranie 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

LayoutOpisKluczowe atrybuty
VBoxPionowospacing, alignment
HBoxPoziomospacing, alignment
GridPaneSiatkahgap, vgap, rowIndex, columnIndex
BorderPane5 streftop, left, center, right, bottom
AnchorPaneKotwiczenietopAnchor, 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

❌ txtPole jest null

Przyczyna: Brak @FXML lub literówka w fx:id

// ✅ Poprawnie:
@FXML private TextField txtPole;  // fx:id="txtPole" w FXML

NumberFormatException

❌ Błąd przy parsowaniu liczby

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

❌ Kliknięcie nie wywołuje metody

Przyczyny:

  • Brak # przed nazwą metody: onAction="#handleKlik"
  • Brak @FXML przed metodą
  • Literówka w nazwie metody

RadioButton nie grupują się

❌ Można zaznaczyć wiele RadioButton

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

❌ Tabela pusta mimo dodania danych

Przyczyny:

  • Brak setCellValueFactory dla kolumn
  • Nazwa w PropertyValueFactory nie zgadza się z getterem
// Klasa: getNazwa() → PropertyValueFactory("nazwa")
// ✅ "nazwa" = getNazwa (bez "get", małą literą)

Brak fx:controller

❌ Kontrolki się wyświetlają, ale nic nie działa

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

TechnologiaPlatformaJęzyk
Android (natywny)AndroidJava / Kotlin
iOS (natywny)iPhone/iPadSwift
React NativeAndroid + iOSJavaScript
FlutterAndroid + iOSDart

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

  1. File → New Project
  2. Wybierz Empty Activity
  3. Podaj nazwę (np. MojaAplikacja)
  4. Language: Java (lub Kotlin)
  5. Minimum SDK: API 21 (Android 5.0)

💡 Na egzaminie wybierz API 21 lub wyższe — obsługuje 98% urządzeń.

📝 Zadanie do teorii

Zadanie: Wymień 3 różnice między Android Studio a NetBeans.
Pokaż rozwiązanie
  1. Android Studio → aplikacje mobilne, NetBeans → desktop i web
  2. Android Studio używa Gradle, NetBeans używa Ant/Maven
  3. 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

PlikRola
MainActivity.javaGłówna klasa — odpowiednik Main w JavaFX
activity_main.xmlUkład UI — odpowiednik FXML
strings.xmlWszystkie teksty (dla wielojęzyczności)
AndroidManifest.xmlUprawnienia, 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

Zadanie: Co oznacza setContentView(R.layout.activity_main)?
Pokaż rozwiązanie

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

LayoutOpisOdpowiednik JavaFX
LinearLayoutPionowo lub poziomoVBox / HBox
ConstraintLayoutWzględne pozycjonowanieAnchorPane
RelativeLayoutPozycja względem innych—
FrameLayoutJeden na drugimStackPane
GridLayoutSiatkaGridPane

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_parentWypełnij rodzica (100%)
wrap_contentDopasuj do zawartości
100dpStał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

Zadanie: Jaka jest różnica między match_parent a wrap_content?
Pokaż rozwiązanie
  • match_parent — element zajmuje całą dostępną przestrzeń rodzica
  • wrap_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

inputTypeKlawiatura
textStandardowa
numberTylko cyfry
numberDecimalCyfry + przecinek
textPasswordHasło (ukryte)
textEmailAddressEmail (z @)
phoneNumer telefonu

📝 Zadanie do teorii

Zadanie: Jak pobrać liczbę z EditText i obsłużyć błąd konwersji?
Pokaż rozwiązanie
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);
Obrazki w Android

Umieść pliki PNG/JPG w folderze res/drawable/. Odwołuj się przez @drawable/nazwa (bez rozszerzenia).

📝 Zadanie do teorii

Zadanie: Który sposób obsługi kliknięcia jest zalecany dla wielu przycisków i dlaczego?
Pokaż rozwiązanie

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

Zadanie: Co się stanie, jeśli zapomnisz wywołać findViewById() dla kontrolki?
Pokaż rozwiązanie

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/

FolderZawartość
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>
Dlaczego zasoby?
  • Łatwe tłumaczenie — dodaj folder values-pl/
  • Jedna zmiana = wszędzie zmienione
  • Różne zasoby dla różnych ekranów

📝 Zadanie do teorii

Zadanie: Jak odwołać się do koloru primary z colors.xml w layoucie XML i w kodzie Java?
Pokaż rozwiązanie
<!-- 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)

AtrybutZnaczenie
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

Zadanie: Jak wyśrodkować przycisk w środku ekranu (pionowo i poziomo)?
Pokaż rozwiązanie
<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();
Ważne! Po każdej zmianie listy wywołaj 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

Zadanie: Co się stanie, jeśli zapomnisz wywołać notifyDataSetChanged() po dodaniu elementu?
Pokaż rozwiązanie

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

OperacjaJavaKotlin
ZmiennaString s = "tekst";val s = "tekst"
Zmienna (edytowalna)int x = 5;var x = 5
Null safetyMoże być null!String? (explicit)
Lambdav -> {...}{ 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

Zadanie: Wymień 2 zalety Kotlina nad Javą.
Pokaż rozwiązanie
  1. Null safety — kompilator wymusza obsługę nulli
  2. 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

Materiał własny MoreTutor · zadanie w formacie szkolnego sprawdzianuŁączy wszystkie trzy tematy sprawdzianu w jednej aplikacji: dwie tablice równoległe, tablica CheckBoxów i RadioGroup spięta z tablicą zniżek.

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)

WarsztatCena
Fotografia120 zł
Programowanie180 zł
Robotyka150 zł
Grafika 3D200 zł
Kategoria zniżkiZniżka
Uczeń szkoły20%
Absolwent10%
Osoba z zewnątrz0%

Wymagania

  • Cztery CheckBox — warsztaty. Nazwy i ceny trzymasz w dwóch tablicach równoległych, nie w sztywnych if-ach.
  • RadioGroup z trzema RadioButton — 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 — Toast z 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 coPkt
Layout: root z android:id="@+id/main", komplet kontrolek z identyfikatorami3
Trzy tablice danych: nazwy, ceny, zniżki3
Tablica CheckBox[] wypełniona przez findViewById2
Pętla: licznik zaznaczonych, suma cen, lista nazw po przecinku4
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łkowitego2
Przycisk Wyczyść działający pętlą po tablicy1
Zanim zaczniesz pisać kod Zbuduj projekt (Build → Make Project) po dodaniu kontrolek do XML-a. Dopóki klasa R nie powstanie, każde R.id. będzie czerwone i zaczniesz szukać błędu tam, gdzie go nie ma.
Do wklejenia na czat Meet — treść zadania
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

0/8 kroków

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.

0
Projekt i root layoutu
▼

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">
Nie zgub 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".
1
Kontrolki w XML
▼

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.

Kontrolkaidtext
CheckBoxcbFotoFotografia (120 zł)
CheckBoxcbProgProgramowanie (180 zł)
CheckBoxcbRoboRobotyka (150 zł)
CheckBoxcbGrafGrafika 3D (200 zł)
RadioGrouprgZnizka— (orientation="vertical")
RadioButtonrbUczenUczeń szkoły (-20%)
RadioButtonrbAbsolwentAbsolwent (-10%)
RadioButtonrbGoscOsoba z zewnątrz (0%)
ButtonbtnPodsumujPodsumuj
ButtonbtnWyczyscWyczyść
TextViewtxtWynik— (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.

2
Tablice danych jako pola klasy
▼

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;
Dlaczego 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.
3
OnCreate: tablica CheckBoxów i listenery
▼

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());
}
Gdzie są RadioButtony? Tutaj ich nie pobieramy — i to jest poprawne

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.

4
Pętla: licznik, suma, nazwy, najdroższy
▼

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;
        }
    }
}
Najdroższy z zaznaczonych, nie z całej tablicy Start od 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 „, ".

5
Zniżka z RadioGroup przez indeks w tablicy
▼

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();
Wariant prostszy, też punktowany Jeśli 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.
6
Kwota do zapłaty i złożenie wyniku
▼
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.

7
Wyczyść i testy końcowe
▼
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 klikaszCo ma się stać
Fotografia + Robotyka, uczeń szkoły2 z 4, najdroższy Robotyka (150 zł), suma 270 zł, do zapłaty 216 zł
wszystkie cztery, absolwent4 z 4, najdroższy Grafika 3D (200 zł), suma 650 zł, do zapłaty 585 zł
Programowanie, osoba z zewnątrz1 z 4, najdroższy Programowanie (180 zł), suma 180 zł, do zapłaty 180 zł
nic nie zaznaczone, przycisk PodsumujToast „Zaznacz co najmniej jeden warsztat", TextView bez zmian
zaznaczony warsztat, żadnej kategoriiToast „Wybierz kategorię zniżki", nic nie jest liczone
Wyczyść po podsumowaniuwszystkie 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("");
    }
}
Nazwa pakietu Pierwsza linia musi zgadzać się z Twoim projektem. Przy projekcie 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

ObjawPrzyczyna
Aplikacja pada przy starcieRoot layoutu bez android:id="@+id/main"
Widać tylko pierwszy CheckBox, reszta poza ekranemLinearLayout bez android:orientation="vertical" — domyślnie układa dzieci poziomo
Pada przy pierwszym kliknięciu Podsumujboxy[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 niskaDzielenie 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 kategoriiKolejność w tablicy ZNIZKI inna niż kolejność RadioButtonów w XML-u
Ceny nie pasują do warsztatówKolejność 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.

Zakres: tablica jako struktura w kodzie Ten dział dotyczy tablic w kodzie Javy — deklaracji, indeksów, pętli, operacji na wartościach. Nie chodzi o wyświetlanie danych na ekranie: 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;
Rozmiar jest stały Po utworzeniu tablicy nie dołożysz do niej elementu. 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
ZapisDotyczyUwaga
tablica.lengthtablicapole, bez nawiasów
napis.length()Stringmetoda, z nawiasami
lista.size()ArrayListmetoda

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
Dwa błędy, które kosztują punkt za każdym razem 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.

Do wklejenia na czat Meet — zadania 11.1-11.6 (tablice)
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]?

ZapisWynik
liczby.length6
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 paleciePozycjaCo ląduje w XMLDo czego
TextTextView<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 inputTypezmienia klawiaturę i walidację wpisu
Multiline Text<EditText android:inputType="textMultiLine">pole wielolinijkowe
ButtonsButton<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
LayoutsLinearLayout, ConstraintLayout<LinearLayout> itd.układ, w którym siedzą kontrolki
LegacyListView, GridView, Spinner<ListView> itd.starsze listy — poza zakresem tego sprawdzianu
CheckBox i RadioButton szukaj w kategorii Buttons, nie Widgets Obie kontrolki dziedziczą po 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.
TextView czy EditText — klasyczne pytanie ze sprawdzianu TextView tylko pokazuje tekst; użytkownik nie może w nim pisać. EditText (w palecie: Plain Text, Password, E-mail, Number…) to pole do wpisywania. Jeśli w zadaniu pada „użytkownik wpisuje", to zawsze 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

CheckBoxRadioButtonSwitch
Ile można zaznaczyćdowolnie wielejeden w grupiestan włącz/wyłącz
Wymaga konteneranietak — RadioGroupnie
Odczyt stanucb.isChecked()rg.getCheckedRadioButtonId()sw.isChecked()
Odznaczenie z koducb.setChecked(false)rg.clearCheck()sw.setChecked(false)
Typowe użycieskładniki, zgody, filtryocena, płeć, jedna odpowiedź w quiziepowiadomienia, tryb ciemny

Cztery pułapki, które wchodzą na sprawdzian

1. RadioButton poza RadioGroup Da się zaznaczyć wszystkie naraz i żaden ich nie odznacza. Objaw wygląda jak błąd kodu, a przyczyna siedzi w XML-u.
2. Zapomniane -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.
3. Dwa różne interfejsy o tej samej nazwie metody 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ę.
4. Porównywanie tekstu zamiast ID 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

Root layoutu musi mieć 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);
    }
}
Co tu sprawdza nauczyciel Tablica 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

KrokCo robiszNa czym się wywala, gdy pominiesz
1W activity_main.xml ustawiasz root na LinearLayout z android:id="@+id/main" i orientation="vertical"Brak ID — NullPointerException przy starcie, w linii z insetami
2Wstawiasz kontrolki (paleta → Buttons: CheckBox, RadioGroup, RadioButton) i nadajesz każdej android:idBez ID nie ma R.id.…, kod się nie skompiluje
3Budujesz projekt (Build → Make Project), żeby powstała klasa RR.id.cbPizza świeci na czerwono, choć XML jest poprawny
4W onCreate, pod kodem z szablonu, robisz findViewById i wypełniasz tablicę boxyboxy[0] zostaje null — wyjątek przy pierwszym kliknięciu
5Piszesz pokazPodsumowanie() jako osobną metodę poza onCreateMetoda w metodzie — błąd składni, którego komunikat niewiele mówi
Czerwone 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ń.

Do wklejenia na czat Meet — zadania 12.1-12.5 (CheckBox, RadioButton)
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.

EkranKontrolkaDlaczego
(a) forma płatnościRadioButton w RadioGroupdokładnie jedna opcja
(b) dodatki do pizzyCheckBoxwiele naraz
(c) zgoda na regulaminCheckBoxpojedynczy stan tak/nie
(d) tryb ciemnySwitchwłącznik ustawienia, działa od razu
(e) odpowiedź w quizieRadioButton w RadioGroupjedna 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.

Do wklejenia na czat Meet — sprawdzian próbny - część zamknięta
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

123456789101112131415
bbbcdbbcbcabbcb

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

Sprawdzian próbny — Android 15:00
1
Ile elementów ma tablica int[] t = new int[5]; i jaka jest wartość t[0]?
2
Jaki jest ostatni poprawny indeks tablicy o 8 elementach?
3
Co zwróci liczby.length dla int[] liczby = {4, 8, 15};?
4
Który zapis jest poprawny dla tablicy, a który dla napisu?
5
Co się stanie po uruchomieniu int[] t = {1, 2, 3}; System.out.println(t[3]);?
6
Ile razy wykona się for (int i = 0; i < t.length; i++) dla tablicy o 6 elementach?
7
int[] a = {1,2,3}; int[] b = a; b[0] = 9; — ile wynosi a[0]?
8
Czym musi być otoczony RadioButton, żeby zaznaczał się tylko jeden?
9
Jak sprawdzić, czy CheckBox cbZgoda jest zaznaczony?
10
Co zwraca rg.getCheckedRadioButtonId(), gdy nic nie jest zaznaczone?
11
Jak odznaczyć wszystkie RadioButtony w grupie rgOcena?
12
Który atrybut XML zaznacza kontrolkę już przy starcie aplikacji?
13
Przeciągasz z palety pozycję Plain Text. Co pojawi się w XML?
14
W której kategorii palety znajdziesz CheckBox i RadioButton?
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"?

🔥 Kalkulator — Rozgrzewka

„Prosty kalkulator na liczbach całkowitych. Poznamy LinearLayout, Button i onClick."

Mini Quiz

  1. Czym różni się match_parent od wrap_content?
  2. Co robi layout_weight?
  3. Jak połączyć Button z XML z kodem Java?
  4. Co to jest setOnClickListener?
Odpowiedzi
  1. match_parent — rozciąga na całą szerokość rodzica; wrap_content — dopasowuje do treści
  2. Rozdziela dostępne miejsce proporcjonalnie między elementy
  3. Button btn = findViewById(R.id.mojPrzycisk);
  4. Metoda podpinająca kod do zdarzenia kliknięcia przycisku

📋 Kalkulator — Instrukcja

„Prosty kalkulator na liczbach całkowitych: + - * / = i C."

Treść zadania

Zadanie

Wykonaj aplikację "Kalkulator" — tylko liczby całkowite:

  1. Wyświetlacz (TextView) — pokazuje aktualną liczbę
  2. Przyciski cyfr 0–9
  3. 4 operacje: + − * /
  4. Przycisk = (oblicz wynik)
  5. Przycisk C (wyczyść wszystko)
  6. Dzielenie przez 0 → komunikat "Błąd"
  7. 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

ElementKolor tłaKolor tekstu
Cyfry (0-9)#333333#FFFFFF
Operacje (+ − * / =)#FF9500#FFFFFF
C (czyszczenie)#A5A5A5#000000

🏗️ Kalkulator — Budowa UI

activity_main.xml

Kluczowe zasady:

  • Root: LinearLayout (vertical) z padding="16dp"
  • Wyświetlacz: TextView z textSize="48sp", wyrównanie do prawej
  • 4 rzędy po 4 przyciski (siatka 4×4)
  • Każdy rząd: LinearLayout (horizontal) z layout_height="0dp" + layout_weight="1"
  • Każdy przycisk: layout_width="0dp" + layout_weight="1" (równa szerokość)
☐ Krok 1: Zmień root na LinearLayout (vertical)

Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout

Ustaw: orientation="vertical", padding="16dp"

☐ Krok 2: Dodaj wyświetlacz (TextView)

Palette → Text → TextView:

idwyswietlacz
text0
textSize48sp
textAlignmenttextEnd
layout_widthmatch_parent
padding20dp
layout_marginBottom16dp
☐ Krok 3: Rząd 1 — przyciski 7, 8, 9, /

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

idtextbackgroundTint
btn77#333333
btn88#333333
btn99#333333
btnDzielenie/#FF9500
☐ Krok 4: Rzędy 2, 3, 4 (kopiuj + zmień)

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)

☐ Krok 5: Pola klasy + findViewById
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);
☐ Krok 6: Obsługa cyfr + operacji + C
// 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;
});
☐ Krok 7: Uruchom i przetestuj

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ć

Sprawdzian z lekcji 05:00
1
Jak pobrać tekst wyświetlany na przycisku w Javie (Android)?
2
Co się stanie przy dzieleniu całkowitym 7 / 2 w Javie (typ int)?
3
Jak przypisać akcję do kliknięcia przycisku w Javie?
4
Dlaczego dzielenie przez 0 (dla typu int) wymaga obsługi wyjątku?
5
Jak zamienić String "42" na liczbę całkowitą w Javie?

Testy manualne

TestKrokiOczekiwany wynik
Dodawanie5 + 3 =8
Odejmowanie10 - 4 =6
Mnożenie6 * 7 =42
Dzielenie15 / 3 =5
Dzielenie całkowite7 / 2 =3
Dzielenie przez 05 / 0 =Błąd
CzyszczenieC0
Łańcuch2 + 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

  1. Czym jest SeekBar i do czego służy?
  2. Czym jest Spinner i jak działa?
  3. Jak ustawić tekst w TextView?
  4. Jaki wzór opisuje BMI?
Odpowiedzi
  1. SeekBar — suwak z wartością min-max. Nasłuchujemy zmian przez OnSeekBarChangeListener.
  2. Spinner — rozwijana lista opcji. Wypełniamy przez ArrayAdapter, nasłuchujemy przez OnItemSelectedListener.
  3. textView.setText("tekst")
  4. 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

Zadanie

Wykonaj aplikację "Kalkulator BMI" z komponentami SeekBar i Spinner:

  1. SeekBar do wyboru wagi (30–200 kg) — z etykietą pokazującą aktualną wartość
  2. Spinner do wyboru wzrostu (140–220 cm, co 1 cm)
  3. Przycisk "Oblicz BMI"
  4. Wyświetlenie wyniku BMI (liczba całkowita)
  5. Interpretacja wyniku (niedowaga/norma/nadwaga/otyłość) z kolorem

Nowe komponenty do nauki

KomponentOpisZastosowanie w projekcie
SeekBarSuwak z wartością liczbowąWybór wagi (30–200 kg)
SpinnerLista rozwijana (dropdown)Wybór wzrostu (140–220 cm)

Skala BMI

BMIKategoria
< 18.5Niedowaga
18.5 – 24.9Norma
25 – 29.9Nadwaga
≥ 30Otyłość

Kalkulator BMI — Budowa UI

☐ Krok 1: Utwórz projekt i zmień root na LinearLayout

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:

orientationvertical
padding24dp
Gdzie to znaleźć? Atrybut orientation jest w sekcji Common Attributes w panelu Attributes po prawej. Padding znajdziesz w sekcji Layout → Padding → kliknij all i wpisz 24dp.
☐ Krok 2: Dodaj tytuł (TextView)

Palette (lewy panel) → Text → przeciągnij TextView na ekran.

Kliknij na dodany TextView. W panelu Attributes po prawej ustaw:

AtrybutWartośćGdzie znaleźć
textKalkulator BMICommon Attributes → text
textSize24spCommon Attributes → textSize
textStyleboldCommon Attributes → textStyle → zaznacz B
layout_gravitycenterLayout → layout_gravity
layout_marginBottom24dpLayout → kliknij dolny margines
☐ Krok 3: Dodaj SeekBar do wagi

Najpierw dodaj etykietę. Palette → Text → TextView, przeciągnij pod tytuł:

AtrybutWartość
idtxtWagaLabel
textWaga: 70 kg
textSize18sp
layout_marginTop16dp

Teraz dodaj suwak. Palette → Widgets → SeekBar, przeciągnij pod etykietę:

AtrybutWartośćOpis
idseekWagaIdentyfikator suwaka
layout_widthmatch_parentPełna szerokość
max170Zakres: 0–170 (+ 30 = 30–200 kg)
progress40Domyślnie 70 kg (40 + 30)
Dlaczego max = 170? SeekBar zawsze zaczyna od 0. Chcemy zakres 30–200 kg, więc: max = 200 − 30 = 170. W kodzie dodamy 30 do wartości: waga = progress + 30.
☐ Krok 4: Dodaj Spinner do wzrostu

Dodaj etykietę. Palette → Text → TextView:

textWzrost:
textSize18sp
layout_marginTop24dp

Teraz dodaj Spinner. Palette → Containers → Spinner, przeciągnij pod etykietę:

AtrybutWartośćOpis
idspinnerWzrostIdentyfikator spinnera
layout_widthmatch_parentPełna szerokość
layout_marginTop8dpOdstęp od etykiety
Spinner znajdziesz w Palette → Containers (nie w Widgets!). Opcje do wyboru dodamy w kodzie Java — wypełnimy go wartościami 140–220 cm.
☐ Krok 5: Dodaj przycisk i TextViews wyniku

Palette → Text → TextView — dodaj dwa TextViews pod Spinnerem:

TextView 1 — wynik liczbowy:

idtxtWynik
textSize48sp
textStylebold
layout_gravitycenter
layout_marginTop32dp

TextView 2 — kategoria:

idtxtKategoria
textSize22sp
layout_gravitycenter
layout_marginTop8dp
☐ Krok 6: Sprawdź Component Tree

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)
Brak przycisku! BMI oblicza się automatycznie przy każdej zmianie suwaka lub spinnera — nie potrzebujemy przycisku "Oblicz".

Jeśli kolejność się nie zgadza — przeciągnij elementy w Component Tree we właściwe miejsce.

☐ Krok 7: Pola klasy + findViewById

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);
☐ Krok 8: Obsługa SeekBar (suwak wagi)

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) { }
});
Jak to działa? Gdy użytkownik przesuwa suwak, 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!
☐ Krok 9: Obsługa Spinner (wybór wzrostu)

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) { }
});
Jak to działa? 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!
☐ Krok 10: Metoda obliczBMI()

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);
}
Uwaga! Nie potrzebujemy już try-catch — SeekBar i Spinner zawsze dają poprawne wartości liczbowe. To wielka zaleta tych komponentów!
☐ Krok 11: Dodaj skalę BMI (SeekBar wynikowy)

Pod txtKategoria dodaj wizualną skalę BMI. W activity_main.xml (zakładka Code/Split):

Etykieta: Palette → Text → TextView:

textSkala BMI:
textSize14sp
layout_marginTop24dp

SeekBar wynikowy: Palette → Widgets → SeekBar:

idseekBmiSkala
layout_widthmatch_parent
max40
progress0
enabledfalse
enabled="false" — użytkownik nie może przesuwać tego suwaka! Służy tylko do wyświetlania. Skala: 0–40 odpowiada BMI 10–50.

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.

☐ Krok 12: Zaktualizuj kod — skala 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);
Jak to działa? BMI 10 = pozycja 0, BMI 25 = pozycja 15, BMI 50 = pozycja 40. Math.max/Math.min pilnują, żeby wartość nie wyszła poza zakres 0–40.
☐ Krok 13: Dodaj importy i uruchom

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ć

Sprawdzian z lekcji 05:00
1
SeekBar ma ustawiony max="170". Chcemy zakres wagi 30–200 kg. Jak obliczyć wagę z wartości progress?
2
Jak wypełnić Spinner listą wartości w kodzie Java?
3
Który interfejs nasłuchuje zmian wartości SeekBar?
4
Wzór na BMI to: waga(kg) / wzrost(m)². Osoba waży 80 kg i ma 200 cm wzrostu. Ile wynosi BMI?
5
Dlaczego przy SeekBar i Spinner nie potrzebujemy try-catch do parsowania liczb?

Testy manualne

Waga (SeekBar)Wzrost (Spinner)BMIKategoriaSkala
70 kg175 cm22Waga prawidłowaSuwak w strefie zielonej
50 kg180 cm15NiedowagaSuwak na lewo
90 kg170 cm31OtyłośćSuwak na prawo
85 kg175 cm27NadwagaSuwak w strefie pomarańczowej

🔥 Lista zakupów — Rozgrzewka

„Lista z możliwością dodawania i usuwania. ListView + adapter."

Mini Quiz

  1. Jak utworzyć ArrayAdapter?
  2. Jak dodać element do listy?
  3. Co musi być wywołane po zmianie danych?
Odpowiedzi
  1. new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, lista)
  2. lista.add("element")
  3. adapter.notifyDataSetChanged()

📋 Lista zakupów — Instrukcja

Treść zadania

Zadanie

Wykonaj aplikację "Lista zakupów":

  1. Pole tekstowe na nowy produkt
  2. Przycisk "Dodaj"
  3. ListView z produktami
  4. Kliknięcie elementu = usunięcie (z potwierdzeniem)
  5. Przycisk "Wyczyść listę"
  6. Walidacja: nie można dodać pustego produktu

🏗️ Lista zakupów — Budowa UI

☐ Krok 1: Utwórz projekt i zmień root na LinearLayout

Nowy projekt: Empty Views Activity, nazwa Zakupy, język Java.

Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout

Ustaw: orientation="vertical", padding="16dp"

☐ Krok 2: Dodaj górny rząd (EditText + Button)

Dodaj LinearLayout (horizontal) — layout_width="match_parent", layout_height="wrap_content".

Wewnątrz:

WidżetidAtrybuty
EditTextedtProduktlayout_width="0dp", layout_weight="1", hint="Nazwa produktu"
ButtonbtnDodajlayout_width="wrap_content", text="Dodaj"
☐ Krok 3: Dodaj ListView i przycisk czyszczenia

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ę"

☐ Krok 4: Pola klasy + findViewById
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);
☐ Krok 5: Adapter + podpięcie do ListView
// 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());
☐ Krok 6: Metoda dodajProdukt() + 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();
}
☐ Krok 7: Usuwanie z potwierdzeniem (AlertDialog)

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ć

Sprawdzian z lekcji 05:00
1
Jak utworzyć ArrayAdapter do wyświetlania listy Stringów w ListView?
2
Co trzeba wywołać po dodaniu elementu do ArrayList, żeby ListView się odświeżył?
3
Jak wyświetlić krótki komunikat (np. "Dodano!") użytkownikowi?
4
Jak obsłużyć kliknięcie na element w ListView?
5
Jak usunąć element z ArrayList na pozycji position?

Testy manualne

TestOczekiwany wynik
Dodaj "Mleko"Mleko pojawia się na liście
Dodaj pusty produktToast z błędem
Kliknij produkt na liścieDialog z pytaniem o usunięcie
Potwierdź usunięcieProdukt znika z listy
Kliknij "Wyczyść"Lista pusta

🔥 Notatnik — Rozgrzewka

„Notatnik z możliwością zapisu do pliku. Dodatkowa funkcjonalność!"

Mini Quiz

  1. Jak uzyskać dostęp do wieloliniowego pola tekstowego?
  2. Jak zapisać tekst do pliku w Android?
Odpowiedzi
  1. EditText z android:inputType="textMultiLine" i android:lines="10"
  2. FileOutputStream lub SharedPreferences (prostsze dla tekstu)

📋 Notatnik — Instrukcja

Treść zadania

Zadanie

Wykonaj aplikację "Notatnik":

  1. Pole tekstowe wieloliniowe na notatkę
  2. Przycisk "Zapisz" — zapisuje notatkę
  3. Przycisk "Wczytaj" — wczytuje zapisaną notatkę
  4. Przycisk "Wyczyść" — czyści pole
  5. Notatka zapisywana w SharedPreferences
  6. Komunikat Toast przy zapisie/wczytaniu

🏗️ Notatnik — Budowa UI

☐ Krok 1: Utwórz projekt i zmień root na LinearLayout

Nowy projekt: Empty Views Activity, nazwa Notatnik, język Java.

Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout

Ustaw: orientation="vertical", padding="16dp"

☐ Krok 2: Dodaj tytuł (TextView)

Palette → Text → TextView:

textNotatnik
textSize24sp
textStylebold
layout_marginBottom16dp
☐ Krok 3: Dodaj pole tekstowe (EditText multiline)

Palette → Text → Multiline Text:

idedtNotatka
layout_widthmatch_parent
layout_height0dp
layout_weight1
gravitytop
hintWpisz notatke...
inputTypetextMultiLine
background@android:drawable/edit_text
☐ Krok 4: Dodaj rząd przycisków (Zapisz, Wczytaj, Wyczysc)

Dodaj LinearLayout (horizontal) z layout_marginTop="16dp".

Wewnątrz 3 Button (każdy: layout_width="0dp", layout_weight="1"):

idtext
btnZapiszZapisz
btnWczytajWczytaj
btnWyczyscWyczysc
☐ Krok 5: Pola klasy + stale + findViewById
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();
☐ Krok 6: Metoda zapiszNotatke() (SharedPreferences)
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();
}
☐ Krok 7: Metody wczytajNotatke() i wyczyscNotatke()
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("");
    }
}
SharedPreferences

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ć

Sprawdzian z lekcji 05:00
1
Jak uzyskać dostęp do SharedPreferences w Activity?
2
Jak zapisać wartość String do SharedPreferences?
3
Jak odczytać zapisany String z SharedPreferences?
4
Co oznacza MODE_PRIVATE w SharedPreferences?
5
Jaki inputType ustawić dla wieloliniowego pola tekstowego?

Testy manualne

TestOczekiwany wynik
Wpisz tekst + ZapiszToast "Zapisano!"
Wyczyść + WczytajPoprzedni 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

  1. Jak przechowywać pytania quizu?
  2. Jak grupować RadioButtony?
  3. Jak sprawdzić który RadioButton jest zaznaczony?
Odpowiedzi
  1. Tablica obiektów lub ArrayList klasy Pytanie
  2. <RadioGroup> w XML
  3. radioGroup.getCheckedRadioButtonId() zwraca ID zaznaczonego

📋 Quiz — Instrukcja

Treść zadania

Zadanie

Wykonaj aplikację "Quiz":

  1. Baza 12 pytań jednokrotnego wyboru
  2. Losowanie 3 pytań z bazy przy każdym uruchomieniu
  3. 4 odpowiedzi do każdego pytania (RadioButton)
  4. Przycisk "Następne" — przejście do kolejnego pytania
  5. Na końcu — wyświetlenie wyniku (X/3)
  6. Możliwość rozpoczęcia od nowa (nowe losowanie!)
  7. Numeracja pytań (1/3, 2/3...)

🏗️ Quiz — Budowa UI

☐ Krok 1: Utwórz projekt i zmień root na LinearLayout

Nowy projekt: Empty Views Activity, nazwa Quiz, język Java.

Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout

Ustaw: orientation="vertical", padding="16dp"

☐ Krok 2: Dodaj TextViews (numer pytania + tresc)

TextView (numer):

idtxtNumerPytania
textPytanie 1/3
textSize16sp

TextView (tresc pytania):

idtxtPytanie
textTresc pytania
textSize20sp
textStylebold
layout_marginTop16dp
layout_marginBottom24dp
☐ Krok 3: Dodaj RadioGroup z 4 RadioButton

Palette → Buttons → RadioGroup (id="radioGroup", layout_width="match_parent")

Wewnątrz 4 RadioButton (każdy: layout_width="match_parent"):

idtext
rbOdp1Odpowiedz 1
rbOdp2Odpowiedz 2
rbOdp3Odpowiedz 3
rbOdp4Odpowiedz 4
☐ Krok 4: Dodaj przycisk i TextView wyniku

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"

☐ Krok 5: Klasa Pytanie.java

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;
    }
}
☐ Krok 6: Pola klasy + findViewById + losujPytania()
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());
Wymagany import! Na górze pliku dodaj:
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];
    }
}
Jak to działa? Tablica 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!
☐ Krok 7: Metody wyswietlPytanie() i sprawdzOdpowiedz()
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();
    }
}
☐ Krok 8: Metody pokazWynik() i restartQuiz()
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();
}
Nowe losowanie! Przy restarcie wywołujemy 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ć

Sprawdzian z lekcji 05:00
1
Jak pogrupować RadioButtony, żeby tylko jeden mógł być zaznaczony?
2
Jak sprawdzić, który RadioButton jest zaznaczony w RadioGroup?
3
Jak odznaczyć wszystkie RadioButtony w RadioGroup?
4
Co zwraca getCheckedRadioButtonId() gdy nic nie jest zaznaczone?
5
Jak przechowywać pytania quizu w Javie, gdy każde pytanie ma tekst, 4 odpowiedzi i numer poprawnej?

Testy manualne

TestOczekiwany wynik
Rozpocznij quizPytanie 1/3 wyświetlone
Kliknij Następne bez wyboruToast "Wybierz odpowiedź!"
Odpowiedz poprawniePrzejście do następnego pytania
Ukończ quiz (5 poprawnych)Wynik: 5/5
Zagraj ponownieQuiz 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

Zadanie

Wykonaj aplikację z dwoma ekranami:

  1. Ekran 1 (MainActivity): formularz logowania — pole nazwy użytkownika, pole hasła, przycisk "Zaloguj"
  2. Pole hasła musi maskować tekst (kropki zamiast liter)
  3. Walidacja: puste pola, hasło min. 4 znaki
  4. Poprawne dane: admin / 1234 — przekierowanie na ekran 2
  5. Ekran 2 (WelcomeActivity): napis "Witaj, [nazwa]!" + przycisk "Wyloguj"
  6. Wylogowanie wraca do ekranu logowania
  7. Komunikaty błędów przez Toast

Czego się nauczysz

PojęcieCo robi
IntentPrzejście między dwoma Activity
Druga ActivityTworzenie kolejnego ekranu (New → Activity)
putExtra / getStringExtraPrzekazywanie danych między ekranami
inputType="textPassword"Maskowanie hasła kropkami
ToastKrótki komunikat dla użytkownika
finish()Zamknięcie Activity (powrót)

Ekran logowania — Budowa krok po kroku

☐ Krok 1: Nowy projekt + LinearLayout

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:

orientationvertical
padding32dp
gravitycenter
☐ Krok 2: Tytuł + dwa EditText (login + hasło)

TextView (tytuł):

idtytul
textEkran logowania
textSize28sp
textStylebold
layout_marginBottom40dp

EditText (login): Palette → Text → Plain Text

idpoleUzytkownik
hintNazwa uzytkownika
inputTypetext
layout_marginBottom16dp

EditText (hasło): Palette → Text → Password (NIE Plain Text!)

idpoleHaslo
hintHaslo
inputTypetextPassword
layout_marginBottom24dp
Maskowanie hasła — inputType="textPassword" automatycznie zamienia litery na kropki (●●●●). Dzięki temu nikt nie podejrzy hasła zerkając na ekran.
☐ Krok 3: Przycisk "Zaloguj"
idbtnZaloguj
textZALOGUJ
layout_widthmatch_parent
backgroundTint#2196F3
textColor#FFFFFF

Component Tree powinno wyglądać tak:

LinearLayout (vertical, gravity=center)
  ├── TextView "tytul"
  ├── EditText "poleUzytkownik"
  ├── EditText "poleHaslo"
  └── Button   "btnZaloguj"
☐ Krok 4: Druga Activity (WelcomeActivity)

To kluczowy moment lekcji — pierwszy raz tworzysz drugi ekran!

  1. W panelu Project kliknij prawym na pakiet com.example.logowanie
  2. New → Activity → Empty Views Activity
  3. Activity Name: WelcomeActivity
  4. Layout Name: activity_welcome (wypełni się automatycznie)
  5. Kliknij Finish
Co się stało? Android Studio utworzył:
  • WelcomeActivity.java — logika drugiego ekranu
  • activity_welcome.xml — wygląd drugiego ekranu
  • Wpis w AndroidManifest.xml rejestrujący nową Activity
Sprawdź AndroidManifest.xml — tam są wszystkie Activity w aplikacji.
☐ Krok 5: Layout activity_welcome.xml

Otwórz activity_welcome.xml i zmień root na LinearLayout (vertical, padding 32dp, gravity center) — tak samo jak w activity_main.

TextView (powitanie):

idtekstPowitalny
textWitaj!
textSize32sp
textStylebold
layout_marginBottom40dp

Button (wyloguj):

idbtnWyloguj
textWYLOGUJ
layout_widthmatch_parent
backgroundTint#F44336
textColor#FFFFFF
☐ Krok 6: MainActivity.java — pola + findViewById

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());
Dane logowania na sztywno — w prawdziwej aplikacji byłyby w bazie danych. Dla naszego ćwiczenia wystarczą dwie zmienne.
☐ Krok 7: Metoda zaloguj() — walidacja + Intent

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();
    }
}
Ważne — kluczowe pojęcia:
  • .trim() — usuwa spacje na początku i końcu (np. " admin " → "admin")
  • return — kończy metodę, dalszy kod się nie wykona
  • Intent — "zamiar" przejścia do innego ekranu
  • putExtra("klucz", wartosc) — dokleja dane do Intentu (jak list w kopercie)
  • startActivity(zamiar) — wykonuje przejście
☐ Krok 8: WelcomeActivity.java — odbiór danych + wylogowanie

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());
Analogia:
  • 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)
Klucz musi być IDENTYCZNY! W 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

TestOczekiwany wynik
Puste pola → kliknij ZALOGUJToast "Wpisz nazwe uzytkownika!"
Tylko hasło → kliknij ZALOGUJToast "Wpisz nazwe uzytkownika!"
Tylko login → kliknij ZALOGUJToast "Wpisz haslo!"
admin / 12 → ZALOGUJToast "Haslo musi miec minimum 4 znaki!"
jan / 5678 → ZALOGUJToast "Bledna nazwa uzytkownika lub haslo!"
admin / 1234 → ZALOGUJOtwiera ekran "Witaj, admin!"
Kliknij WYLOGUJPowrót do ekranu logowania
Wpisz hasłoWidać kropki, nie litery

Ekran logowania — Sprawdzian

5 pytań · 5 minut · minimum 60% żeby zdać

Sprawdzian z lekcji 05:00
1
Czym jest Intent w Androidzie?
2
Jak zamaskować tekst w polu hasła (kropki zamiast liter)?
3
Jak przekazać tekst z MainActivity do WelcomeActivity?
4
Co robi metoda finish()?
5
Co stanie się gdy klucz w getStringExtra jest INNY niż w putExtra?

📜 Android — Ściąga

Podstawowe kontrolki

KontrolkaXMLJava
TextViewandroid:text="..."txt.setText(...)
EditTextandroid:hint="..."edt.getText().toString()
Buttonandroid:onClick="metoda"btn.setOnClickListener(...)
ImageViewandroid:src="@drawable/..."img.setImageResource(...)
CheckBoxandroid:checked="true"cb.isChecked()
RadioButtonW RadioGrouprg.getCheckedRadioButtonId()

Layouty

LayoutUżycie
LinearLayoutandroid:orientation="vertical|horizontal"
ConstraintLayoutapp:layout_constraint...="parent|@id/xxx"
RelativeLayoutandroid:layout_below="@id/xxx"

Wymiary

WartośćZnaczenie
match_parentWypełnij rodzica
wrap_contentDopasuj do zawartości
dpDensity-independent pixels (wymiary)
spScale-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

❌ findViewById zwraca null

Przyczyny:

  • Literówka w ID: R.id.txtWynik ≠ android:id="@+id/txtwynik"
  • Wywołanie przed setContentView()
  • ID z innego layoutu

NumberFormatException

❌ Błąd parsowania liczby
// ✅ 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ę

❌ Dodane elementy nie widoczne

Przyczyna: Brak adapter.notifyDataSetChanged()

lista.add("nowy");
adapter.notifyDataSetChanged();  // WYMAGANE!

RadioGroup nie zwraca wyboru

❌ getCheckedRadioButtonId() zwraca -1

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

❌ Obrót telefonu powoduje błąd

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

❌ Resources$NotFoundException

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

OperatorOpisPrzykład
+ - * /Arytmetyczne10 / 3 → 3.333
//Dzielenie całkowite10 // 3 → 3
%Reszta z dzielenia10 % 3 → 1
**Potęgowanie2 ** 3 → 8
== != < >Porównania5 == 5 → True
and or notLogiczneTrue 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")
⚠️ Python wymaga dwukropka po 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

MetodaOpisPrzykł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

MetodaOpis
.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
💡 Na egzaminie INF.04 dane są w pliku tekstowym z separatorem ; 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.

PyQt6, nie PySide6 — pilnuj tego przy kopiowaniu kodu z internetu:

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:

ElementPyQt6 — tak piszemyPySide6 — nie używamy
importfrom PyQt6 import QtWidgetsfrom PySide6 import QtWidgets
konwersja .uipyuic6 mainwindow.ui -o mainwindow.pypyside6-uic
stałe (enumy)pełna ścieżka: Qt.AlignmentFlag.AlignCenterskrót: Qt.AlignCenter
własne sygnałypyqtSignal, pyqtSlotSignal, 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

Schemat pracy:
1. Projektujesz formularz wizualnie w Qt Creator (Designer) — przeciągasz widżety myszką
2. Zapisujesz formularz jako plik .ui
3. Konwertujesz na Python komendą: pyuic6 mainwindow.ui -o mainwindow.py
4. Tworzysz main.py w VS Code — importujesz wygenerowany formularz i dodajesz logikę (funkcje, SQL, sygnały)

Qt Creator — tworzenie formularza

  1. Otwórz Qt Creator → File → New → Qt Designer Form → wybierz Main Window → Create
  2. Z panelu Widget Box (po lewej) przeciągnij potrzebne widżety na formularz (np. QLabel, QPushButton, QListWidget)
  3. W panelu Property Editor (po prawej) ustaw objectName każdego widżetu — to nazwa, przez którą odwołujesz się w kodzie (np. listWidget, comboBox, btnFiltruj, lblWartosc)
  4. Ustaw layout: zaznacz formularz → prawy klik → Lay out in a Grid (lub Vertically / Horizontally)
  5. Zapisz jako mainwindow.ui w 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żetOpisWażne metody
QLabelEtykieta tekstowa.setText(), .text()
QLineEditPole tekstowe (input).text(), .setText(), .clear()
QPushButtonPrzycisk.clicked.connect(fn)
QListWidgetLista elementów.addItem(), .clear(), .currentRow()
QComboBoxLista rozwijana.addItems(), .currentText()
QCheckBoxPole wyboru.isChecked(), .stateChanged.connect(fn)
QStackedWidgetKontener stron (widoków).setCurrentIndex(n), .currentIndex(), .count()

Layouty (w Qt Creator)

Grid LayoutVertical / Horizontal Layout
Siatka (wiersze × kolumny)Stackowanie (pionowo / poziomo)
Precyzyjna kontrola pozycjiProsty układ liniowy
Prawy klik → Lay out in a GridPrawy 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()
⚠️ Używaj ? (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ź.

przypomnienie
Jak wczytać zawartość pliku tekstowego w Pythonie?
Używamy konstrukcji 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).
kod
Co robi metoda 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().
koncepcja
Dlaczego w SQL używamy ? zamiast f-stringa do wstawiania wartości?
Parametryzowane zapytania (?) 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.
kod
Jak utworzyć tabelę w SQLite, która nie będzie zduplikowana przy ponownym uruchomieniu?
Używamy 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.
koncepcja
Jak w PyQt6 dodać element do listy (QListWidget)?
Używamy 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

Zasada #1: Uczeń pisze, Ty obserwujesz. Podpowiadaj pytaniami.
Zasada #2: Po każdym kroku — uruchom (F5) i sprawdź wynik.
Zasada #3: Błędy czytajcie razem. Python ma czytelne komunikaty.

5. Częste blokady

ProblemPodpowiedź
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

WymaganieDetaleStatus
Wczytanie dane.txtwith open(), split(";"), parsowanie typów✅
Baza magazyn.dbsqlite3.connect()✅
Tabela produktyCREATE TABLE IF NOT EXISTS✅
INSERT danychParametryzowane (?) z pętli✅
SELECT wszystkieORDER BY cena DESC✅
SELECT po kategoriiWHERE kategoria = ?✅
AgregacjaSUM(cena * ilosc) GROUP BY✅
PyQt6 oknoQMainWindow + setWindowTitle(„Rejestr produktów")✅
QListWidgetWyświetla produkty✅
QComboBox kategoriiQComboBox + addItems() z listą kategorii✅
Przycisk FiltrujQPushButton + clicked.connect() → odśwież listę✅
QLabel wartośćŁączna wartość = cena × ilość✅
Znaczące nazwyprodukty, kategoria, wartosc, filtruj✅

#01 — Budowa krok po kroku

0/8 kroków
0
Szkielet main.py
▼

„Startujemy. Najpierw zaprojektujemy formularz w Qt Creator, a potem napiszemy logikę w VS Code."

Formularz w Qt Creator:
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.

1
Wczytanie dane.txt
▼

„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"

2
SQLite — baza + tabela
▼

„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.

3
INSERT — wstawianie danych
▼
# 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."

4
Zapytania SQL
▼

„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.

5
PyQt6 — widżety
▼

„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.

6
Ładowanie danych do QListWidget
▼

„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.

7
Filtrowanie — QComboBox + QPushButton
▼

„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.

Gotowe! QListWidget wyświetla produkty, QComboBox filtruje, QLabel pokazuje wartość.

Bonus: Dodaj dynamiczne kategorie — zamiast hardcodowanych, pobierz z bazy: SELECT DISTINCT kategoria FROM produkty

#01 — Gotowy kod

Formularz Qt Creator (mainwindow.ui)

WidżetobjectNameWłaściwości
QLabellblTytultext: „Rejestr produktów", font: bold 18pt
QLabellblKategoriatext: „Kategoria:"
QComboBoxcomboBox(elementy dodawane z kodu)
QPushButtonbtnFiltrujtext: „Filtruj"
QListWidgetlistWidgetfont: Consolas 10pt
QLabellblWartosctext: „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ć

Sprawdzian z lekcji 05:00
1
Która konstrukcja automatycznie zamyka plik po zakończeniu bloku?
2
Co zwraca "10;Laptop;3499.99".split(";")?
3
Dlaczego używamy ? w zapytaniach SQL zamiast f-stringów?
4
Jak w PyQt6 podłączyć kliknięcie przycisku do funkcji filtruj?
5
Jak w PyQt6 wyczyścić i dodać element do QListWidget?

🔥 Rozgrzewka — Dziennik ucznia

Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.

przypomnienie
Jak obliczyć średnią z listy liczb w Pythonie?
Średnia = suma / ilość. Używamy 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.
kod
Jak w SQL policzyć średnią ocen dla każdego ucznia?
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.
koncepcja
Czym różni się 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:.
kod
Jak formatować liczbę do 2 miejsc po przecinku?
Używamy f-stringa z formatowaniem: f"{srednia:.2f}". Lub funkcji round(srednia, 2). Przykład: srednia = 4.666667 → f"{srednia:.2f}" → "4.67".
koncepcja
Jak w PyQt6 dodać element do QListWidget?
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

Zasada: Uczeń powinien odczuć, że to ten sam schemat co #01. Buduj pewność siebie — „widzisz, znasz to".

5. Częste blokady

ProblemPodpowiedź
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

WymaganieDetaleStatus
Wczytanie dane_oceny.txtwith open(), split(";"), int(ocena)✅
Baza dziennik.dbsqlite3.connect()✅
Tabela ocenyCREATE TABLE IF NOT EXISTS✅
INSERT danychParametryzowane z pętli✅
SELECT wszystkieORDER BY uczen, przedmiot✅
AVG per przedmiotSELECT przedmiot, AVG(ocena) GROUP BY✅
Najlepszy uczeńAVG + ORDER BY DESC LIMIT 1✅
Filtr po przedmiocieWHERE przedmiot = ?✅
PyQt6 oknoQMainWindow + setWindowTitle(„Dziennik ucznia")✅
QListWidgetWyświetla oceny✅
QComboBox przedmiotówQComboBox + addItems() z DISTINCT✅
QLabel średniaŚrednia wyświetlonych ocen✅
QLabel najlepszyUczeń z najwyższą średnią✅

#02 — Budowa krok po kroku

0/7 kroków
0
Szkielet + importy
▼

„Identyczny start jak w #01. Formularz w Qt Creator, potem pyuic6."

Formularz w Qt Creator:
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.ui
pyuic6 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."

1
Wczytanie dane_oceny.txt
▼

„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.

2
SQLite — baza + tabela + INSERT
▼

„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.

3
Zapytania SQL — średnia + najlepszy
▼
# Ś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ą.

4
PyQt6 — widżety
▼

„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.

5
Funkcja ładowania danych
▼
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.

6
Filtrowanie + finalizacja
▼
def filtruj(self):
    wybrana = self.ui.comboBox.currentText()
    self.zaladuj(wybrana)

self.ui.btnFiltruj.clicked.connect(self.filtruj)
Gotowe! Dziennik filtruje oceny po przedmiocie, liczy średnią, pokazuje najlepszego ucznia.

Bonus: Dodaj przycisk „Statystyki" wyświetlający średnią per przedmiot w QMessageBox.

#02 — Gotowy kod

Formularz Qt Creator (mainwindow.ui)

WidżetobjectNameWłaściwości
QLabellblTytultext: „Dziennik ucznia", font: bold 18pt
QLabellblPrzedmiottext: „Przedmiot:"
QComboBoxcomboBox(elementy z kodu)
QPushButtonbtnFiltrujtext: „Filtruj"
QListWidgetlistWidgetfont: Consolas 10pt
QLabellblSredniatext: „Średnia: —", font: 12pt
QLabellblNajlepszytext: „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ć

Sprawdzian z lekcji 05:00
1
Jak obliczyć średnią z listy oceny = [4, 5, 3]?
2
Które zapytanie SQL policzy średnią ocen dla każdego przedmiotu?
3
Co zwraca cursor.fetchone() gdy nie ma więcej wierszy?
4
Jak sformatować liczbę 3.14159 do 2 miejsc po przecinku?
5
Jak w PyQt6 wyczyścić i dodać element do QListWidget?

🔥 Rozgrzewka — Wypożyczalnia książek

Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.

przypomnienie
Jak wykonać UPDATE w SQLite — zmienić wartość w istniejącym wierszu?
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().
kod
Jak pobrać zaznaczony element z QListWidget w PyQt6?
item = list_widget.currentItem() zwraca zaznaczony element (lub None). Tekst: item.text(). Indeks: list_widget.currentRow() (zwraca -1 jeśli nic nie zaznaczono).
koncepcja
Jak przechować status „wypożyczona/dostępna" w bazie danych?
Kolumna typu INTEGER z wartościami 1 (dostępna) lub 0 (wypożyczona). W SQL: dostepna INTEGER DEFAULT 1. Przy wypożyczeniu: UPDATE ksiazki SET dostepna = 0 WHERE id = ?.
kod
Jak wyświetlić tylko dostępne książki (niewypożyczone)?
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.
koncepcja
Po co używamy try/except przy operacjach na bazie?
Obsługa błędów — np. gdy plik nie istnieje, baza jest zablokowana, lub dane są nieprawidłowe. 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

ProblemPodpowiedź
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

WymaganieDetaleStatus
Wczytanie dane_ksiazki.txtwith open(), split(";"), int(rok), int(dostepna)✅
Baza biblioteka.dbsqlite3.connect()✅
Tabela ksiazkiCREATE TABLE IF NOT EXISTS, 6 kolumn✅
INSERT danychParametryzowane, z pętli✅
SELECT + sortowanieORDER BY tytul✅
Filtr po gatunkuWHERE gatunek = ?✅
Filtr „tylko dostępne"WHERE dostepna = 1✅
COUNT per gatunekSELECT gatunek, COUNT(*) GROUP BY✅
UPDATE statusuUPDATE ksiazki SET dostepna = ? WHERE id = ?✅
PyQt6 oknoQMainWindow + setWindowTitle(„Wypożyczalnia książek")✅
QListWidgetTytuł, autor, rok, status✅
QComboBox gatunekDISTINCT + „wszystkie"✅
QCheckBox dostępneQCheckBox + isChecked()✅
Przycisk Wypożycz/ZwróćUPDATE + odśwież✅
QLabel statystykiDostępne X / Y✅

#03 — Budowa krok po kroku

0/7 kroków
0
Szkielet + importy
▼
Formularz w Qt Creator:
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.ui
pyuic6 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."

1
Wczytanie dane_ksiazki.txt
▼
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.

2
SQLite — baza + tabela + INSERT
▼
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).

3
PyQt6 — widżety + QCheckBox
▼

„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().

4
Funkcja ładowania danych
▼
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ął."

5
Wypożycz / Zwróć — UPDATE
▼

„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.

6
Filtrowanie + finalizacja
▼
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)
Gotowe! Wypożyczalnia: filtr po gatunku, checkbox „tylko dostępne", przycisk Wypożycz/Zwróć z UPDATE.

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żetobjectNameWłaściwości
QLabellblTytultext: „Wypożyczalnia książek", font: bold 18pt
QLabellblGatunektext: „Gatunek:"
QComboBoxcomboBox(elementy z kodu)
QPushButtonbtnFiltrujtext: „Filtruj"
QCheckBoxchkDostepnetext: „Tylko dostępne"
QListWidgetlistWidgetfont: Consolas 10pt
QLabellblStattext: „Dostępne: 0/0", font: 12pt
QPushButtonbtnWypozycztext: „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ć

Sprawdzian z lekcji 05:00
1
Które zapytanie zmieni status książki o id=5 na wypożyczoną?
2
Jak pobrać indeks zaznaczonego elementu w QListWidget (PyQt6)?
3
Jak wyświetlić tylko dostępne książki (dostepna = 1)?
4
Co się stanie jeśli wykonamy UPDATE bez WHERE?
5
Do czego służy try/except przy operacjach bazodanowych?

🔥 Rozgrzewka — Stacja pogodowa

Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.

przypomnienie
Jak w SQL policzyć średnią, minimum i maksimum temperatury?
SELECT AVG(temperatura), MIN(temperatura), MAX(temperatura) FROM pomiary. Funkcje agregujące: AVG() — średnia, MIN() — najmniejsza, MAX() — największa, SUM() — suma, COUNT() — liczba wierszy.
kod
Jak w SQL pogrupować dane po mieście i policzyć średnią dla każdego?
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.
koncepcja
Jak przechowywać datę w SQLite i jak ją porównywać?
SQLite nie ma natywnego typu DATE — używamy TEXT w formacie YYYY-MM-DD. Porównania tekstowe działają poprawnie: WHERE data > '2025-01-10'. W Pythonie: datetime.strptime(tekst, "%Y-%m-%d").
kod
Jak wybrać pomiary z zakresu dat?
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.
koncepcja
Co robi klauzula 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

ProblemPodpowiedź
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

WymaganieDetaleStatus
Wczytanie dane_pogoda.txtwith open(), split(";"), float(temp), int(wilg)✅
Baza pogoda.dbsqlite3.connect()✅
Tabela pomiaryCREATE TABLE IF NOT EXISTS, 6 kolumn✅
INSERT danychParametryzowane, z pętli✅
SELECT + sortowanieORDER BY data, miasto✅
Filtr po mieścieWHERE miasto = ?✅
MIN/MAX/AVG tempAgregacja per wyświetlone dane✅
Najzimniejszy dzieńORDER BY temperatura ASC LIMIT 1✅
Średnia wilgotnośćAVG(wilgotnosc) GROUP BY miasto✅
PyQt6 oknoQMainWindow + setWindowTitle(„Stacja pogodowa")✅
QListWidget pomiarówData, miasto, temp, wilg, ciśn✅
QComboBox miastDISTINCT + „wszystkie"✅
QLabel min/max/avgTemperatury✅
QLabel najzimniejszyData + miasto + temperatura✅
Przycisk StatystykiQMessageBox ze średnimi per miasto✅

#04 — Budowa krok po kroku

0/8 kroków
0
Szkielet + importy
▼
Formularz w Qt Creator:
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.ui
pyuic6 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."

1
Wczytanie dane_pogoda.txt
▼
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.

2
SQLite — baza + tabela + INSERT
▼
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.

3
Zapytania SQL — testy
▼
# 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).

4
PyQt6 — widżety
▼

„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.

5
Funkcja ładowania danych
▼
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."

6
Filtrowanie + przycisk
▼
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ą?"

7
Przycisk Statystyki — messagebox
▼

„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)
Gotowe! Stacja pogodowa: filtr po mieście, min/max/avg, najzimniejszy dzień, statystyki w QMessageBox.

Bonus: Dodaj kolorowanie w QListWidget: temperatury ujemne → niebieskie tło, powyżej 0 → zielone.

#04 — Gotowy kod

Formularz Qt Creator (mainwindow.ui)

WidżetobjectNameWłaściwości
QLabellblTytultext: „🌤️ Stacja pogodowa", font: bold 18pt
QLabellblMiastotext: „Miasto:"
QComboBoxcomboBox(elementy z kodu)
QPushButtonbtnFiltrujtext: „Filtruj"
QPushButtonbtnStatystykitext: „📊 Statystyki"
QListWidgetlistWidgetfont: Consolas 10pt
QLabellblTemptext: „Min: — | Max: — | Avg: —", font: 11pt
QLabellblZimnotext: „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ć

Sprawdzian z lekcji 05:00
1
Które zapytanie policzy średnią temperaturę dla każdego miasta?
2
Jak wybrać pomiary tylko z zakresu dat 10-15 stycznia 2025?
3
Czym różni się WHERE od HAVING?
4
Jak znaleźć maksymalną temperaturę w każdym mieście?
5
Jak wyświetlić tylko miasta ze średnią temperaturą powyżej 0°C?

🔥 Rozgrzewka — Logowanie z nawigacją menu

Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.

przypomnienie
Co to jest 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.
kod
Jak odczytać tekst wpisany w pole QLineEdit?
Używamy metody .text(): wartosc = entry.text(). Zwraca string. Aby wyczyścić pole: entry.clear(). Aby wstawić tekst: entry.setText("tekst").
koncepcja
Jak wczytać plik tekstowy linia po linii w Pythonie?
Używamy 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.
kod
Jak porównać dwa stringi w Pythonie (np. login)?
Używamy operatora ==: if wpisane_imie == "Jan":. Uwaga: porównanie jest case-sensitive — "jan" != "Jan". Aby ignorować wielkość liter: wpisane_imie.strip().lower() == "jan".
koncepcja
Jak przełączyć widok w QStackedWidget przez rozwijane menu?
Tworzymy 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 + QMenu do nawigacji między widokami
  • Odczyt danych z pliku dane.txt za 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."

Schemat architektury:
mainwindow.ui → pyuic6 mainwindow.ui -o mainwindow.py → from mainwindow import Ui_MainWindow
zaloguj.ui → pyuic6 zaloguj.ui -o zaloguj.py → from zaloguj import Ui_Zaloguj
formularz.ui → pyuic6 formularz.ui -o formularz.py → from formularz import Ui_Formularz
wyswietl.ui → pyuic6 wyswietl.ui -o wyswietl.py → from wyswietl import Ui_Wyswietl
dane.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

ProblemPodpowiedź
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

WymaganieDetaleStatus
Plik dane.txt3 linie: imię, nazwisko, klasa✅
Wczytanie plikuwith open(), .strip() na liniach✅
Okno PyQt6QMainWindow (class MainWindow), tytuł, rozmiar✅
QPushButton + QMenu (navbar)Rozwijane menu z 3 opcjami nawigacji✅
QStackedWidgetTworzony w kodzie, 3 widoki z osobnych .ui✅
Widok 1 — napisLabel z tekstem powitalnym✅
Widok 2 — QLineEdit × 2Pola: Imię, Nazwisko✅
Widok 2 — QPushButtonPrzycisk „Zaloguj" z clicked.connect()✅
Logika logowaniaPorównanie .text() z danymi z pliku✅
Błędne logowanieQMessageBox.critical()✅
Widok 3 — profilQLabel z imieniem, nazwiskiem, klasą✅
Profil zablokowany w menuakcja_profil.setEnabled(False) do momentu logowania✅
Znaczące nazwyentryImie, entryNazwisko, zaloguj, itp.✅

#05 — Budowa krok po kroku

0/6 kroków
0
Formularz w Qt Creator + pyuic6
▼

„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."

Formularze w Qt Creator (4 pliki .ui):

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.ui

2. zaloguj.ui — File → New → Qt Designer Form → Widget → Create
Przeciągnij: QLabel (objectName: lblPowitanie)
Lay out Vertically. Zapisz jako zaloguj.ui

3. 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.ui

4. 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."

1
Wczytanie danych z pliku
▼

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

2
Navbar + QStackedWidget — z Qt Creator
▼

„Przycisk menu i QStackedWidget zostały już zaprojektowane w Qt Creator. Zobaczmy jakie widżety mamy dostępne przez obiekt ui."

Pliki .ui i ich widżety:
Plik .uiobjectNameTypOpis
mainwindow.uibtnMenuQPushButtonPrzycisk „☰ Menu" z QMenu
zaloguj.uilblPowitanieQLabelTekst powitalny
formularz.uilblImieQLabelEtykieta „Imię:"
formularz.uientryImieQLineEditPole na imię
formularz.uilblNazwiskoQLabelEtykieta „Nazwisko:"
formularz.uientryNazwiskoQLineEditPole na nazwisko
formularz.uibtnZalogujQPushButtonPrzycisk „Zaloguj"
wyswietl.uilblProfilQLabelDane 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)."

Jak to działa:
• 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)
3
Navbar — QMenu + tekst powitalny
▼

„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.

4
Funkcja logowania
▼

„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.

Ważne! Metodę zaloguj(self) umieść przed linią self.ui_formularz.btnZaloguj.clicked.connect(self.zaloguj) — Python musi znać metodę zanim ją podłączysz do sygnału.
5
Profil + blokada menu
▼

„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?"

Gotowe! Widok Profil się pojawił, opcja w menu się odblokowała. Wpisz złe dane — pojawi się QMessageBox z błędem.

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 .uiobjectNameTypOpis
mainwindow.uibtnMenuQPushButtonPrzycisk „☰ Menu" z rozwijanym QMenu
zaloguj.uilblPowitanieQLabelTekst powitalny
formularz.uilblImieQLabelEtykieta „Imię:"
formularz.uientryImieQLineEditPole na imię
formularz.uilblNazwiskoQLabelEtykieta „Nazwisko:"
formularz.uientryNazwiskoQLineEditPole na nazwisko
formularz.uibtnZalogujQPushButtonPrzycisk „Zaloguj"
wyswietl.uilblProfilQLabelDane 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
Jak uruchomić:
1. Zaprojektuj 4 formularze w Qt Creator → zapisz jako mainwindow.ui, zaloguj.ui, formularz.ui, wyswietl.ui
2. 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 folderze
4. Uruchom: python main.py
5. 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ć

Sprawdzian z lekcji 05:00
1
Który widżet PyQt6 wyświetla jedną stronę naraz i pozwala przełączać widoki?
2
Jak podpiąć rozwijane menu (QMenu) do przycisku QPushButton?
3
Co zwraca metoda entry.text()?
4
Dlaczego po readlines() należy użyć .strip() na każdej linii?
5
Jak przełączyć stronę w QStackedWidget na stronę o indeksie 2?

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

Zadanie

Napisz program konsolowy który:

  1. Wyświetla menu z 4 walutami: USD, EUR, GBP, CHF (kursy w PLN)
  2. Pyta użytkownika ile PLN chce przeliczyć
  3. Pyta na jaką walutę przeliczyć
  4. Wyświetla wynik z dokładnością do 2 miejsc po przecinku
  5. Po przeliczeniu pyta czy kontynuować — pętla działa do wyjścia
  6. Łapie błędy gdy użytkownik wpisze tekst zamiast liczby (try/except)

Czego się nauczysz

PojęcieCo robi
dict (słownik)Tabela kursów walut: nazwa → kurs
input()Pobranie danych od użytkownika
float()Konwersja stringa na liczbę zmiennoprzecinkową
while TruePętla menu — działa do break
try / exceptObsługa błędów (np. tekst zamiast liczby)
f-string :.2fFormatowanie liczby do 2 miejsc po przecinku
defWydzielenie 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

☐ Krok 1: Słownik kursów + funkcja wyświetlająca menu

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")
kursy.items() — daje pary (klucz, wartość). W pętli rozpakowujemy je do dwóch zmiennych: waluta i kurs.
☐ Krok 2: Funkcja przeliczająca walutę
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.

Dlaczego dzielimy? Kurs USD = 4.05 PLN znaczy: 1 USD kosztuje 4.05 PLN. Więc 100 PLN to 100 / 4.05 ≈ 24.69 USD.
☐ Krok 3: Główna pętla programu (while True)
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
Kluczowe rzeczy:
  • .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ż do break
☐ Krok 4: Walidacja waluty (sprawdzenie czy istnieje)

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
continue — przerywa bieżącą iterację pętli i przechodzi do następnej. Idealnie gdy chcemy "spróbować ponownie".
☐ Krok 5: try/except — co jeśli ktoś wpisze tekst?

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
Jak to działa?
  • 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
☐ Krok 6: Uruchom i przetestuj wszystkie scenariusze

W terminalu uruchom: python konwerter.py

Sprawdź każdy scenariusz:

TestOczekiwany wynik
Wpisz: 100, EUR23.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: tWraca 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.txt zamiast 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ć

Sprawdzian z lekcji 05:00
1
Co zwraca funkcja input()?
2
Jak sformatować liczbę 4.0567 do "4.06"?
3
Co robi continue w pętli?
4
Co stanie się gdy użytkownik wpisze "abc" a kod robi float("abc") bez try/except?
5
Jak sprawdzić czy klucz "EUR" istnieje w słowniku 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

Arkusz CKE · INF.04-01-26.06-SG · egzamin zawodowy, czerwiec 2026Część I arkusza (aplikacja konsolowa) i część III w zakresie, który jej dotyczy. Treść wymagań przepisana z arkusza bez zmian.

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

Część I. Aplikacja konsolowa

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 ą ę ł ń ó ś ż ź Ą Ę Ł Ń Ó Ś Ż Ź (plik znaki.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 projektu konsola.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 arkuszaGdzie w programieJak łatwo to stracić
Funkcja zwracająca hasłodef generuj_haslo() zakończona return haslofunkcja robi print zamiast return
Liczba znaków z każdego zbiorulosuj_znaki(ZBIOR, ile) wywołana pięć razylosowanie z jednego wspólnego zbioru, wtedy proporcje wychodzą przypadkowe
Każdy znak w losowym miejscupę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 napisowejhaslo = "" i doklejanie znakówfunkcja zwraca listę znaków
Bez gotowego generatoratylko random.randint do losowania liczbsecrets.token_urlsafe, random.choices, random.sample
5 haseł ze znaczącym komunikatempętla for numer in range(1, 6) na dole plikusame hasła bez opisu, albo jedno hasło
Nagłówek dokumentacji funkcjikomentarz według listingu 1 pod def generuj_haslo()brak pola autor albo wpisane imię zamiast numeru zdającego
Zrzuty, plik egzamin, konsola.zipfolder z numerem zdającego (krok 8)zrzut samego okna bez paska zadań, zip bez całego projektu

Czego się nauczysz

PojęcieDo 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 returnlosuj_znaki(zbior, ile) zamiast pięciu kopii tej samej pętli
stała (nazwa wielkimi literami)zbiory znaków, których program nie zmienia
docstringnagłó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.

1
Folder egzaminacyjny, archiwum i projekt
▼

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/
  1. Utwórz folder z numerem zdającego i w nim trzy podfoldery. Nazwy dokładnie jak w arkuszu, małymi literami.
  2. Rozpakuj Web1.7z hasłem *LoGiny&H@sla8. Na Windows: prawy przycisk → 7-Zip → Wypakuj tutaj. Na macOS: Keka albo The Unarchiver, bo oba obsługują hasło.
  3. Utwórz projekt konsola: w PyCharm New Project, w VS Code nowy folder. W środku plik main.py.
Hasło do archiwum przepisuj znak po znaku Zaczyna się od *, 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”.
2
Zbiory znaków jako stałe
▼

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 = "!@#$%^&*()_+"
Dlaczego wielkie litery w nazwach W Pythonie nazwa pisana wielkimi literami oznacza stałą, czyli wartość, której program nie zmienia. Egzaminator ocenia „czytelny kod i znaczące nazewnictwo”, a ten zapis od razu pokazuje, że to dane z arkusza, a nie zmienne robocze.
Dlaczego kopiować z pliku Na stanowisku egzaminacyjnym może nie być polskiego układu klawiatury, a zamiast ż ł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.
3
Funkcja losuj_znaki(zbior, ile)
▼

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. śŁ
Pułapka: 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.
A 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ą.
4
generuj_haslo() — część 1: wylosuj 12 znaków
▼

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.

Nawiasy wokół sumy Pozwalają rozbić długie wyrażenie na kilka linii bez znaku \. Każda linia to jeden wiersz z listy wymagań, więc egzaminator odhacza je po kolei.
5
generuj_haslo() — część 2: każdy znak w losowe miejsce
▼

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ótznaklen(haslo)wylosowana pozycjahaslo po obrocie
1q00 (jedyna możliwa)q
2b11 (na koniec)qb
3m21 (w środek)qmb
Pułapka: 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.
Dlaczego nie 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.
6
Program główny — 5 haseł z komunikatem
▼

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.
7
Sprawdzarka — 15 wymagań w 3 sekundy
▼
Materiał własny MoreTutorNa egzaminie sprawdzarki nie ma. Służy do ćwiczeń i nie trafia do konsola.zip.

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.

  1. Pobierz sprawdz_hasla.py do folderu, w którym leży main.py.
  2. 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ć.

8
Dokumentacja i oddanie (część III)
▼

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.

SystemSkrótGdzie ląduje plik
Windows (egzamin)Win + PrtScObrazy → Zrzuty ekranu, potem zmień nazwę i przenieś
macOS (ćwiczenia w domu)Cmd + Shift + 3Biurko. 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)
Sprawdzarka nie idzie do zipa Jeśli 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_znaki losuje ile znaków z jednego zbioru i oddaje je jako napis.
  • generuj_haslo najpierw 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ć

Sprawdzian z lekcji 05:00
1
Zbiór CYFRY = "0123456789". Która linia poprawnie losuje indeks cyfry?
2
Co da "abc"[:1] + "X" + "abc"[1:]?
3
Hasło ma teraz 5 znaków. Z jakiego zakresu losujemy pozycję dla kolejnego znaku?
4
Funkcja generuj_haslo() wypisuje hasło przez print i nic nie zwraca. Na ekranie wszystko się zgadza. Co z punktacją?
5
Co wpisujesz w polu 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 -1

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

Arkusz CKE · INF.04-01-25.01-SG · egzamin zawodowy, styczeń 2025Część I arkusza (aplikacja konsolowa) i część III w zakresie, który jej dotyczy. Treść wymagań przepisana z arkusza bez zmian.

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

Część I. Aplikacja konsolowa

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ą
  • 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.zip z 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 arkuszaGdzie w programieJak łatwo to stracić
Pola niedostępne dla klas potomnychself.__tablica, self.__liczba_elementowjedno 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ówkaż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 -1return i w pętli, return -1 po pętlielse: return -1 w pętli
Nieparzyste: wypisz i zwróć liczbęprint w pętli, return ile na końcutylko jedno z dwóch
Średnia zwracanareturn suma / self.__liczba_elementow// albo print zamiast return
Program główny: rozmiar > 20, brak komunikatu przy -1Tablica(50) i if indeks != -1:Tablica(20) (20 nie jest większe od 20) albo wypisane „-1”
Nagłówek dokumentacji metodydocstring według listingu 1 pod def szukajbrak pola parametry albo imię zamiast numeru zdającego

Czego się nauczysz

PojęcieDo czego dziś służy
klasa, konstruktor __init__tablica i operacje w jednym obiekcie
pole prywatne self.__nazwawymaganie „niedostępne dla klas potomnych”
metoda z return kontra metoda z printpodział pracy między klasą a programem głównym
wyszukiwanie liniowepierwsze 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.

1
Folder egzaminacyjny i projekt
▼
00000000000/          <- numer zdającego (PESEL), na ćwiczeniach dowolny
├── konsolowa/
├── webowa/
└── dokumentacja/
  1. Folder z numerem zdającego i trzy podfoldery, nazwy dokładnie jak w arkuszu.
  2. Projekt konsola (PyCharm: New Project, VS Code: nowy folder), w nim main.py.
  3. Archiwum pliki3.zip na razie zostaje zamknięte. Część I nie potrzebuje żadnych plików.
2
Klasa, pola prywatne i konstruktor
▼
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 polaPoza klasąW klasie potomnejSpełnia arkusz?
self.tablicadostępnedostępnenie
self._tablicadostępne (to tylko umowa)dostępnenie
self.__tablicaAttributeErrorAttributeErrortak
Odpowiednik w innych językach W C++, C# i Javie to słowo 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.
Pułapka: 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.
3
Metoda wyświetlająca: „indeks: wartość”
▼
    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()
Dlaczego 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.
4
Metoda wyszukująca: indeks albo -1
▼
    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.

Pułapka: 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.
Pułapka: zapamiętywanie indeksu w zmiennej 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.
5
Nieparzyste i średnia
▼
    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
MetodaWypisujeZwraca
wyswietlwszystkie elementynic
szukajnicindeks albo -1
nieparzysteliczby nieparzysteich liczbę
srednianicśrednią

Tę tabelę uczeń powinien umieć wyrecytować. Każdy wiersz to inne zdanie z arkusza.

Pułapka: // 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.
A 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.
6
Program główny
▼

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
Stała wartość zamiast 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.
7
Sprawdzarka — 29 wymagań
▼
Materiał własny MoreTutorNa egzaminie sprawdzarki nie ma. Służy do ćwiczeń i nie trafia do konsola.zip.

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ć.

  1. Pobierz sprawdz_tablice.py do folderu z main.py.
  2. 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.

8
Dokumentacja i oddanie (część III)
▼

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ć

Sprawdzian z lekcji 05:00
1
Które pole w Pythonie jest niedostępne dla klasy potomnej?
2
Która linia losuje liczbę z zakresu 1–1000 włącznie?
3
Metoda szukaj ma w pętli if ...: return i oraz else: return -1. Co się stanie?
4
Arkusz: „wszystkie operacje są ograniczone wartością pola liczby elementów”. Która pętla to spełnia?
5
Szukanej wartości nie ma w tablicy. Co wyświetla program główny?

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

Arkusz CKE · INF.04 · egzamin zawodowy, czerwiec 2025Część I arkusza (aplikacja konsolowa) i część III w zakresie, który jej dotyczy. Wymagania przepisane z arkusza, kryteria z oficjalnych zasad oceniania CKE.

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

Część I. Aplikacja konsolowa

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 ocenianiaGdzie w programieJak ł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 globalnychlosuj_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 rozmiarzelista list zestawystała tablica na 100 losowań
2.3 Generator pseudolosowy, zakres 1–49random.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 losowaniawyswietl_zestawywypisanie całej listy jednym print(zestawy)
2.6 Losowanie zestawu bez powtórzeńwhile + if liczba not in zestawfor sześć razy bez sprawdzania
2.7 Zliczone i wyświetlone wystąpienia 1–49zlicz_wystapienia, wyswietl_wystapienialicznik od 0 do 48 i przesunięte wyniki
4.1–4.5 Nagłówek dokumentacjidocstring pod def losuj_zestawybrak 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.

1
Folder egzaminacyjny i projekt
▼
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.

2
Szkielet: main() i wczytanie liczby losowań
▼
import random


def main():
    ile_zestawow = int(input("Ile wygenerować losowań? "))
    print(ile_zestawow)


main()
Dlaczego od razu 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ć.
3
Jeden zestaw bez powtórzeń
▼

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.

Pułapka: 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.
A 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.
4
Funkcja wypełniająca: losuj_zestawy(ile_zestawow)
▼

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.

Pułapka: 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.
5
Funkcja wyświetlająca w formacie z obrazu 1
▼
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.

6
Zliczanie wystąpień 1–49 i program główny
▼
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)
Dlaczego [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
...
7
Sprawdzarka — 16 wymagań
▼
Materiał własny MoreTutorNa egzaminie sprawdzarki nie ma. Nie trafia do konsola.zip.

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ą.

8
Dokumentacja i oddanie (część III)
▼

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ć

Sprawdzian z lekcji 05:00
1
Która pętla gwarantuje 6 różnych liczb w zestawie?
2
Licznik wystąpień to [0] * 49. Co się stanie?
3
Jak funkcja losująca przekazuje dane do programu głównego bez zmiennych globalnych?
4
zestawy = [[5, 9, 1, 30, 22, 41], [7, 3, 48, 11, 2, 19]]. Ile wynosi zestawy[1][2]?
5
Która linia wyświetla losowania numerowane od 1, jak na obrazie 1?

📝 Ś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"}  # dict

f-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 + b

Lista — 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

❌ IndentationError — złe wcięcia
# ❌ Mieszanie tab i spacji
if True:
	print("tab")     # ← TAB
    print("space")   # ← 4 spacje → BŁĄD

# ✅ Zawsze 4 spacje
if True:
    print("ok")
❌ NameError — niezdefiniowana zmienna
# ❌
print(imie)        # imie nie istnieje

# ✅
imie = "Maks"
print(imie)
❌ TypeError — zły typ
# ❌ input() zwraca string!
wiek = input("Wiek: ")
print(wiek + 1)        # "18" + 1 → TypeError

# ✅
wiek = int(input("Wiek: "))
print(wiek + 1)        # 19
❌ FileNotFoundError
# ❌ 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!
❌ IndexError — poza zakresem
# ❌
l = [1, 2, 3]
print(l[3])    # IndexError! (indeksy 0, 1, 2)

# ✅
print(l[2])    # 3
print(l[-1])   # 3 (ostatni)
❌ SQL — tuple z jednym elementem
# ❌ Brak przecinka — to nie tuple!
cursor.execute("SELECT * WHERE k=?", ("elektronika"))  # Error

# ✅ Przecinek tworzy tuple
cursor.execute("SELECT * WHERE k=?", ("elektronika",))
❌ Zapomniany conn.commit()
# ❌ 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!
❌ Brak mainwindow.py (pyuic6)
# ❌ 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() w złym miejscu
# ❌ 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() vs sorted()
# ❌ .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)

ParametrWartość
Czas210 minut (3,5 godziny)
Punkty50
Liczba zadań~8 zadań głównych, podzielonych na podpunkty
FormaJeden arkusz: część „bez komputera" (papier) + część „przy komputerze"
Języki programowaniaC++, Java, Python (wybierasz jeden)
Oprogramowanie na saliŚrodowisko do programowania, arkusz kalkulacyjny (Excel/Calc), baza danych (SQL)
InternetBrak — komputer odcięty od sieci
Co możesz mieć przy sobie: dozwolona jest dokumentacja oprogramowania udostępniona na komputerze (np. help środowiska). Nie masz internetu ani własnych notatek. Dlatego szablony algorytmów muszą być w głowie.

Pięć działów — i ile są warte

Rozkład punktów jest dość stały rok do roku. Z arkusza maj 2026:

DziałWagaCharakter
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ę
Wniosek strategiczny: ponad połowa punktów to algorytmika + programowanie. Ale SQL i Excel (razem 36%) są najszybsze do opanowania, bo opierają się na powtarzalnych schematach. Teoria to „darmowe" punkty — mało nauki, pewny zwrot.

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)

  1. Najpierw przejrzyj cały arkusz (5 min) — zaznacz zadania które od razu umiesz.
  2. Zbierz pewne punkty — teoria (systemy, sieci) i proste podpunkty SQL/Excel. Szybkie, gwarantowane.
  3. Potem zadania programistyczne — najwięcej punktów, ale czasochłonne. Zrób podpunkty od najłatwiejszego (zwykle .1 przed .3).
  4. Podpunkty są niezależne — jeśli nie umiesz 3.3, zrób 3.1 i 3.2. Punkty się sumują.
  5. Zapisuj co 10 minut i nazywaj pliki dokładnie tak jak każe polecenie.
  6. Pilnuj czasu — orientacyjnie ~4 min na punkt. Nie utknij 40 min na jednym podpunkcie za 2p.

Co oznacza wynik (orientacyjnie)

WynikInterpretacja
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
Matury rozszerzonej nie da się „zakuć na ostatnią noc" — to umiejętność (pisanie kodu, układanie zapytań), nie wiedza do wykucia. Liczy się regularna praktyka przez cały rok i przerabianie arkuszy.

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)

Arkusz CKE · matura maj 2026 · poziom rozszerzonyTegoroczny arkusz w całości — oryginalne treści zadań, dane i punktacja.

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łPunktyWaga
Programowanie (napisy, struktury, pliki)1734%
Algorytmika i rekurencja1224%
Bazy danych (SQL)918%
Arkusz kalkulacyjny + modelowanie918%
Teoria (systemy, sieci)36%
Wniosek: 58% punktów (29/50) to algorytmika + programowanie. SQL i Excel to po 18% i opierają się na powtarzalnych szablonach — najszybszy zwrot. Teoria 6%, ale przy ~3h nauki to pewny komplet.

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:

PlikWierszyStrukturaZadanie
pary.txt500dwa słowa w wierszu (spacja), litery a–d, max 503
korpo.txt50 000liczba/wiersz = nr przełożonego pracownika i; 0 = prezes4
staw.txt365 dniTAB: Data, Temp, Opady7
klienci.txt2045TAB: IdKlienta, Imie, Nazwisko, Plec (K/M)8
transakcje.txt1000TAB: IdTransakcji, DataTransakcji, IdKlienta, IdSklepu, IdSprzedawcy8
opis_transakcji.txt1466TAB: IdTransakcji, IdProduktu, Cena, Liczba8
Pułapki w danych — to wywala na egzaminie:
  • 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 Pythonie float("9,8") wybucha — trzeba float(x.replace(",", ".")).
  • transakcje.txt — 163 transakcje mają puste IdSprzedawcy = kasy samoobsługowe. To klucz do zadania 8.3! Po split("\t") to "", nie None.
  • opis_transakcji.txt — wiele wierszy na jedną transakcję (pozycje koszyka). Kwota = suma Cena × Liczba.
  • korpo.txt — drzewo zapisane jako tablica rodziców. Pracownik i jest w linii i (od 1).
  • Wszędzie separator TAB (\t) poza pary.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.

Klucz całego zadania: 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 = 777
  • A(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 ✓.

nliczba wywołańi-ty drugi argument
832³⁻ⁱ
2ᵏk2ᵏ⁻ⁱ
2ᵏ−1k−12ᵏ⁻ⁱ−1
Sedno dydaktyczne: w zadaniach rekurencyjnych CKE najpierw szukaj wzoru zamkniętego (tu: mnożenie). Bez tego 1.2 i 1.3 wymagają mozolnego śledzenia stosu wywołań — wykonalne, ale wolne i podatne na błąd przy dużych n.

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ą".

Dlaczego zakaz stringów? Bo CKE sprawdza czy umiesz operować na cyfrach arytmetycznie (% 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))
Odpowiedzi (zweryfikowane na realnym pary.txt):
  • 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)))
CKE nie nazywa tego „drzewem" — sam musisz rozpoznać strukturę przełożony→podwładny. Bez memoizacji liczenie głębokości dla każdego osobno to O(n²) i przy 50k może nie zdążyć. Z memoizacją — O(n).
Odpowiedzi (zweryfikowane na realnym korpo.txt): 4.2 liście = 25113  ·  4.3 najwięcej bezpośrednich podwładnych: pracownik 2 (19 podwładnych)  ·  4.4 najwieksza liczba przełożonych = 22, ma ja 2 pracownikow.

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.

Przypomnienie — jak działa system pozycyjny. W systemie o podstawie B cyfry ważą kolejne potęgi B (od prawej: B⁰, B¹, B²…). Np. w piątkowym (B=5): 1440₅ czytamy jako
1·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 ✓
Odpowiedź: w piątkowym 1212₅, w trójkowym 1000220₃.
Metoda „dziel i spisuj reszty" działa dla każdej podstawy: dzielisz liczbę całkowicie przez podstawę, reszta to kolejna cyfra (od końca), iloraz dzielisz dalej — aż dojdziesz do 0. Reszty czytasz od dołu do góry.

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ŻELI lub 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), potem MAX per 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
Walidacja modelu: przy 80 amurach model daje 30 kwietnia rano = 25,79% — dokładnie tyle, ile podaje arkusz. To potwierdza, że kolejność (odjęcie → wzrost) jest poprawna.
  • 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²).
Odpowiedzi (zweryfikowane symulacją): 7.3 → dzień 167 (14 sierpnia 2023, ~75,4%)  ·  7.4 → 94 amury (przy 93 rzęsa dochodzi do 52,5%, przy 94 maksymalny zarost spada do 49,2% ≤ 50%).
Pułapka: przecinek dziesiętny w kolumnie Temp przy imporcie. Ustaw separator albo użyj 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).

Mini-słownik SQL (przyda się w każdym podpunkcie):
  • SELECT … FROM tabela — wybierz kolumny z tabeli
  • WHERE warunek — tylko wiersze spełniające warunek
  • GROUP BY X — zgrupuj wiersze o tym samym X i licz na grupach
  • COUNT(*) = ile wierszy, SUM(…) = suma, COUNT(DISTINCT X) = ile różnych X
  • ORDER 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).

Odpowiedzi (zweryfikowane na realnych plikach):
  • 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
Cały warsztat SQL w jednym zadaniu: JOIN + agregaty (8.1), podzapytanie + 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))
Jak ćwiczyć ten arkusz: nie rozwiązuj wszystkiego naraz. Wracaj do konkretnego zadania po przerobieniu danego działu — Zad. 5 po systemach, Zad. 2 po algorytmach na liczbach, Zad. 4 po strukturach danych, Zad. 8 po SQL, Zad. 7 po arkuszu kalkulacyjnym. W ostatnich tygodniach przed maturą — cały arkusz na czas (210 min).

🐍 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.

Jak uruchomić: zapisz kod do pliku .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%';
Zweryfikowane odpowiedzi: Zad 2 → 0 i 3 · Zad 3 → 2206 / 18 / 7 par · Zad 4 → 25113 liści, prac. 2 (19), głęb. 22 · Zad 7 → dzień 167 / 94 amury · Zad 8 → Marcelino Kruk (8), 689 K / 651 M, 28 sklepów / 19 443,29 zł, sprzedawca 14 (styczeń).

📘 Maj 2026 — Zadanie 2 (dodawanie): pełny przewodnik

Arkusz CKE · matura maj 2026 · poziom rozszerzony · zadanie 2Oryginalna treść i punktacja — dokładnie to, co było na tegorocznym egzaminie.

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.

Subtelność, którą łatwo przeoczyć. Liczba przeniesień nie jest równa liczbie cyfr liczby. Może być zero (gdy żadna kolumna nie przekracza 9, np. 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.
Pułapka językowa. "Liczba przeniesień" to ile razy w trakcie dodawania kolumna wyprodukowała 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.

Kluczowa obserwacja. Dla każdej kolumny 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.

Metoda zliczania: dla każdej kolumny i liczymy 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
Odpowiedź: 37932 + 12528 = 50460, liczba przeniesień = 3.

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.

Odpowiedź: 88765 + 11111 = 99876, liczba przeniesień = 0.

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.

Odpowiedź: 456789 + 222222 = 679011, liczba przeniesień = 3.

Tabela zbiorcza odpowiedzi

aba + bLiczba przeniesień
3793212528504603
8876511111998760
4567892222226790113
Częsta pomyłka. Uczniowie liczą tylko te kolumny, w których obie cyfry są "duże", ignorując carry wpływające z kolumny niższej. W wierszu 3 sama kolumna setek (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ę.
Sanity check dla wiersza 2. Jeśli największa cyfra w liczbie 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

KategoriaCo wolno
Arytmetyka+ - * /, div (czyli //), mod (czyli %)
Porównania= < > <= >= !=
Logiczneand, or, not
Sterująceif, else, while, for
Inneprzypisanie <- / =, 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

ParametrWartość
Liczba iteracjik - liczba cyfr (np. dla 5-cyfrowej liczby: 5 obrotów pętli)
Złożoność czasowaO(k) = O(log10 a)
Złożoność pamięciowaO(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.

Zasada generalna: egzaminator nie sprawdza tylko wyniku dla jednego przykladu z arkusza. Symuluje algorytm na 2-3 wlasnych przypadkach brzegowych (sama 9-tki, jedna cyfra przeniesienia konca, rowne cyfry sumujace sie do 10). Jezeli algorytm przejdzie tylko 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.

Materiał własny MoreTutor · nie z arkusza CKEZadania napisane pod ten sam schemat — trening po omówieniu oryginału.

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.

Przypomnienie reguly: przeniesienie w kolumnie powstaje, gdy cyfra_a + cyfra_b + przeniesienie_z_poprzedniej_kolumny >= 10. Liczymy od najmlodszej cyfry (prawej) do najstarszej (lewej).
Trening 1. Policz liczbe przeniesien dla dodawania 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.

Trening 2. Policz liczbe przeniesien dla 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)
Lawine carry latwo zbudowac: wez liczbe pelna dziewiatek (np. 9999) i dodaj 1: przeniesienie idzie przez wszystkie pozycje. Tutaj 19999 + 80001 robi to samo zjawisko, tylko z innymi cyframi startowymi.
Trening 3. Skonstruuj pare liczb 4-cyfrowych, ktorych dodawanie pisemne daje dokladnie 0 przeniesien. Wyjasnij regule.
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.

Trening 4. Skonstruuj pare liczb 3-cyfrowych dajacych dokladnie 3 przeniesienia.
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).

Trening 5. Skonstruuj pare liczb 4-cyfrowych dajaca 0 przeniesien, ale o sumie cyfr 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)
Wniosek dydaktyczny: liczba przeniesien NIE jest proporcjonalna do wielkosci wyniku ani do sumy cyfr. 9999 (wynik 9090+0909) jest 9x wieksze od 1110 (555+555), a powstalo bez ani jednego carry. Przeniesienia opisuja strukture cyfr, nie wielkosc.
Trening 6 (rozszerzenie). Zmodyfikuj algorytm z punktu 2.2 tak, zeby liczyl przeniesienia przy dodawaniu pisemnym w systemie binarnym. Wyjasnij na czym polega "przeniesienie binarne".
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
Uogolnienie: ten sam algorytm dziala dla dowolnej podstawy 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.
Wazne na egzaminie: w zadaniu 2.2 arkusz maturalny wymaga liczb o tej samej liczbie cyfr. Algorytm binarny w zadaniu 6 dziala dla DOWOLNYCH liczb (bo warunek petli 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

Arkusz CKE · matura maj 2026 · poziom rozszerzony · zadanie 3Oryginalna treść i punktacja — dokładnie to, co było na tegorocznym egzaminie.

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.

Dlaczego napisy? CKE wybiera operacje na napisach prawie co roku, bo to fundament informatyki stosowanej: kompresja (gzip, brotli), wyszukiwarki (indeksy odwrocone), kryptografia (haszowanie, podpisy), parsing kodu zrodlowego (kompilatory), bioinformatyka (dopasowanie DNA), NLP (tokenizacja). Kazdy z tych obszarow w wersji szkolnej daje sie sprowadzic do trzech operacji: zliczanie, porownywanie, dopasowywanie wzorca. Dokladnie te trzy operacje masz tu na talerzu.

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

WlasnoscWartosc
Liczba wierszy500
Format wierszaslowo1 slowo2\n
Alfabettylko a-z (male litery)
Dlugosc pojedynczego slowaod 1 do 50 znakow
Separator wewnatrz wierszapojedyncza spacja
Plik testowypary_przyklad.txt (odpowiedzi w tresci zadania)
Plik wynikowywyniki3.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]
Strategia rozwiazywania na maturze: najpierw uruchom kazdy algorytm na 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
...
Pulapka formatowa: w 3.3 wynikow jest kilka (siedem par na realnym 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

SlowoZnakord()Suma czastkowa
okoo111111
k107218
o111329
piesp112112
i105217
e101318
s115433

|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

LiteraWystapienia w adabbcddeWystapienia w aadabbbccdcmin(.,.)
a232
b232
c131
d322
e100
Suma7

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.

Dlaczego 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 == sufiks s2. Maks dlugosc k taka, ze s1[:k] == s2[-k:].
  • Wariant B: prefiks s2 == sufiks s1. Maks dlugosc k taka, ze s2[: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) — generator ord(z) for z in s daje kody znakow, sum agreguje.
  • 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] — pierwsze k znakow s1; s2[-k:] — ostatnie k znakow s2.
  • or — krotkie spiecie. Jesli pierwsze porownanie wyjdzie True, drugie nie jest liczone.
  • W zad_3_3 nie 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()
Dlaczego brute-force wystarcza. 500 par x max 50 znakow x 50 iteracji w 3.3 to 1,25 mln porownan stringow. Python zrobi to w ulamku sekundy. Nie trzeba KMP, z-funkcji ani hashowania Rabina-Karpa — egzamin CKE ocenia poprawnosc i czytelnosc, nie zlozonosc asymptotyczna.

Zweryfikowane odpowiedzi na realnym pary.txt

PodpunktPara / lista parWartosc
3.1gpeeazeugmvsbzwsrxfplqdbakoxxe lhpbmoirdm2206
3.2aacbcccaacacbcabac cccccaaaacaccbabcba18
3.37 par (ponizej)dlugosci 5-7
#s1s2Dlugosc
1bbbbaabbababbaaaabaaaaabaaabbbabab5
2aababbbababbbbbbaabbbbbaabbababababa7
3aaaababaaaabbbbaabbbbbabbbaaaa6
4bbbbabaaabbbabbaaababaabbbbbbba5
5ccccabacbbaacbbabcbcbcbaa5
6caabbccabccccabccccabbaac6
7abaacabcccccabbbcabbbcbbbbbcabaca5
Sanity check na pary_przyklad.txt (odpowiedzi z tresci zadania):
  • 3.1 → eddcd eededcddceeecededcc 1403
  • 3.2 → ccccdcedddddecde ccdcdddecc 10
  • 3.3 → ececdddeed dddeedd 6
Wszystkie trzy musza sie zgadzac przed puszczeniem na 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

Najczestsza pomylka w 3.3. Tresc CKE mowi: prefiksosufiks pary (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:]

WyrazenieZnaczeniePrzyklad 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.

Demonstracja: 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.
Napiska[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
Regula: gdy chcesz sufiks dlugosci k, zawsze 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 dlugosc
  • range(5, min(...) + 1) — preemptywne filtrowanie; gubicie pelne maksimum, ktore moze byc potrzebne do dalszej decyzji
  • range(1, max(len(s1), len(s2)) + 1) — slice'y poza zakresem krotszego slowa
Filtrowanie >= 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

Komisja punktuje tez forme. Plik wynikowy musi miec prefiks numerem podzadania (etykieta 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))
Regula kciuka: jesli wasz wynik dla 3.3 daje 0 par albo 1 pare, najpewniej testujecie tylko jeden kierunek prefiksosufiksu. Poprawny wynik na realnym pary.txt to 7 par o dlugosci ≥ 5.
Materiał własny MoreTutor · nie z arkusza CKEZadania napisane pod ten sam schemat — trening po omówieniu oryginału.

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.

Trening 1: Oblicz 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).

LiteraASCII
m109
a97
t116
u117
r114
a97
f("matura") = 109 + 97 + 116 + 117 + 114 + 97 = 650
sum(ord(c) for c in "matura")  # 650
Trening 2: Dla pary s1="kotek", s2="piesek" policz sume wspolnych wystapien wszystkich liter alfabetu (definicja W z 3.2).
Pokaz odpowiedz
Literad(x, "kotek")d(x, "piesek")min
e121
i010
k211
o100
p010
s010
t100

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
Trening 3: Czy para s1="matka", s2="kura" ma prefiksosufiks o dlugosci co najmniej 2? Sprawdz oba kierunki dla dlugosci 2, 3 i 4.
Pokaz odpowiedz
KierunekDl.PrefiksSufiksRowne?
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).

Typowy blad: sprawdzenie tylko kierunku A i ogloszenie braku prefiksosufiksu. Definicja jest symetryczna wzgledem rol s1/s2.
Trening 4: Skonstruuj pare slow, dla ktorej maksymalny prefiksosufiks ma dlugosc dokladnie 5. Zapisz pare, wskaz wspolny fragment i kierunek.
Pokaz odpowiedz

Najprostsza konstrukcja: pieciolitrowy rdzen otoczony znakami gwarantujacymi brak rozszerzenia do 6.

s1s2Wspolny fragmentKierunek
abcdexyabcdeabcdeA: 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
Trening 5 (mini-projekt): Wyobraz sobie wariant zadania, w ktorym plik zawiera 500 par liczb calkowitych (np. 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 kodzieWersja slownaWersja liczbowa
Parsowanies1, 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))
CounterCounter(s)Counter(str(n))
Prefiksosufikss1[: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

Arkusz CKE · matura maj 2026 · poziom rozszerzony · zadanie 4Oryginalna treść i punktacja — dokładnie to, co było na tegorocznym egzaminie.

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 i12345678
szef[i]01122445

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

PodpunktPytanieOperacja na tablicy szef
4.1analiza malej hierarchii (papier)narysuj drzewko, policz reka
4.2ilu jest liscipolicz numery, ktore nie wystepuja jako wartosc
4.3kto ma najwiecej bezposrednich podwladnychpolicz wystapienia kazdej wartosci, znajdz maksimum
4.4maksymalna glebokosc hierarchiidla kazdego i idz po szef[] az do 0, znajdz najdluzszy lancuch

Trzy wlasciwosci parent-array, ktore warto wbic do glowy

  1. Pamiec liniowa — N wierzcholkow zajmuje N liczb, niezaleznie od ksztaltu drzewa.
  2. Wedrowka w gore jest natychmiastowa — z dowolnego wierzcholka do korzenia idziesz w O(glebokosc), bez rekursywnego DFS-a.
  3. 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 i12345678
parent[i]01212234

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 pracownikaLiczba bezposrednich podwladnychLiczba wszystkich podwladnych
127
23 (z arkusza)4 (z arkusza)
311
411
500
600
700
80 (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.
Szybka sprawdzka: SUMA wartosci w kolumnie "bezposrednich podwladnych" = liczba pracownikow z przelozonym = n - 1 = 7. Sprawdz: 2 + 3 + 1 + 1 + 0 + 0 + 0 + 0 = 7. OK. Gdyby ci nie wyszlo, znaczy ze cos zle policzyles lub zgubiles jednego.

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

PodpunktPytanieMetodaWynik na korpo.txt
4.2Ile lisciNumery NIEobecne w kolumnie parent[]25113
4.3Kto ma najwiecej bezposrednich podwladnychZliczanie wystapien w kolumnie parent[]pracownik 2 (19 podwladnych)
4.4Maksymalna glebokosc (liczba przelozonych nad jednym pracownikiem)Dla kazdego i ida w gore po parent[], licz kroki, bierz max (z memoizacja)22 i 2
Najczestszy blad w 4.1: mylenie "bezposrednich" z "wszystkimi" podwladnymi. Bezposredni to TYLKO dzieci w drzewie (jeden poziom). Wszyscy to caly potomek (wszystkie poziomy w dol). Dla pracownika 2: bezposrednich 3 (dzieci: 3, 5, 6), wszystkich 4 (3, 5, 6, 7) — bo 7 jest dzieckiem 3, czyli posrednim podwladnym 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.

PodpunktPytanieWynik
4.2Liczba lisci (pracownicy bez podwladnych)25113
4.3Pracownik z najwieksza liczba bezposrednich podwladnychnr 2, 19 podwladnych
4.4Maksymalna glebokosc (liczba przelozonych nad jednym pracownikiem)22 i 2

Wczytanie pliku — fundament dla 4.2, 4.3, 4.4

Reprezentacja w pamieci. Wczytujemy plik do listy 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] — wartosc 0 w szef oznacza 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.
Wynik 4.2: 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 pobiera ile_podwladnych[k] i to jest wartosc porownywana.
  • Zwraca klucz, ktory dal najwieksza wartosc. W zadaniu zagwarantowano, ze taki pracownik jest jeden.
Wynik 4.3: pracownik 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 jak szef, 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.

Wynik 4.4: 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).
Alternatywa iteracyjna. Mozna zamiast 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):

i12345678
szef[i]01122446

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

PodpunktOperacjaZlozonoscCzas dla n = 50000
4.2Counter na liscieO(n)ponizej 100 ms
4.3max po slownikuO(k) gdzie k = liczba szefowponizej 50 ms
4.4DFS z memoizacjaO(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.

Reguly gry. Pracownicy numerowani 1..50000. Wartosc 0 w pliku oznacza "brak przelozonego" (prezes), nie pracownika numer zero. Drzewo ma dokladnie jeden korzen. Kazda pomylka w tym miejscu propaguje sie na wszystkie trzy podpunkty.

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.

Test sanity przed oddaniem. Dla 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.
Skrot mentalny. Jeden sentinel None na indeksie 0, jeden del ile[0], jedna memoizowana funkcja glebokosci. Trzy linie obrony przed siedmioma pulapkami.
Materiał własny MoreTutor · nie z arkusza CKEZadania napisane pod ten sam schemat — trening po omówieniu oryginału.

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.

Trening 1: Dla tabelki 8-osobowej z sekcji "Maly przyklad" (parent = [_,0,1,2,1,2,2,3,4]) podaj, ilu pracownikow ma dokladnie jednego bezposredniego podwladnego.
Pokaz odpowiedz

Z tabeli zliczen: pracownicy 3 i 5 maja po 1 podwladnym (odpowiednio 7 i 8). Odpowiedz: 2 pracownikow.

Trening 2: Jaka jest srednia liczba przelozonych przypadajaca na jednego pracownika (bez prezesa) w tej samej hierarchii?
Pokaz odpowiedz

Suma glebokosci dla pracownikow 2..8: 1+1+2+2+2+2+3 = 13. Liczba pracownikow: 7. Srednia: 13/7 ≈ 1,86.

Trening 3: Skonstruuj tablice szefow dla drzewa: prezes (1) ma dwoch podwladnych (2, 3), a kazdy z nich po trzech (2 → 4,5,6; 3 → 7,8,9). Lacznie 9 pracownikow.
Pokaz odpowiedz
              1
            /   \
           2     3
          /|\   /|\
         4 5 6 7 8 9
i123456789
szef[i]011222333

Czyli szef = [0, 0, 1, 1, 2, 2, 2, 3, 3, 3] (pierwszy 0 to padding indeksu, drugi 0 to szef prezesa).

Trening 4: Zmien rozwiazanie 4.4 na wersje iteracyjna (bez rekurencji, bez 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()))
Trening 5: Rozszerz 4.3 — wypisz trzech pracownikow z najwieksza liczba bezposrednich podwladnych (top 3).
Pokaz odpowiedz
print(ile_podwladnych.most_common(3))
# Counter ma wbudowana metode most_common(k) — zwraca liste par (klucz, liczba)
Trening 6 (mini-projekt): Dodaj funkcjonalnosc "wypisz wszystkich podwladnych pracownika i" — bezposrednich i posrednich, czyli cale poddrzewo w dol. Wykorzystaj BFS. Wskazowka: najpierw zbuduj liste dzieci (odwrotnosc relacji szef → podwladny), potem BFS.
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

Arkusz CKE · matura maj 2026 · poziom rozszerzony · zadanie 5Oryginalna treść i punktacja — dokładnie to, co było na tegorocznym egzaminie.

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:

PozycjaCyfraWagaWkład
3 (tysiące)1103 = 10001000
2 (setki)4102 = 100400
1 (dziesiątki)4101 = 1040
0 (jedności)0100 = 10

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 0 do B-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 · Bpozycja po wszystkich pozycjach.
Pułapka, w którą wpadają wszyscy: w systemie piątkowym cyfra 5 NIE ISTNIEJE. Dozwolone cyfry to 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:

SystemRozbicieWartość (w dziesiętnym)
dziesiętny (B=10)1·1000 + 4·100 + 4·10 + 0·11440
piątkowy (B=5)1·125 + 4·25 + 4·5 + 0·1245
ósemkowy (B=8)1·512 + 4·64 + 4·8 + 0·1800

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

  1. Weź liczbę dziesiętną N.
  2. Podziel przez B. Zapisz iloraz q i resztę r.
  3. Reszta r jest najmłodszą cyfrą wyniku (skrajnie prawą).
  4. Iloraz q staje się nową liczbą N. Wróć do kroku 2.
  5. Powtarzaj, dopóki iloraz nie spadnie do 0.
  6. 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.

Mini-check zrozumienia.
  • Ile cyfr ma system o podstawie 7? (Odp.: 7 — od 0 do 6.)
  • Czy zapis 2087 jest poprawny? (Odp.: nie — cyfra 8 nielegalna w bazie 7.)
  • Ile wynosi 10B w dziesiętnym dla dowolnego B ≥ 2? (Odp.: zawsze B. To jest sens "dziesiątki" — zawsze oznacza podstawę systemu.)
Jeśli wszystkie trzy poszły gładko, masz fundament.

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ą.

Reguła kciuka. Każda cyfra w systemie o bazie b niesie wagę bk, gdzie k to pozycja licząc od prawej, startując od zera. Konwersja w drugą stronę to dzielenie z resztą przez b; reszty czytamy od dołu do góry.

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ę.

Co ugruntowaliśmy. Dwa kierunki konwersji w dwóch bazach (3 i 5) z tabelarycznym śladem. Dokładnie te same dwie operacje rozłożą zadanie 5 — najpierw zamieniamy 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

PozycjaCyfraWaga (5k)Iloczyn
31125125
2425100
14520
0010
Suma245

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

PozycjaCyfraWagaIloczyn
31125125
222550
1155
0212
Suma182

245 + 182 = 427. Lewa strona zgadza się.

Prawa równość: 42710 = Y3 − 1100023

Krok 1. Konwersja 1100023 → baza 10

PozycjaCyfraWaga (3k)Iloczyn
51243243
418181
30270
2090
1030
0212
Suma326

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

PozycjaCyfraWagaIloczyn
61729729
502430
40810
30270
22918
1236
0010
Suma753

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
Odpowiedź: brakujące liczby to 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".

Najdroższa pułapka egzaminacyjna: użycie cyfry, która w danym systemie nie istnieje. To natychmiastowe zero za fragment, bez dyskusji.

Pułapka 1 — cyfry spoza zakresu systemu

System o podstawie B dopuszcza wyłącznie cyfry 0, 1, ..., B-1.

System (podstawa)Dozwolone cyfryPrzykład błędu
Dwójkowy (2)0, 11022 — brak cyfry 2
Trójkowy (3)0, 1, 210003003
Piątkowy (5)0, 1, 2, 3, 415405
Ósemkowy (8)0..7788
Szesnastkowy (16)0..9, A..F1G16

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.

Checklist przed oddaniem zadania:
  1. Każda cyfra w odpowiedzi mieści się w zakresie 0..B-1?
  2. Konwersja powrotna daje tę samą liczbę dziesiętną?
  3. Reszty z dzielenia odczytane od dołu?
  4. 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.

HEXBIN (4 bity)HEXBIN (4 bity)
0000081000
1000191001
20010A1010
30011B1011
40100C1100
50101D1101
60110E1110
70111F1111

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
+50000 0101
−51111 1011 (negacja 0000 0101 = 1111 1010, +1 = 1111 1011)
−11111 1111
min na 8 bitach1000 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.

NBityBo 2k
100010210=1024 > 1000
1 000 00020220=1 048 576
255828=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.

Materiał własny MoreTutor · nie z arkusza CKEZadania napisane pod ten sam schemat — trening po omówieniu oryginału.

Zadania własne — ćwiczenia

Sześć zadań rozgrzewkowych. Cel: zautomatyzować trzy schematy — dzielenie przez bazę, rozwinięcie pozycyjne i dodawanie z konwersją przez dziesiętny.

Reguła pracy: rób na brudno tabelkę pozycji wagowych ZANIM zaczniesz liczyć. Wagi to potęgi bazy: dla bazy 5 to 1, 5, 25, 125, 625; dla bazy 3 to 1, 3, 9, 27, 81, 243, 729; dla bazy 16 to 1, 16, 256.
Trening 1: Zamień 11011012 na system dziesiętny.
Pokaż odpowiedź
Cyfra1101101
Waga6432168421
Wkład643208401

Suma: 64 + 32 + 8 + 4 + 1 = 109. Odpowiedź: 109.

Trening 2: Zamień 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.

Trening 3: Zamień FF16 na system dziesiętny.
Pokaż odpowiedź

W bazie 16 cyfra F oznacza wartość 15. FF16 = 15·16 + 15·1 = 240 + 15 = 255.

Trening 4: Wykonaj 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.

Trening 5: Ile cyfr binarnych ma liczba 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.

k2^k
9512
101024

Ponieważ 512 < 1000 < 1024, najwyższa cyfra binarna ma wagę 2^9. Cyfr jest 9 + 1 = 10. Pełna konwersja: 100010 = 11111010002.

Trening 6: Skonwertuj 2710 na bazy 2, 3, 4, 5, 8, 16. Zwróć uwagę na liczbę cyfr w każdej reprezentacji.
Pokaż odpowiedź
BazaReprezentacjaLiczba cyfrWeryfikacja
211011516+8+2+1 = 27
3100041·27 = 27 (czysta potęga trójki)
412331·16 + 2·4 + 3 = 27
510231·25 + 0·5 + 2 = 27
83323·8 + 3 = 27
161B21·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))
Trening 7 (analogiczne do maturalnego): Uzupełnij: 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

  1. 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)
  2. Jakie wagi mają kolejne pozycje w systemie dwójkowym, licząc od prawej? (1, 2, 4, 8, 16, 32, 64, 128, 256, 512...)
  3. Ile to jest 2 do potęgi 8 i 2 do potęgi 9? (256 i 512)
  4. 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)
  5. Jakie cyfry występują w systemie piątkowym? (tylko 0, 1, 2, 3, 4 — nigdy 5)
  6. 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

MinutyCzynnośćCel pedagogiczny
0-5Pytania diagnostyczne — szybka rozmowa o wagach pozycyjnychSprawdzenie czy uczeń rozumie zapis pozycyjny w ogóle
5-15Wspólna zamiana 427 na system dwójkowy metodą dzielenia z resztą na tablicyWyrobienie mechanicznego nawyku — każdy krok widoczny
15-22Weryfikacja 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-32Zamiana 427 na system piątkowy + zliczenie sumy cyfr (3202, suma=7)Przenoszenie techniki dzielenia z resztą na inną podstawę
32-40Rozwiązanie 5.3 — dyskusja o zakresie 256-511 i zapisie nierównościPołączenie liczby cyfr z zakresem liczbowym, myśl matematyczna
40-45Krótki sprawdzian — każdy zamienia 200 na dwójkowy w zeszycieUtrwalenie 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

Arkusz CKE · matura maj 2026 · poziom rozszerzony · zadanie 6Oryginalna treść i punktacja — dokładnie to, co było na tegorocznym egzaminie.

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.

Analogia listu pocztowego: koperte (pakiet) ma sens wyslac tylko gdy znasz adres odbiorcy. Adres IP = adres pocztowy urzadzenia. Router = poczta, ktora patrzy na adres i wybiera trase.

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.
Odpowiedz na zadanie: IPv4 = 32 bity (4 bajty). IPv6 = 128 bitow (16 bajtow).

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:

  1. Wiodace zera w grupie mozna pominac: 0db8 → db8, 0000 → 0
  2. 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

WartoscBityCo to jest
1 bajt8jeden oktet IPv4, jedna liczba 0-255
port TCP/UDP16zakres 0-65535
IPv4324 bajty
adres MAC486 bajtow, np. 00:1A:2B:3C:4D:5E (warstwa 2 OSI)
IPv612816 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.

CIDRMaska dziesietnieBity hostaHostow uzytkowychTypowe zastosowanie
/8255.0.0.02416 777 214Stara klasa A, RFC 1918 (10.0.0.0/8)
/16255.255.0.01665 534Srednia siec, 172.16.0.0/16
/24255.255.255.08254Typowa siec domowa / male biuro
/26255.255.255.192662Mala podsiec (np. dzial firmy)
/30255.255.255.25222Link 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:

ZakresCIDRLiczba adresowGdzie spotykasz
10.0.0.0 — 10.255.255.25510.0.0.0/8~16,7 mlnDuze firmy, korporacje
172.16.0.0 — 172.31.255.255172.16.0.0/12~1 mlnSrednie sieci, Docker (default bridge 172.17.0.0/16)
192.168.0.0 — 192.168.255.255192.168.0.0/16~65 tysDomowy router (192.168.0.1 / 192.168.1.1)
Trick CKE: jesli widzisz w zadaniu adres typu 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

WarstwaNazwaPrzyklady
7AplikacjiHTTP, HTTPS, FTP, SMTP, DNS
6PrezentacjiTLS/SSL, kodowanie znakow
5SesjiZarzadzanie sesjami (rzadko explicit)
4TransportowaTCP, UDP (porty)
3SieciowaIP (IPv4, IPv6), ICMP, routing
2Lacza danychEthernet, MAC, ARP, Wi-Fi
1FizycznaKable, swiatlowod, fale radiowe
Uwaga o TLS/SSL: rozne podreczniki klasyfikuja go na warstwie 5, 6 lub "miedzy 4 a 7". Na maturze CKE umieszcza go najczesciej na warstwie 6 (prezentacji) — tej wersji sie trzymaj, ale wiedz, ze w realnej praktyce to klasyfikacja umowna.
Mnemoznik: Polacy Po Studiach Trzymaja Strasznie Lewe Fuchy (od gory) lub po angielsku All People Seem To Need Data Processing. IP siedzi na warstwie 3 — adresacja i routing pakietow miedzy sieciami.

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.

ProtokolBityBajtyCyfry hex w pelnym zapisie
IPv43248 (32 bity / 4 bity na cyfre)
IPv61281632 (128 bitow / 4 bity na cyfre)
1 bajt = 8 bitow. Jesli pytanie brzmi "w bitach", podajesz 32 i 128. Jesli "w bajtach" — 4 i 16. Nie mieszaj jednostek w jednej odpowiedzi.

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

MAC adres (48 bitow, np. 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).
Materiał własny MoreTutor · nie z arkusza CKEZadania napisane pod ten sam schemat — trening po omówieniu oryginału.

Zadania własne — ćwiczenia

Skrot do zapamietania: IPv4 = 32 bity = 4 bajty (zapis dziesietny, 4 oktety). IPv6 = 128 bitow = 16 bajtow (zapis szesnastkowy, 8 grup po 16 bitow). Z tego wynikaja wszystkie obliczenia liczby adresow, masek i podzialu na podsieci.
Trening 1: Podaj dlugosc adresu IPv4 i IPv6 w bitach.
Pokaz odpowiedz IPv4 = 32 bity, IPv6 = 128 bitow.
Trening 2: Ile bajtow zajmuje pojedynczy adres IPv6 w pamieci? A IPv4?
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.

Trening 3: Z ilu oktetow sklada sie adres IPv4 i jaki jest zakres wartosci kazdego oktetu?
Pokaz odpowiedz 4 oktety, kazdy w zakresie 0-255 (bo 28 = 256 wartosci, od 0 do 255 wlacznie).
Trening 4: Z ilu grup szesnastkowych sklada sie adres IPv6 i ile bitow ma kazda grupa?
Pokaz odpowiedz 8 grup po 16 bitow (4 cyfry hex × 4 bity = 16 bitow na grupe). 8 × 16 = 128 bitow.
Trening 5: Skroc adres IPv6: 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 ::.
Trening 6: Ile maksymalnie unikatowych adresow mozna zaadresowac w IPv4?
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.

Trening 7: Ile razy IPv6 ma wiecej mozliwych adresow niz IPv4?
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.
Trening 8: Ile adresow zawiera podsiec 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.

AdresRola
10.0.0.0Adres sieci — nie przypisujesz hostowi
10.0.0.1 ... 10.0.0.254254 uzyteczne adresy hostow
10.0.0.255Adres broadcast — nie przypisujesz hostowi
Trening 9: Ile hostow uzytkowych miesci sie w podsieci 192.168.10.0/27?
Pokaz odpowiedz 32 − 27 = 5 bitow hosta → 25 = 32 adresy → 32 − 2 (siec + broadcast) = 30 hostow uzytkowych.
Trening 10: Adres 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.

Trening 11: Adres 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.
Trening 12: Co oznacza adres 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.1 sprawdza, czy stos sieciowy w systemie w ogole dziala.
Trening 13: Na ktorej warstwie OSI dziala protokol TCP, a na ktorej IP?
Pokaz odpowiedz TCP — warstwa 4 (transportowa). IP — warstwa 3 (sieciowa). TCP segmentuje dane i gwarantuje dostarczenie, IP adresuje i trasuje pakiety.
Trening 14: Jaki rekord DNS zwroci adres IPv6 dla domeny 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

  1. Ile bitow ma jeden bajt? (8)
  2. Ile roznych wartosci mozna zapisac na 8 bitach? (256, czyli 2^8)
  3. Co znaczy zapis 192.168.1.1 — z czego sklada sie kazda liczba? (4 oktety po 8 bitow, kazdy 0-255)
  4. Co oznacza skrot "/24" przy adresie sieci? (24 bity to czesc sieci, reszta to host — 32-24=8 bitow na hosty)
  5. Czemu IPv6 powstal? (IPv4 ma za malo adresow, zaczelo brakowac okolo 2011 roku)
  6. 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

MinutyCzynnoscCel pedagogiczny
0-5Pytania diagnostyczne, zapis na tablicy: 1 bajt = 8 bitow, IPv4 = 32 bity, IPv6 = 128 bitowWyrownanie podstaw, rozroznienie bit/bajt
5-15Zadanie 6.1 — stosunek przestrzeni adresowej. Pokaz trik z odejmowaniem wykladnikow 2^96Praca z potegami dwojki, oszacowania rzedu wielkosci
15-25Zadanie 6.2 — maska /24. Na tablicy rozpisz 32 bity, zakresl 24 bity sieci, policz 2^8 = 256, odejmij 2 zarezerwowane = 254 hostyZrozumienie maski sieciowej, rozroznienie hostow od adresow
25-35Zadanie 6.3 — porownanie zapisow IPv4 (dziesietny po kropkach) vs IPv6 (hex po dwukropkach). Cwiczenie skracania "::" na 2-3 przykladachCzytanie i interpretacja zapisow adresow
35-42Cwiczenie utrwalajace — uczen samodzielnie liczy hostow dla /16, /28, /30Transfer wiedzy na inne maski
42-45Podsumowanie 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

Arkusz CKE · matura maj 2026 · poziom rozszerzony · zadanie 7Oryginalna treść i punktacja — dokładnie to, co było na tegorocznym egzaminie.

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:

  1. Wzrost 1,75% — zachodzi w nocy, pomiar nastepuje rano.
  2. Amury biale — kazdy zjada 0,25 m² dziennie. Strata regularna, codzienna, w dzien.
  3. 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.
Strategia egzaminacyjna: najpierw zbuduj model, sprawdz dzien 61 przeciwko 25,79%, dopiero potem dolicz pelne 184 dni i odpowiadaj na 7.3 i 7.4. Bez walidacji checkpointu mozesz miec wszystkie liczby zle i nawet o tym nie wiedziec.

Punktacja — co dostajesz za co

PodpunktPunktyCo realnie robisz
7.13SREDNIA.JEZELI po miesiacach + wykres kolumnowy z opisami
7.22Najdluzszy run zer w kolumnie Opady, per miesiac
7.32Symulacja 184 dni, znalezienie pierwszego dnia >75%
7.42Iteracja 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.

Pulapka numer jeden — przecinek dziesietny. Plik 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

  1. Dane → Z tekstu/CSV (Excel 365) lub Wstaw → Arkusz z pliku (LibreOffice).
  2. Separator: Tabulator.
  3. 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.
  4. Po imporcie w komorce zewnetrznej wpisz =SUMA(B2:B366). Jezeli zwraca 0 — 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

  1. Zaznacz dane A1:C366 (z naglowkami).
  2. Wstaw → Tabela przestawna.
  3. Pole Data przeciagnij do Wiersze. Excel sam zaproponuje grupowanie po miesiacach. (W LibreOffice klikasz prawym → Grupowanie → Miesiace.)
  4. 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

  1. Zaznacz F1:G13 (z naglowkami).
  2. Wstaw → Wykres → Kolumnowy → Kolumnowy 2-W zwykly.
  3. Kliknij wykres → Projektowanie → Dodaj element wykresu:
    • Tytul wykresu: Srednie miesieczne temperatury — Staw, 2022
    • Tytuly osi → Os pozioma: Miesiac
    • Tytuly osi → Os pionowa: Temperatura [°C]
  4. Etykiety miesiecy: kliknij os X prawym → Wybierz dane → Etykiety osi poziomej → Edytuj i wskaz liste nazw Sty, Lut, Mar, …, Gru.
Punktacja CKE. 1 pkt za poprawne wartosci srednich, 1 pkt za zaokraglenie do 1 miejsca, 1 pkt za wykres z tytulem i opisami osi. Brak tytulu osi = minus pol punktu w praktyce egzaminatora.

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

Pulapka graniczna: seria przez kilka miesiecy. Jesli ciag suchy ciagnie sie np. od 28 stycznia do 5 lutego, kolumna 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.

Model matematyczny (jeden dzien):
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.NaglowekWzor / wartosc
ADataA2 = 2023-03-01, A3 = A2+1, w dol do A185
BDzien tyg.=DZIEN.TYG(A2; 2) → 1=poniedz., 5=piatek
CPowierzchnia rano [m²]C2 = 2000; C3 = (C2 − $G$1·0,25 − JEZELI(B2=5; 60; 0)) · 1,0175
D% powierzchni=C2/10000
G1Liczba amurow (parametr)80 (dla 7.3), zmienna (dla 7.4)
Pulapka indeksowania: wiersz C3 liczy stan na 2 marca rano, czyli wynik nocy z 1 na 2 marca. Przy szukaniu "dnia od 1 marca" — dzien 1 to wiersz C2 (stan poczatkowy 2000 m²), dzien 2 to wiersz C3, itd. Dzien k jest w wierszu C(k+1).

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:

DzienDataPowierzchnia [m²]%
1662023-08-13 (niedz.)7427,1074,27%
1672023-08-14 (pon.)7536,7375,37%
1682023-08-15 (wt.)7648,2776,48%
Odpowiedz 7.3: rzesa pierwszy raz przekroczyla 75% powierzchni stawu w dniu 167, liczac od 1 marca 2023, czyli 14 sierpnia 2023 (poniedzialek). Powierzchnia w tym dniu rano: 7536,73 m² (75,37%).

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
Odpowiedz 7.4: 94 amury. Przy 93 amurach maksimum w okresie wynosi 52,50% (przekracza limit), przy 94 maksimum spada do 49,17% — to pierwsza wartosc spelniajaca warunek "maksymalnie 50%".

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.

Regula zlota: jezeli twoja symulacja 7.3/7.4 nie zwraca 25,79% na 30 kwietnia 2023 rano (czyli okolo 2579,5 m²), masz blad w modelu. Najpierw kalibruj na 30 IV, potem czytaj wynik finalny.

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,8 jako 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 typPnWtSrCzPtSoNd
1 (default)2345671
2 (ISO)1234567
30123456

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.

Mata kontrolna na 5 minut przed oddaniem arkusza:
  1. 30 IV rano przy 80 amurach = 25,79% (~2579,5 m²) — TAK / NIE.
  2. Dzien 167 (14 sierpnia) przekracza 75% — TAK / NIE.
  3. Przy 93 amurach maksimum > 50%, przy 94 maksimum ≤ 50% — TAK / NIE.
  4. Wykres ma tytul + 2 osie opisane + miesiace na osi X — TAK / NIE.
Cztery razy TAK = komplet 9 punktow.
Materiał własny MoreTutor · nie z arkusza CKEZadania napisane pod ten sam schemat — trening po omówieniu oryginału.

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.

Trening 1 — srednia z jednej kolumny (rozgrzewka, 2 min): W arkuszu 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.

Trening 2 — najdluzszy ciag dni bezopadowych w Excelu (8 min): W kolumnie B (B2:B185) masz opady w mm. W kolumnie E zbuduj licznik kolejnych dni bezopadowych: jezeli dzien jest bezopadowy (opad = 0), to licznik rosnie o 1, w przeciwnym razie zeruje sie. W komorce G1 zwroc najdluzszy ciag.
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.

Trening 3 — bez amurow i bez piatkowego odlowu (analitycznie + Python, 8 min): Ile dni potrzeba, aby rzesa z poczatkowych 2000 m² pokryla caly staw (10000 m²) przy regularnym wzroscie 1,75% dziennie? Rozwiaz analitycznie (logarytm) i potwierdz petla w Pythonie.
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.

Trening 4 — wzrost o 1% zamiast 1,75% (10 min): Zalozmy, ze biolog poprawia oszacowanie wzrostu rzesy na 1% dziennie (zamiast 1,75%). Pozostale warunki bez zmian — 1 marca 2023 (sroda), 184 dni, piatkowy odlow 60 m². Ile amurow wystarczy, by maksymalna powierzchnia rzesy nie przekroczyla 50% stawu?
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.

Trening 5 — ostrzejszy limit 30% (8 min): Zmodyfikuj kod z 7.4 tak, by szukal najmniejszej liczby amurow utrzymujacych pokrycie maksymalnie na poziomie 30% (czyli 3000 m²). Reszta parametrow oryginalna: start 1 marca 2023 (sroda) z 2000 m², 184 dni, wzrost 1,75%, piatkowy odlow 60 m².
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.

Trening 6 — bez piatkowego odlowu (10 min): W zadaniu 7.3 mamy 80 amurow i piatkowy odlow 60 m². A gdyby gospodarz zrezygnowal z piatkowego odlowu (zostaje tylko codzienne zjadanie przez 80 amurow)? Czy rzesa nadal przekroczy 75% w okresie 1 marca – 31 sierpnia 2023? W ktorym dniu?
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

  1. Co to znaczy "srednia miesieczna temperatura" — jak ja policzysz dla marca? (suma 31 wartosci / 31)
  2. Jak rozpoznasz w danych ze nastapil koniec ciagu dni bezopadowych? (pojawi sie wiersz z opadem > 0)
  3. Jaka powierzchnia rzesy odpowiada 75% stawu o powierzchni 10000 m2? (7500 m2)
  4. Ile dni jest od 1 marca do 31 sierpnia 2023? (184 dni — 31+30+31+30+31+31)
  5. 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)
  6. 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

MinutyCzynnoscCel pedagogiczny
0-5Pytania diagnostyczne, odczyt fragmentu polecenia 7.3 na glosSprawdzenie czy uczen rozumie regulamin symulacji
5-15Recznie w tabelce policzyc 3 doby (1, 2, 3 marca 2023): stan rano, w dzien, wieczorem, po nocyZrozumienie cyklu dobowego zanim padnie kod
15-20Zadania 7.1 i 7.2 — krotkie, na rozgrzewke (srednia + najdluzszy ciag)Latwe punkty, budowa pewnosci, czytanie pliku linia po linii
20-35Symulacja 7.3 w Pythonie — wspolnie pisana petla 184 dni, walidacja na 25.79%Glowna umiejetnosc: petla z licznikiem dnia tygodnia i warunkami
35-43Zadanie 7.4 — owijka petli zewnetrznej po liczbie amurowPokazanie ze symulacje mozna "wywolac wiele razy" zmieniajac parametr
43-45Podsumowanie: co dalo punkty, co zabralo czasStrategia 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

Arkusz CKE · matura maj 2026 · poziom rozszerzony · zadanie 8Oryginalna treść i punktacja — dokładnie to, co było na tegorocznym egzaminie.

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

TabelaOdpowiada na pytanieKolumny
klienciKIM jest klientIdKlienta, Imie, Nazwisko, Plec
transakcjeKIEDY zakup, GDZIE (sklep), U KOGO (sprzedawca lub kasa samoobsl.)IdTransakcji, DataTransakcji, IdKlienta, IdSklepu, IdSprzedawcy
opis_transakcjiCO konkretnie kupiono w danej transakcji i ile zaplaconoIdTransakcji, 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).

Regula kciuka: jesli pytanie dotyczy kto — siegasz do 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

KonstrukcjaCo robiPrzyklad
SELECT ... FROM ...Wybiera kolumny z tabeliSELECT Imie FROM klienci
WHEREFiltruje wiersze warunkiem (przed grupowaniem)WHERE Plec = 'K'
GROUP BYGrupuje wiersze o wspolnej wartosciGROUP BY IdKlienta
HAVINGFiltruje grupy po agregacjiHAVING COUNT(*) > 5
COUNT(*)Liczy wiersze w grupieSELECT IdKlienta, COUNT(*) FROM transakcje GROUP BY IdKlienta
COUNT(DISTINCT x)Liczy rozne wartosciCOUNT(DISTINCT IdSklepu)
SUM(x)Sumuje wartosci kolumnySUM(Cena * Liczba)
ORDER BY x DESCSortuje malejacoORDER BY COUNT(*) DESC
LIMIT nZwraca tylko n pierwszych wierszyLIMIT 1 (top 1)
JOIN ... ONLaczy tabele po wspolnym kluczuFROM 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

PodpunktTabele potrzebneWzorzecWynik
8.1klienci + transakcjegroup + count + top 1Marcelino Kruk (8 transakcji)
8.2klienci + transakcje (NOT IN)antyjoin + group by plecK = 689, M = 651
8.3transakcje + opis_transakcjifilter (pusty sprzedawca) + DISTINCT + SUM28 sklepow, 19443,29 zl
8.4transakcjegroup po (sprzedawca, miesiac) + count DISTINCT sklepow + top 1sprzedawca 14, miesiac 01, 3 sklepy
8.5Produkty + Kategorie + opis_transakcjiJOIN + LIKE + DISTINCTzapytanie SQL
Zlota zasada Zadania 8: najpierw narysuj na boku, ktore tabele potrzebujesz i jak je laczysz (kreska po wspolnym Id). Dopiero potem piszesz SQL. 80% bledow na tym zadaniu bierze sie z mylenia, gdzie znajduje sie dana informacja.

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.

Setup w SQLite (kilka polecen w shellu):
.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.

Setup w MySQL / phpMyAdmin (jesli pracujesz na prawdziwej bazie, nie SQLite):

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.

RoznicaSQLite (ten przewodnik)MySQL / phpMyAdmin
Puste pole przy imporciezawsze pusty string ''pusty string '' przy GUI CSV import; prawdziwy NULL tylko przez LOAD DATA + NULLIF
Fragment datySUBSTR(data, 4, 2) na teksciekolumna typu DATE + MONTH(data) / DATE_FORMAT(data, '%m')
LIMIT nLIMIT nLIMIT n (identyczne)
Laczenie tekstow||CONCAT(a, b)
Nazwy tabel/kolumnbez cudzyslowoww 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. Alias t skraca dalsze referencje.
  • JOIN klienci k ON k.IdKlienta = t.IdKlienta — do kazdej transakcji dokleja wiersz z klientem o tym samym IdKlienta.
  • 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.
Odpowiedz 8.1: Marcelino Kruk — 8 transakcji.
Anti-pattern: 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. DISTINCT zapobiega duplikatom.
  • Warunek zewnetrzny WHERE IdKlienta NOT IN (...) — filtruje klienci do tych, ktorych nie ma na liscie aktywnych.
  • Zliczenie z GROUP BY Plec — rozbija wynik na dwa wiersze.
Plecilu
K689
M651
Odpowiedz 8.2: 689 kobiet i 651 mezczyzn nie kupilo nic w okresie 1–3 dnia kazdego miesiaca, godz. 22–24.
Alternatywa rownowazna: 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 do Cena i Liczba. Klucz laczenia to IdTransakcji.
  • 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.
Odpowiedz 8.3: 28 roznych sklepow, lacznie zaplacono 19443,29 zl.

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.
IdSprzedawcymiesiacsklepy
14013
Odpowiedz 8.4: sprzedawca o identyfikatorze 14, miesiac styczen (01), obsluzyl klientow w 3 roznych sklepach.
Dlaczego nie samo 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

PodpunktOdpowiedz
8.1Marcelino Kruk — 8 transakcji
8.2K = 689, M = 651 (nic nie kupili)
8.328 roznych sklepow; 19443,29 zl lacznie
8.4sprzedawca 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 poleceniaOperacja SQLTabela uzywana do filtra
kategoria „spozywcze"WHERE k.NazwaKategorii = 'spozywcze'Kategorie
opis zawiera fragmentWHERE p.Opis LIKE '%do ekspresu kolbowego%'Produkty
zostal zakupionyJOIN opis_transakcji (domyslnie INNER)opis_transakcji
kazdy produkt razSELECT 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.

Dlaczego JOIN, a nie podzapytanie z IN? Oba dzialaja. JOIN + DISTINCT to klasyczna szkolna konstrukcja, ktora egzaminator widzi czesciej. Trzymaj sie wersji z JOIN-em — jest krotsza i czytelna.

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.

Punktacja CKE w 8.5: 2 pkt za zapytanie zwracajace komplet poprawnych produktow. 1 pkt: podzbior poprawnych albo poprawne + dodatki (np. niesprzedane). 0 pkt: brak filtra po opisie lub kategorii, brak warunku „zakupione", blad skladniowy.
Wariant „lista produktow zamiast zapytania": arkusz CKE w 8.5 wymaga zapisania samego zapytania SQL i nic wiecej. Gdyby jednak Twoj arkusz (albo polecenie nauczyciela) prosil dodatkowo o wypisanie konkretnych nazw — odpal zapytanie w SQLite/Access, skopiuj wynik z okienka rezultatu i dolacz jako tabele pod kodem. To dwie minuty pracy, ale bez uruchomienia zapytania trafiasz w opisy palcem na podstawie zgadywania.

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 liczymyZ czegoCo grupujemy
Liczba transakcji per klienttransakcje samoIdKlienta
Liczba pozycji per klienttransakcje JOIN opis_transakcjiIdKlienta
Wartosc zakupow per klienttransakcje JOIN opis_transakcjiIdKlienta, 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 SUBSTRING bywa 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).

Egzaminacyjny anti-pattern: proba rozwiazania calego zadania jednym wielkim zapytaniem z trzema 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.
Checklist przed oddaniem arkusza:
  1. Sprawdz typy kolumn po imporcie (PRAGMA table_info(transakcje)) — wszystko prawdopodobnie jako TEXT, wiec ceny trzeba przekonwertowac.
  2. Odpal SELECT COUNT(*) per tabela — porownaj z liczba wierszy w pliku minus naglowek.
  3. 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.
  4. Czytaj polecenie doslownie: „ile" wymaga liczby, „kto" — listy. Wpisanie zapytania zwracajacego liste przy poleceniu „podaj liczbe" to strata punktu mimo poprawnej logiki.
Materiał własny MoreTutor · nie z arkusza CKEZadania napisane pod ten sam schemat — trening po omówieniu oryginału.

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

Trening 1: Wypisz wszystkie kobiety zarejestrowane w bazie. Wynik: 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.

Trening 2: Ilu mamy klientow w bazie? Ile z nich to mezczyzni? Najprosciej dwoma zapytaniami.
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.

Trening 3: Top 3 klientow po liczbie transakcji w okresie. Wynik: 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.

Trening 4: Wypisz 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.

Trening 5: Wypisz klientow (imie, nazwisko), ktorzy w STYCZNIU nic nie kupili. Liczy sie brak jakiejkolwiek transakcji w tym miesiacu — pozostale miesiace nie maja znaczenia.
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.

Trening 6: Wskaz srednia wartosc transakcji dokonanej w kasie samoobslugowej.
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

MechanikaKiedy uzywaszPulapka
COUNT(*) vs COUNT(kol)Liczenie wierszy vs liczenie nie-NULL-iNULL w kolumnie zaniza wynik COUNT(kol)
WHERE vs HAVINGFiltr przed agregacja vs poWHERE SUM(...) > 100 wywala blad skladni
NOT IN z podzapytaniemNegacja przynaleznosciNULL w podzapytaniu daje pusty wynik
SUBSTR/Mid na dacie-napisieDaty trzymane jako tekstSkladnia zalezna od silnika — sprawdz dokumentacje
GROUP BY + wszystkie nieagregowane kolumnyKazde zapytanie z agregacja i dodatkowymi polamiBrak 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

  1. Co robi JOIN miedzy dwiema tabelami? (laczy wiersze po wspolnej kolumnie, np. IdKlienta)
  2. Czym rozni sie WHERE od HAVING? (WHERE filtruje wiersze przed grupowaniem, HAVING filtruje grupy po GROUP BY)
  3. Co zwraca COUNT(*) a co COUNT(kolumna)? (COUNT(*) liczy wszystkie wiersze, COUNT(kolumna) pomija NULL)
  4. Jak posortowac wynik malejaco i wziac tylko pierwsze 5? (ORDER BY ... DESC LIMIT 5)
  5. Czym sie rozni NOT IN (podzapytanie) od LEFT JOIN ... WHERE ... IS NULL? (oba znajduja "brakujace" — NOT IN czytelniejsze, LEFT JOIN szybsze na duzych danych)
  6. 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_transakcji bez 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 NULL w podzapytaniu, jesli kolumna moze byc NULL.
  • JOIN z opis_transakcji przy zliczaniu transakcji — uczen w 8.1 dorzuca JOIN opis_transakcji i 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

MinutyCzynnoscCel pedagogiczny
0-5Pytania diagnostyczne + szybki rzut oka na schemat 3 tabelSprawdzic czy uczen rozumie relacje klienci-transakcje-opis_transakcji
5-12Razem rozwiazac 8.1 (JOIN klienci+transakcje, GROUP BY, ORDER BY DESC, LIMIT 1) — wynik Marcelino KrukKlasyczny patron "top 1" — fundament dla 8.4
12-228.2 samodzielnie z podpowiedzia: "uzyj NOT IN i pamietaj o GROUP BY po plci"Wprowadzenie podzapytania z NOT IN na konkretnym przykladzie
22-328.3 razem — dwa osobne zapytania (COUNT DISTINCT sklepow, SUM po JOIN-ie z opis_transakcji), warunek = '' OR IS NULLPolaczenie filtra na pustym sprzedawcy z agregacja po dwoch tabelach
32-408.4 samodzielnie — SUBSTR na dacie, GROUP BY po dwoch kolumnach, COUNT(DISTINCT IdSklepu)Najtrudniejszy podpunkt: grupowanie po (sprzedawca, miesiac)
40-45Powtorka 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.

MinutyCzynnoscCel pedagogiczny
0-5Bez podpowiedzi: uczen ustnie tlumaczy roznice WHERE/HAVING i NOT IN/LEFT JOINSzybki sanity-check czy podstawy przetrwaly do dzis
5-15Runda 2 / Cwiczenie 1 (CASE WHEN + procent) samodzielnie, bez podgladania odpowiedziCASE WHEN nie padlo wczesniej — sprawdzasz czy transferuje wiedze na nowa konstrukcje
15-25Runda 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-35Runda 2 / Cwiczenie 6 (capstone: SUBSTR + JOIN + SUM + top-1 w jednym zapytaniu) bez dzielenia na krokiTest integracji — czy uczen samodzielnie sklada trzy poznane wzorce w jedno zapytanie
35-42Szybki przeglad pulapek z checklisty (info-box "Checklist przed oddaniem arkusza" wyzej) — uczen czyta na glos i mowi, ktora go kiedys zlapalaUtrwalenie przez werbalizacje wlasnych bledow, nie powtorzenie teorii
42-45Domkniecie: czego uczen NIE jest pewien — zapisz to i wroc do tego jako pierwszy punkt na sesji powtorkowej tuz przed egzaminemJasny kontrakt na ostatnia prosta zamiast ogolnikowego "powtorz sobie SQL"
Jesli uczen utknie na Cwiczeniu 6 (capstone): nie tlumacz od nowa 8.1-8.4 — zapytaj, ktory z trzech skladnikow (grupowanie po miesiacu / JOIN+SUM / top-1) sprawia problem, i cofnij sie punktowo tylko do tego jednego cwiczenia z pierwszej rundy, ktore go trenowalo. Ostatnia lekcja przed egzaminem to nie czas na powrot do podstaw calego zadania.

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?

Definicja System liczbowy to sposób zapisu liczb. Podstawa (baza) systemu to ilość dostępnych cyfr.
DEC (10) → 0-9 · BIN (2) → 0-1 · OCT (8) → 0-7 · HEX (16) → 0-9, A-F
SystemPodstawaCyfryPrzykład
Binarny (BIN)20, 11011₂ = 11₁₀
Ósemkowy (OCT)80–713₈ = 11₁₀
Dziesiętny (DEC)100–911₁₀
Szesnastkowy (HEX)160–9, A–FB₁₆ = 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)

Metoda: Każde 4 bity = 1 cyfra HEX. Zgrupuj bity od prawej i zamień.
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'
⚠️ Typowe zadanie maturalne: „Zamień liczbę 2A3₁₆ na system binarny" — używaj metody grupy po 4 bity: 2→0010, A→1010, 3→0011 → 001010100011₂

📝 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.

Do wklejenia na czat Meet — zadania 1.1-1.6
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?

KodZakresIle wartości
bez znaku0 … 255256
U2−128 … 127256

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

OperatorSymbolPythonOpis
AND (koniunkcja)∧ / ·andPrawda gdy OBA prawdziwe
OR (alternatywa)∨ / +orPrawda gdy CHOĆ JEDEN prawdziwy
NOT (negacja)¬ / !notOdwraca 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

Kluczowe na maturze!
¬(A ∧ B) = ¬A ∨ ¬B
¬(A ∨ B) = ¬A ∧ ¬B

Negacja koniunkcji → alternatywa negacji. Negacja alternatywy → koniunkcja negacji.

Inne prawa algebry Boole'a

PrawoANDOR
TożsamościA ∧ 1 = AA ∨ 0 = A
DominacjiA ∧ 0 = 0A ∨ 1 = 1
IdempotentnośćA ∧ A = AA ∨ A = A
DopełnienieA ∧ ¬A = 0A ∨ ¬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
⚠️ Typowe zadanie: „Dla jakich wartości p, q wyrażenie ¬(p ∨ q) ∧ (p ⊕ q) jest prawdziwe?" — Rozpisz tablicę prawdy, oblicz krok po kroku.

📝 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.

Do wklejenia na czat Meet — zadania 2.1-2.5
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?

ABA AND NOT BNOT A AND Bwynik
00000
01011
10101
11000

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?

ABsumaprzeniesienie
0000
0110
1010
1101

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

Na maturze CKE używa własnego pseudokodu:
• 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śćNazwaPrzykład
O(1)StałaDostęp do elementu tablicy T[i]
O(log n)LogarytmicznaWyszukiwanie binarne
O(n)LiniowaPrzejście po tablicy, szukanie max
O(n log n)Liniowo-log.Merge sort, Quick sort (avg)
O(n²)KwadratowaBubble sort, Selection sort
O(2ⁿ)WykładniczaGenerowanie 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

Na maturze często pytają o:
• 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)

Idea: Porównuj pary sąsiednich elementów i zamieniaj je, jeśli są w złej kolejności. Powtarzaj aż do posortowania.
# 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)

Idea: Znajdź minimum w nieposortowanej części i zamień z pierwszym elementem tej części.
# 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)

Idea: Bierz kolejny element i wstaw go we właściwe miejsce w posortowanej części.
# 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)

Wymaganie: Tablica MUSI być posortowana! Złożoność O(log n) — drastycznie szybsze od liniowego.
# 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

AlgorytmNajlepszyŚredniNajgorszyPamięć
Bubble SortO(n)O(n²)O(n²)O(1)
Selection SortO(n²)O(n²)O(n²)O(1)
Insertion SortO(n)O(n²)O(n²)O(1)
Szukanie linioweO(1)O(n)O(n)O(1)
Szukanie binarneO(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.

Do wklejenia na czat Meet — zadania 4.1-4.7
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.

PrzebiegTablica po przebieguZamiany w przebiegu
start8, 3, 5, 1, 9, 2—
13, 5, 1, 8, 2, 94
23, 1, 5, 2, 8, 93
31, 3, 2, 5, 8, 92
41, 2, 3, 5, 8, 91
51, 2, 3, 5, 8, 90

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.

Jak to zapisywać na maturze Tabela: wiersz na przebieg, w komórce cała tablica. Nie zapisuj każdej zamiany osobno — zajmie pół strony i zgubisz się. Jeśli polecenie mówi „po każdym przebiegu", to znaczy po przejściu całej pętli wewnętrznej, nie po każdej zamianie.

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.

KrokCo robimyTablica po kroku
start—8, 3, 5, 1, 9, 2
1minimum z całej tablicy to 1 — zamiana z pozycją 11, 3, 5, 8, 9, 2
2minimum z reszty to 2 — zamiana z pozycją 21, 2, 5, 8, 9, 3
3minimum z reszty to 3 — zamiana z pozycją 31, 2, 3, 8, 9, 5
4minimum z reszty to 5 — zamiana z pozycją 41, 2, 3, 5, 9, 8
5minimum z reszty to 8 — zamiana z pozycją 51, 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.

Kiedy to ma znaczenie Gdy zamiana jest kosztowna (duże rekordy, zapis na dysk), sortowanie przez wybór wygrywa mimo tej samej złożoności O(n²). To bywa treścią pytania „uzasadnij wybór algorytmu".

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 elementTablica po wstawieniu
start8, 3, 5, 1, 9, 2
33, 8, 5, 1, 9, 2
53, 5, 8, 1, 9, 2
11, 3, 5, 8, 9, 2
91, 3, 5, 8, 9, 2
21, 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ŚredniaNajgorszyUwaga
BąbelkoweO(n²)O(n²)z flagą przerwania: O(n) dla danych posortowanych
Przez wybórO(n²)O(n²)zawsze n−1 zamian
Przez wstawianieO(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 linioweO(n)O(n)działa na dowolnych danych
Wyszukiwanie binarneO(log n)O(log n)tylko na posortowanych

Dla n = 1000:

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

Kroklewyśrodekprawywartość w środkudecyzja
105102323 < 72 → idziemy w prawo, lewy = 6
268105656 < 72 → lewy = 9
3991072znaleziono, indeks 9

Szukamy 40 (nie ma go w tablicy):

Kroklewyśrodekprawywartość w środkudecyzja
105102323 < 40 → lewy = 6
268105656 > 40 → prawy = 7
36673838 < 40 → lewy = 7
47774545 > 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.

Warunek, o którym łatwo zapomnieć Wyszukiwanie binarne działa wyłącznie na tablicy posortowanej. Na nieposortowanej zwróci błędny wynik, nie błąd wykonania — i to jest gorsze, bo nic się nie wysypie. Jeśli zadanie każe wyszukiwać w danych z pliku, najpierw je posortuj albo użyj wyszukiwania liniowego.

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.

Dwie rzeczy, na których traci się punkty 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

Każda rekurencja wymaga:
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

CechaRekurencjaIteracja
CzytelnośćCzęsto lepszaMoż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, NWDSortowanie, zliczanie
⚠️ Na maturze: Rysuj drzewo wywołań! Przy każdym wywołaniu zapisuj parametry i wynik. Sprawdź ile razy wywoływana jest funkcja.

📝 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")
⚠️ Na maturze 2025: Dane mają ~1000+ wierszy. Nie kopiuj ich ręcznie — wczytuj programem! Wyniki zapisuj do pliku .txt.

📝 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

Krok 1 na maturze: Otwórz plik .txt w Excelu/Calc → Kreator importu → separator (średnik/tabulator) → kolumny z właściwymi typami.

Kluczowe funkcje

FunkcjaOpisPrzykład
SUMASuma zakresu=SUMA(B2:B100)
ŚREDNIAŚrednia arytmetyczna=ŚREDNIA(C2:C100)
MIN / MAXMinimum / Maksimum=MAX(D2:D100)
ILE.LICZBIle komórek z liczbami=ILE.LICZB(A:A)
DŁUGOŚĆIle znaków w tekście=DŁUGOŚĆ(A2)
LEWY / PRAWYN znaków od lewej/prawej=LEWY(A2;3)
FRAGMENT.TEKSTUWycinek 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

TypZapisZachowanie przy kopiowaniu
WzględneA1Zmienia się (przesunięcie)
Bezwzględne$A$1NIE zmienia się
Mieszane$A1 lub A$1Jedna współrzędna stała
Kiedy $? Gdy kopiujesz formułę i chcesz by odwołanie NIE przesuwało się. Np. dzielenie przez łączną sumę: =B2/$B$101

Wykresy

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)
⚠️ Pułapka maturalna: Przy kopiowaniu formuł sprawdź, czy adresy powinny być bezwzględne ($). Częsty błąd: WYSZUKAJ.PIONOWO z tabelą bez $ — przy kopiowaniu tabela się „rozjeżdża".

📝 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

OperatorPrzykład
=, !=, <, >, <=, >=WHERE cena > 100
AND, OR, NOTWHERE cena > 100 AND kategoria = 'odzież'
BETWEENWHERE wiek BETWEEN 18 AND 25
INWHERE miasto IN ('Kraków', 'Warszawa')
LIKEWHERE nazwisko LIKE 'Kow%'
IS NULLWHERE 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 vs HAVING:
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;
⚠️ Na maturze na papierze (Część 1): Pisz SQL czytelnie. Każda klauzula w nowej linii. Nie zapomnij o średniku na końcu! Aliasy (AS) ułatwiają czytanie.

📝 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ń.

Do wklejenia na czat Meet — zadania 8A.1-8C.12 i dwa domowe
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:

ŚrodowiskoCo 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.
Plik jest napisany tak, aby działał w obu silnikach Bez 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;
pacjentlekarzdatastatus
LewandowskaAdamczyk2026-05-04nieobecnosc
ChmielFabisiak2026-05-04odwolana
ZajacAdamczyk2026-05-05odbyta
DziedzicCichocka2026-05-06odbyta
EjsmondGawlik2026-05-08odbyta
SadowskaCichocka2026-05-08odbyta

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;
miesiacilesuma
2026-01192598,00
2026-02182413,00
2026-03233209,00
2026-04151948,00
2026-05192567,00
2026-06263344,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;
lekarzwszystkieodwołaneprocent
Hajduk20315,0
Gawlik15213,3
Fabisiak15213,3
Borowiec10110,0
Cichocka1417,1
Dudek1616,3
Adamczyk1915,3
Ejsmont1100,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)

Adres IPv4 to 32 bity zapisane jako 4 oktety oddzielone kropkami.
Przykład: 192.168.1.100 = 11000000.10101000.00000001.01100100

Klasy adresów IP

KlasaZakresMaska domyślnaPrzeznaczenie
A1.0.0.0 – 126.x.x.x255.0.0.0 (/8)Duże sieci
B128.0.0.0 – 191.255.x.x255.255.0.0 (/16)Średnie sieci
C192.0.0.0 – 223.255.255.x255.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

CIDRMaskaHostówAdresów
/24255.255.255.0254256
/25255.255.255.128126128
/26255.255.255.1926264
/27255.255.255.2243032
/28255.255.255.2401416
/30255.255.255.25224
Wzór: Ilość hostów = 2^(32 - maska) - 2. Odejmujemy 2 bo: adres sieci + broadcast.

Protokoły

ProtokółPortOpis
HTTP80Strony WWW (nieszyfrowane)
HTTPS443Strony WWW (szyfrowane TLS)
FTP21Transfer plików
SSH22Zdalny terminal (szyfrowany)
SMTP25Wysyłanie e-maili
DNS53Tłumaczenie domen na IP
DHCP67/68Automatyczne 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ł.

Do wklejenia na czat Meet — zadania 9.1-9.5
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.

WersjaBityBajtyLiczba adresówZapis
IPv4324232 ≈ 4,3 mld192.168.1.1 — cztery liczby 0–255
IPv61281621282001: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ółPortTransportDo czego
HTTP80TCPstrony WWW bez szyfrowania
HTTPS443TCPstrony WWW szyfrowane (TLS)
FTP21 sterowanie, 20 daneTCPprzesyłanie plików
DNS53UDP przy zapytaniach, TCP przy transferze strefyzamiana nazwy domeny na adres IP
SSH22TCPzdalny 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

JednostkaWartość
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

Wzór: Na zakodowanie N różnych wartości potrzebujemy ⌈log₂(N)⌉ bitów.

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

KodowanieBajty/znakZakres
ASCII1 (7 bitów)128 znaków (0-127): litery ang., cyfry, znaki specjalne
ASCII rozszerzony1 (8 bitów)256 znaków — dodaje znaki narodowe (np. Windows-1250)
UTF-81-4Cały Unicode; ASCII-compatible (znaki ang. = 1 bajt)
UTF-162-4Cał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)
⚠️ Na maturze: Pamiętaj o przeliczaniu jednostek! 1 KB = 1024 B, nie 1000. Częsty błąd: zapomnienie o /8 przy przeliczaniu bitów na bajty.

📝 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.

Do wklejenia na czat Meet — zadania 10.1-10.5
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
LiteraCzęstośćKodDługośćBity razem
A450145
C12100336
B13101339
D16111348
F51100420
E91101436

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%.

Dlaczego dekodowanie jest jednoznaczne Żaden kod nie jest początkiem innego (własność prefiksowa) — A to 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ź.

przypomnienie
Co oznacza zapis T[i] mod 2 w pseudokodzie?
Operator 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.
koncepcja
Czym różni się pętla "dopóki" od pętli "dla i od 1 do n"?
dopóki (while) — sprawdza warunek przed każdą iteracją, sami zarządzamy iteratorem (i ← i + 1). dla (for) — iterator jest automatyczny. W dopóki łatwo o pętlę nieskończoną!
kod
Jak w pseudokodzie CKE zapisujemy przypisanie wartości do zmiennej?
Strzałka w lewo: wynik ← 0 lub i ← i + 1. NIE używamy = do przypisania! = w pseudokodzie to porównanie (równość).
koncepcja
Co to jest złożoność czasowa O(n)?
Złożoność O(n) oznacza, że czas wykonania rośnie liniowo z rozmiarem danych. Jeśli mamy n elementów, algorytm wykona ~n operacji. Przykład: jednokrotne przejście przez tablicę.
matura
Jak prawidłowo śledzić zmienne w algorytmie krok po kroku?
Narysuj tabelkę śledzenia z kolumnami dla każdej zmiennej (i, wynik, T[i]) i wierszami dla każdej iteracji. Wypełniaj po kolei, NIE pomijaj kroków. To jedyny sposób na 100% pewność!

#01 — Analiza algorytmu (pseudokod)

Typ zadania: Część 1 — arkusz papierowy · Czas: ~30 min · Pseudokod CKE

Materiał własny MoreTutor · nie z arkusza CKEZadanie w formacie maturalnym, na innych danych niż tegoroczny arkusz — te same umiejętności, świeży materiał.

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 wynik po 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

0/6 kroków
0
Rozpoznanie algorytmu
▼

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

Co tu sie dzieje: algorytm idzie po kolei przez tablice T i do zmiennej wynik dodaje tylko te liczby, ktore sa nieparzyste. Czyli liczy sume liczb nieparzystych.
Slowniczek:
• mod = reszta z dzielenia (w Pythonie to %)
• T[i] = i-ty element tablicy
• wynik <- 0 = przypisanie: wstaw 0 do zmiennej wynik
1
Tabelka sledzenia
▼

„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."

Jak czytac tabelke: w kazdym wierszu sprawdzasz jedna liczbe z tablicy. Jesli reszta z dzielenia przez 2 to 1, dodajesz ja do 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)
Sprawdzenie na palcach: 7+9 = 16, 16+1 = 17, 17+5 = 22, 22+3 = 25. Zgadza sie.
2
Odpowiedz b) - opis algorytmu
▼

„Opisz jednym zdaniem, co robi algorytm. Nie pisz jak to robi - tylko co zwraca."

Jak pisac opis na maturze: nazwij dokladnie wynik (suma, liczba, najwieksza wartosc), nazwij wejscie (tablica T o n elementach) i podaj warunek (nieparzyste, podzielne przez 3, dodatnie itp.).
Odp: Algorytm oblicza sume wszystkich elementow nieparzystych
w tablicy T o n elementach.
Czesty blad: „sumuje liczby z tablicy" - to za malo. Trzeba napisac, ze sumuje tylko NIEPARZYSTE. Bez tego CKE nie da punktow.
3
Odpowiedz c) - modyfikacja
▼

„Co trzeba zmienic, zeby algorytm sumowal parzyste zamiast nieparzystych?"

Pomysl: liczba parzysta to taka, ktora dzieli sie przez 2 bez reszty. Czyli 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
4
Odpowiedz d) - zlozonosc
▼

„Ile razy wykona sie petla, jesli tablica ma n elementow?"

Odp: dokladnie n razy - po jednym przebiegu na kazdy element.

Jak myslec o zlozonosci: patrzysz, ile razy petla sie wykonuje w zaleznosci od rozmiaru danych (n). Jedna petla idaca po tablicy = 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.
5
Odpowiedz e) - Python
▼

„Przepisujemy pseudokod 1:1 na Pythona. Bez sztuczek - zwykla petla for, if i zmienna wynik. Tak jak na kartce."

Jedna roznica: w pseudokodzie tablica jest indeksowana od 1 do n, a w Pythonie od 0 do n-1. Najprosciej iterowac po elementach 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
Wersja blizsza pseudokodowi (z indeksem 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
Wersja z plikiem (typowa matura - dane sa w 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)
Co warto zapamietac:
• % 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 ekran

Cwiczenia 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.

Schemat pracy: przeczytaj pseudokod → narysuj tabelke → wpisuj kolejne wartosci linia po linii → ostatnia wartosc wynik to odpowiedz.
Zadanie 1 (rozgrzewka): Dla T = [3, 1, 4, 2, 5], n = 5 przesledz ponizszy pseudokod i podaj wartosc 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:

krokiT[i]wynik po dodaniu
1133
2214
3348
44210
55515

wynik = 15. Po piatym kroku i staje sie 6, warunek i <= 5 juz nie jest spelniony, petla konczy prace.

Zadanie 2: Dla T = [4, 7, 2, 9, 1, 5, 8, 3], n = 8 przesledz pseudokod i podaj 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.

Zadanie 3: Dla T = [4, 7, 2, 9, 1, 5, 8, 3], n = 8 przesledz pseudokod. Co liczy ten algorytm? Podaj 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):

iT[i]T[i] > 4?wynik
27tak1
49tak2
65tak3
78tak4
Zadanie 4 (modyfikacja): Wez bazowy algorytm z lekcji (suma nieparzystych) i zmodyfikuj go tak, zeby zwracal ilosc liczb nieparzystych, a nie ich sume. Napisz pseudokod i sprawdz na T = [4, 7, 2, 9, 1, 5, 8, 3].
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.

Zadanie 5: Dla T = [3, 8, 2, 9, 4, 1, 6, 7], n = 8 przesledz pseudokod. Co liczy? Podaj 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):

krokiT[i]T[i] > wynik?wynik po kroku
start———3 (= T[1])
1288 > 3, TAK8
2322 > 8, NIE8
3499 > 8, TAK9
4544 > 9, NIE9
5611 > 9, NIE9
6766 > 9, NIE9
7877 > 9, NIE9

Pogrubione wiersze to te, w ktorych wynik sie zmienia — reszta to porownania bez podmiany. wynik = 9.

Zadanie 6 (wyzwanie): Przesledz pseudokod dla T = [2, 5, 3, 8, 1, 4], n = 6. Co liczy? Jaki ma 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:

iT[i]T[i+1]T[i] < T[i+1]?wynik
125tak1
253nie1
338tak2
481nie2
514tak3

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.

Wzorce ktore wlasnie poznales — pojawiaja sie na maturze niemal w kazdym roku:
  • 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
Trzy schematy, ktore pokrywaja 80% pseudokodow z arkuszy CKE.

Wskazowki dla prowadzacego

Cel lekcji: uczen ma samodzielnie przesledzic pseudokod w tabelce i przepisac go na Pythona z prosta petla. Nie chodzi o optymalny kod — chodzi o to zeby uczen rozumial co robi kazda linijka. Jezeli na koncu lekcji uczen umie palcem pokazac „tutaj sprawdzamy nieparzystosc, tutaj dodajemy do wyniku" — lekcja zaliczona.

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.

  1. „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.
  2. „Ile wynosi 7 mod 2? A 8 mod 2?" — sprawdza znajomosc operatora modulo. Musi wiedziec ze nieparzyste daja 1, parzyste daja 0.
  3. „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.
  4. „Czym rozni sie = od == w Pythonie?" — kluczowe przed przepisaniem na kod. Pierwsze przypisuje, drugie porownuje.
  5. „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

Moment 1 — „pseudokod to przepis, nie magia". Zachodzi gdy uczen po raz pierwszy widzi tabelke sledzenia wypelniona do konca i sam mowi „aha, to po prostu chodzi po liscie i dodaje". Wykorzystaj: powiedz wprost ze kazdy algorytm, nawet najtrudniejszy z matury, mozna rozlozyc na tabelke z kolumnami „i", „warunek", „wynik". To uniwersalny klucz do calej maturalnej czesci algorytmicznej.
Moment 2 — „pseudokod i Python to ta sama logika, inny zapis". Zachodzi gdy uczen przeklada linijka po linijce 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.
Czego nie robic: nie wprowadzaj 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ń.

Czas: 10:00

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ź.

przypomnienie
Jak otworzyć plik tekstowy w Pythonie z polskimi znakami?
with open("plik.txt", "r", encoding="utf-8") as f: — użyj encoding="utf-8" żeby polskie znaki się wyświetlały poprawnie!
kod
Jak podzielić linię "2024-01-01;-5.2;0.0" na osobne wartości?
parts = linia.strip().split(";") — najpierw strip() usuwa \n, potem split(";") dzieli po średniku. Wynik: ["2024-01-01", "-5.2", "0.0"]
koncepcja
Czym różni się 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!
matura
Jak znaleźć maksimum i minimum z listy liczb?
max(lista) i min(lista). Dla obiektów używaj key: max(dane, key=lambda x: x["temp"]). Pamiętaj: zwraca element, nie indeks!
kod
Jak wyciągnąć miesiąc z daty "2024-01-15"?
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

Materiał własny MoreTutor · nie z arkusza CKEZadanie w formacie maturalnym, na innych danych niż tegoroczny arkusz — te same umiejętności, świeży materiał.

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

0/8 kroków
0
Wczytanie danych z pliku
▼
Co robimy: czytamy plik linia po linii. Każdą linię dzielimy średnikiem na 4 kawałki (data, temperatura, opady, ciśnienie). Wartości wrzucamy do czterech równoległych list — pozycja 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])
Dlaczego 4 listy a nie słownik: nie musisz pamiętać żadnej nowej składni. Indeks 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])
Pułapka numer jeden przy czterech listach naraz — kolejność w 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.

Trzy drobiazgi w tej samej funkcji:
  • if linia != "": przed split() — pusty wiersz na końcu pliku albo przypadkowy enter w środku daje IndexError: list index out of range przy parts[0].
  • int(parts[3]) dla ciśnienia zamiast float — ciśnienie w pliku jest całkowite, float zadziała, ale w wynikach zobaczysz 1013.0 zamiast 1013.
  • Nazwy funkcji piszemy z małej litery, z podkreśleniami: wczytaj_dane, nie WczytajDane. WielkaLitera jest w Pythonie zarezerwowana dla klas — egzaminator za to punktu nie odbierze, ale w kodzie od razu widać, co jest klasą, a co funkcją.
1
a) Min i max temperatura
▼
Co robimy: zakładamy, że pierwszy dzień jest jednocześnie najzimniejszy i najcieplejszy. Potem przechodzimy pętlą — gdy znajdziemy zimniejszy/cieplejszy dzień, podmieniamy wartość i zapamiętujemy datę.
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")
Pułapka: NIE zaczynaj od 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.
2
b) Średnia temperatura per miesiąc
▼
Nowa struktura — słownik (dict): do tej pory używaliśmy list, gdzie do wartości dostajemy się przez numer pozycji (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.
Co robimy: miesiąc wyciągamy z daty "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()
Dlaczego dwa słowniki, a nie lista list: słownik z licznikiem i słownik z sumą to najprostsza wersja jaką da się napisać na maturze. 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.
3
c) Dni z opadami
▼
Co robimy: zwykły licznik. Pętla przechodzi po wszystkich dniach, gdy opady są większe od 0 — zwiększamy licznik o 1. Procent: licznik podzielony przez liczbę wszystkich dni razy 100.
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) + "%")
4
d) Najdłuższa seria bez opadów
▼
Co robimy: trzymamy bieżącą długość serii (ile dni z rzędu bez opadów) i maksymalną długość. Gdy opady są 0 — bieżąca rośnie. Gdy spadnie deszcz — porównujemy bieżącą z maksymalną i resetujemy do 0. Zapamiętujemy też indeks początku serii, żeby wiedzieć daty od/do.
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)
Pułapka maturalna: jeżeli ostatnia seria w danych jest najdłuższa, pętla nigdy nie wejdzie do else — porównanie musi być POWTÓRZONE po pętli. To najczęstszy błąd w tym typie zadania.
Wariant alternatywny (równoważny): można też aktualizować 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.
5
e) Ciśnienie > 1015 i temp < 0
▼
Co robimy: drugi licznik, tym razem z dwoma warunkami połączonymi 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))
6
f) Zapis wyników do pliku
▼
Co robimy: otwieramy plik w trybie "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")
Sprawdzenie: po uruchomieniu otwórz wyniki.txt w notatniku. Powinny być 4 podpunkty (a, c, d, e). Średnie miesięczne (b) są w osobnym pliku wyniki_b.txt.
7
Rozszerzenie INF.04 — te same wyniki w oknie PyQt6 (form.ui)
▼
Po co GUI w zadaniu maturalnym:

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 konsolowaWersja z oknem
print(tekst)self.listaWynikow.addItem(tekst)
program liczy i kończy sięprogram czeka na kliknięcie użytkownika
wynik leci od razufunkcja 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:

PlikCo w nim jestCzy dotykasz
pyproject.tomlmetadane projektu dla Qt Creatoranie
form.uiwygląd okna — układasz go myszką w Designerzetak, w Designerze
mainwindow.pylogika zadania i podpięcie przyciskówtak, tu piszesz
Dwie drogi z 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żetobjectNameDo czego służy
QLabeletykietaStatusnapis „Wczytano N…"
QPushButtonprzyciskWczytajuruchamia obliczenia
QListWidgetlistaWynikowpokazuje wyniki zamiast print()
QPushButtonprzyciskZapiszzapisuje plik wynikowy

Na koniec zaznacz okno i wybierz Lay out Vertically, żeby widżety układały się jeden pod drugim przy zmianie rozmiaru.

To jest 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>
„Edytor nie podpowiada mi nazwy przycisku po 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.loadUipyuic6 + ui_form.py
dostęp do widżetuself.przyciskWczytajself.ui.przyciskWczytaj
podpowiadanie w edytorzenie majest
po zmianie formularzanic nie robiszkompilujesz ponownie
plików w projekciebez zmiandochodzi 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())
Cztery pułapki, na których wywraca się ten szablon:
  1. uic.loadUi() musi być pierwsze. Przed nim żaden widżet z formularza jeszcze nie istnieje — odwołanie do self.listaWynikow linijkę wyżej to AttributeError. Kolejność w __init__ jest zawsze ta sama: super(), loadUi(), dopiero potem własne pola i connect.
  2. 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 — dostaniesz SyntaxError: invalid syntax i czerwony znacznik na tej linii.
  3. 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.
  4. Ścieżka do form.ui. Samo 'form.ui' działa tylko wtedy, gdy terminal stoi w folderze projektu. Uruchomienie z innego katalogu daje FileNotFoundError. Dlatego w kodzie jest Path(__file__).parent — to zawsze folder, w którym leży mainwindow.py, niezależnie od tego, skąd odpalasz.
Dwie rzeczy, które zawsze sprawdza egzaminator INF.04: 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.
Oczekiwany wynik — po kliknięciu Wczytaj na liście pojawia się 17 pozycji, dokładnie te same liczby co w wersji konsolowej z kroków 1–5:
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: 24
Jak to sprawdzić: wrzuć dane_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.

Setup: zanim zaczniesz, stworz plik 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.
Zadanie 1: Otworz plik 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.

Zadanie 2: W pliku 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.

Zadanie 3: Plik 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.

Zadanie 4: Wczytaj plik 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.

Zadanie 5: Plik 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 >.

Zadanie 6: Wykorzystaj listy z zadania 5. Znajdz najdluzsza serie kolejnych dni, w ktorych temperatura byla dodatnia (> 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.

Po wykonaniu calego zestawu: opanowales caly schemat z lekcji — wczytanie kilku list rownoleglych, znajdowanie min/max z zapamietaniem indeksu, liczenie sredniej oraz pilnowanie serii. To dokladnie te same techniki, ktorych uzyjesz w zadaniach maturalnych z pliku .txt.

Wskazowki dla prowadzacego

Zalozenie poziomu: uczen zna petle for, if, listy, .append(), open(). Nie zna jeszcze: slownikow zaawansowanych, list comprehensions, lambdy, f-stringow ze skomplikowana logika. Caly cwiczenie ma uczyc myslenia w listach rownoleglych — to fundament zanim przejdzie do slownikow w nastepnej lekcji.

Pytania diagnostyczne na start (5 min)

Zadaj te pytania zanim otworzysz edytor. Jezeli uczen nie umie odpowiedziec na pierwsze trzy, cofnij sie do podstaw list i petli zanim ruszysz dalej.
  1. "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.
  2. "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.
  3. "Po split mam liste 4 stringow. Jak zamienic '-3.2' na liczbe?" — float(). Czesty blad: uzycie int() na temperaturze.
  4. "Mam 4 rownolegle listy. Co to znaczy ze sa rownolegle?" — ten sam indeks i odnosi sie do tego samego dnia we wszystkich listach. To kluczowe pojecie.
  5. "Jak znalezc najmniejsza wartosc w liscie liczb, ale BEZ uzycia funkcji min()?" — petla z if 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!"

Moment 1 — indeks jako klucz laczacy listy. Gdy uczen pierwszy raz pisze 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.
Pulapka prowadzacego: nie pokazuj 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".
Po lekcji: uczen powinien umiec samodzielnie wczytac dowolny plik CSV/srednikowy do list rownoleglych i policzyc min/max/srednia/zliczanie warunkowe. To fundament pod cwiczenie me03 (slowniki) i me04 (sortowanie + top-N).

📝 Sprawdzian — Programowanie Python

Sprawdź swoją wiedzę z przetwarzania plików w Pythonie. Masz 10 minut na 5 pytań.

Czas: 10:00

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ź.

przypomnienie
Jaka jest różnica między adresem A1 a $A$1?
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.
kod
Jak zliczyć komórki spełniające warunek np. większe od 50?
=LICZ.JEŻELI(B2:B100;">50") — warunek w cudzysłowie! Dla wielu warunków: =LICZ.WARUNKI(...).
koncepcja
Do czego służy WYSZUKAJ.PIONOWO (VLOOKUP)?
Szuka wartości w PIERWSZEJ kolumnie tabeli i zwraca wartość z innej kolumny tego samego wiersza. Schemat: =WYSZUKAJ.PIONOWO(co;gdzie;która_kolumna;0). 0 = dokładne dopasowanie!
matura
Jak importować dane z pliku .txt do Excela?
Otwórz plik w Excelu → Kreator importu → wybierz separator (średnik, tabulator) → sprawdź podgląd kolumn. Pamiętaj o typach danych (liczby vs tekst)!
kod
Jaka formuła sumuje tylko gdy spełniony jest warunek?
=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

Materiał własny MoreTutor · nie z arkusza CKEZadanie w formacie maturalnym, na innych danych niż tegoroczny arkusz — te same umiejętności, świeży materiał.

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

0/6 kroków
0
Import danych
▼
Co robimy: Otwieramy plik z ocenami w Excelu. Dane sa oddzielone srednikami, wiec Excel musi wiedziec, gdzie konczy sie jedna kolumna i zaczyna druga.

"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
Tip: Wstaw nowy wiersz 1 z naglowkami (Imie, Nazwisko, Klasa, Mat, Inf, Ang). Po wstawieniu dane Anny zjada do wiersza 2, Jana do wiersza 3, ostatni uczen do wiersza 201. Cala dalsza lekcja zaklada wlasnie taki uklad: naglowki w wierszu 1, dane w wierszach 2-201. Jak wstawic wiersz: prawy klik na numerze wiersza 1 → Wstaw.
1
a) Suma i srednia
▼
Co robimy: Dla kazdego ucznia liczymy sume 3 ocen (kolumna G) i srednia (kolumna H). Wpisujemy formule dla pierwszego ucznia i przeciagamy w dol. Adresy wzgledne (bez dolarow) — chcemy zeby przy kopiowaniu w dol D2:F2 zmienialo sie na D3:F3, D4:F4 itd.
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
Tip: Aby zaokraglic srednia do 2 miejsc po przecinku: zaznacz H2:H201 → prawy klik → Format komorek → Liczba → Miejsca dziesietne: 2.
2
b) LICZ.WARUNKI
▼
Co robimy: Liczymy uczniow spelniajacych DWA warunki naraz: klasa = 4A oraz wynik z informatyki >= 50. Do tego sluzy LICZ.WARUNKI (liczbe mnoga poznasz po nazwie — "warunki").
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.
Uwaga: Warunek liczbowy MUSI byc w cudzyslowie: ">=50", nie samo >=50. Bez cudzyslowu Excel zwroci blad.
3
c) MAX + WYSZUKAJ.PIONOWO
▼
Co robimy: Najpierw znajdujemy najwyzszy wynik z matematyki przez MAX. Potem szukamy, kto go ma. Robimy mala tabelke pomocnicza, w ktorej matematyka jest w PIERWSZEJ kolumnie — bo WYSZUKAJ.PIONOWO zawsze szuka w pierwszej kolumnie tabeli.
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)
Jak czytac WYSZUKAJ.PIONOWO: =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).
Wazne ograniczenie: jezeli dwoch uczniow ma identyczny najwyzszy wynik z matematyki, WYSZUKAJ.PIONOWO zwroci tylko pierwszego od gory w tabelce K2:M201 — drugi nie pojawi sie w wyniku. Na maturze zwykle dane sa tak zbudowane, ze MAX jest unikalny, ale warto wiedziec.
Alternatywa "na piechote": Wpisz =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.

4
d) SREDNIA.JEZELI
▼
Co robimy: Liczymy srednia z informatyki tylko dla uczniow spelniajacych warunek (klasa = 4A albo 4B). SREDNIA.JEZELI bierze srednia tylko z tych wierszy, gdzie warunek pasuje.
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
5
e) Wykres kolumnowy
▼
Co robimy: Excel nie zrobi wykresu z surowych 200 wierszy. Najpierw budujemy malutka tabelke 2×3 (2 klasy × 3 przedmioty) ze srednimi, a potem z niej rysujemy wykres.
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
Co bedzie na osi X (uwaga): Excel sam decyduje co idzie na os X — przy zaznaczeniu P1:S3 zwykle wstawi klasy (4A, 4B) na osi X, a slupki pogrupuje po przedmiotach (legenda: Matematyka / Informatyka / Angielski). Jezeli chcesz odwrotnie (przedmioty na osi X, klasy w legendzie), kliknij wykres → karta Projekt wykresu → przycisk Przelacz wiersz/kolumne. Opisz osie zgodnie z tym, co faktycznie pokazuje wykres: os pozioma = "Klasa" lub "Przedmiot" (zaleznie od ukladu), os pionowa = "Srednia ocena".
Wymagania na maturze: wykres MUSI miec tytul, opisy obu osi i legende. Brak ktoregokolwiek = punkty w plecy.

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.

Zadanie 1: W kolumnie A masz oceny z polskiego dla 30 uczniow (komorki A2:A31). W komorce C2 policz srednia, w C3 najwyzsza, w C4 najnizsza ocene.
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 ...".

Zadanie 2: W kolumnie B (B2:B31) sa oceny koncowe (1-6). Policz w komorce D2, ilu uczniow dostalo ocene 5. W D3 — ilu dostalo ocene 1 (niedostateczna).
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).

Zadanie 3: Sklep ma arkusz: kolumna A = nazwa produktu, kolumna B = kategoria ("napoje", "chleb", "owoce"), kolumna C = cena (50 wierszy danych od wiersza 2). W komorce E2 zsumuj cene wszystkich produktow z kategorii "napoje".
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.

Zadanie 4: Ten sam arkusz sklepu (50 produktow). W F2 policz srednia cene dla kategorii "chleb". W F3 policz, ile produktow ma cene wieksza niz 10 zl.
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.

Zadanie 5: Masz mala tabelke z kodami produktow i ich cenami: A2:A6 = kody (P001, P002, P003, P004, P005), B2:B6 = ceny (12, 25, 8, 40, 15). W komorce D2 wpisz kod "P003" i w komorce E2 zwroc cene tego produktu uzywajac WYSZUKAJ.PIONOWO.
Pokaz odpowiedz
D2:  P003
E2:  =WYSZUKAJ.PIONOWO(D2; A2:B6; 2; FALSZ)

Cztery argumenty po kolei:

  1. D2 — czego szukamy (kod P003)
  2. A2:B6 — tabela do przeszukania (pierwsza kolumna A musi zawierac to czego szukamy)
  3. 2 — numer kolumny w tabeli z ktorej chcemy wynik (B to druga kolumna)
  4. 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.

Zadanie 6: Arkusz pracownikow: A = imie, B = nazwisko, C = pensja (20 wierszy od wiersza 2). W kolumnie D wpisz dla kazdego pracownika tekst "wysoka" jezeli pensja jest wyzsza niz 5000, w przeciwnym razie "standardowa". Formule w D2 napisz tak, zeby dalo sie ja skopiowac w dol do D21.
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".

Wskazowka koncowa: jezeli formula zwraca #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

Cel lekcji: uczen po 45 min ma samodzielnie zbudowac arkusz z SUMA, SREDNIA, LICZ.WARUNKI, SREDNIA.JEZELI i WYSZUKAJ.PIONOWO oraz wykres kolumnowy. Nie wchodzimy w INDEKS+PODAJ.POZYCJE — to material na pozniej.

Pytania diagnostyczne (pierwsze 5 min)

  1. Czym rozni sie =SUMA(D2:D200) od =D2+D3+...+D200? Sprawdza czy uczen rozumie sens zakresu i po co w ogole istnieja funkcje.
  2. Co zwroci =SREDNIA(D2:F2)? Sprawdza czy uczen czyta zakres poziomo vs pionowo.
  3. Wyobraz sobie, ze w komorce H2 masz =SREDNIA(D2:F2)/$I$1 i 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.
  4. Czym rozni sie LICZ.JEZELI od LICZ.WARUNKI? Sprawdza czy uczen wie kiedy potrzebny jest wiecej niz jeden warunek.
  5. Co zwraca WYSZUKAJ.PIONOWO gdy nie znajdzie wartosci? Sprawdza czy uczen rozumie blad #N/D! i kolejnosc argumentow.
Uwaga merytoryczna: w glownej tresci lekcji (kroki 1, 3c) nie uzywamy adresowania bezwzglednego — formuly w wierszu 2 sa wzgledne i celowo przesuwaja sie przy przeciaganiu w dol. Dolary $ 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
Moment aha! #1 — adresowanie bezwzgledne. Glowna lekcja celowo nie wprowadza dolarow (formuly G2/H2 maja byc wzgledne, zeby przesuwaly sie przy przeciaganiu). Zrob osobny boczny przyklad na 2 minuty: w komorce J1 wpisz 100, w K2 wpisz =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.
Moment aha! #2 — WYSZUKAJ.PIONOWO szuka tylko w pierwszej kolumnie. Wiekszosc uczniow probuje szukac matematyki w kolumnie D i zwracac imie z A — i sie wscieka ze nie dziala. Gdy pokazesz ze wystarczy skopiowac D do nowej kolumny K i odwrocic logike (szukaj w K, zwroc z L/M) — uczen lapie ze funkcje maja swoje ograniczenia, ktore obchodzi sie ukladem danych, nie wymyslaniem bardziej skomplikowanej formuly.
Uwaga: nie wchodz w INDEKS+PODAJ.POZYCJE nawet jesli ktorys uczen zapyta. Powiedz wprost: "to jest lepsze rozwiazanie, ale poznamy je na nastepnej lekcji — dzis cementujemy WYSZUKAJ.PIONOWO". Mieszanie poziomow w 45 minut zabija grupe srodkowa.

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ń.

Czas: 10:00

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ź.

przypomnienie
Jaka jest kolejność klauzul w zapytaniu SELECT?
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT. Pamiętaj: WHERE przed GROUP BY, HAVING po GROUP BY!
kod
Czym różni się WHERE od HAVING?
WHERE filtruje wiersze PRZED grupowaniem. HAVING filtruje grupy PO grupowaniu. Z HAVING używasz funkcji agregujących (COUNT, SUM, AVG).
koncepcja
Co robi JOIN i jakie są jego typy?
INNER JOIN — tylko pasujące wiersze z obu tabel. LEFT JOIN — wszystko z lewej + pasujące z prawej. Na maturze najczęściej INNER JOIN.
matura
Jak posortować wyniki malejąco po kolumnie "cena"?
ORDER BY cena DESC. Domyślnie jest ASC (rosnąco). Możesz sortować po wielu kolumnach: ORDER BY kategoria ASC, cena DESC.
kod
Jak zliczyć liczbę rekordów w grupach?
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

Materiał własny MoreTutor · nie z arkusza CKEZadanie w formacie maturalnym, na innych danych niż tegoroczny arkusz — te same umiejętności, świeży materiał.

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

0/7 kroków
0
Analiza schematu bazy
▼
Zanim napiszesz cokolwiek, rozrysuj jak tabele sie lacza. Klucz obcy 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
Regula laczenia: zawsze lacz po kolumnach id z jednej tabeli i cos_id z drugiej.
1
a) Tytuly + autorzy (JOIN)
▼
Chcemy zobaczyc tytul ksiazki razem z imieniem i nazwiskiem autora. Dane sa w dwoch tabelach, wiec uzywamy 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;
Co robi ktora linia:
- SELECT wybiera 3 kolumny do wyswietlenia
- FROM ksiazki tabela glowna
- JOIN autorzy ON ... dolaczamy autorow po kluczu
- ORDER BY nazwisko sortujemy alfabetycznie po nazwisku
2
b) Ile ksiazek per autor (GROUP BY + HAVING)
▼
Liczymy ile ksiazek napisal kazdy autor i pokazujemy tylko tych, ktorzy maja minimum 3. Grupujemy po autorze i filtrujemy grupy przez HAVING.
-- 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;
Pamietaj: 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.
Dlaczego tylko 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.
3
c) Nigdy nie wypozyczone (NOT IN)
▼
Najpierw wewnetrzne zapytanie zwraca liste 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
);
Jak to czytac: "wez tytuly z tabeli ksiazki, gdzie id NIE nalezy do listy wypozyczonych ksiazek". Podzapytanie w nawiasach wykonuje sie pierwsze, potem dopiero zewnetrzne SELECT.
Wazne zalozenie: przyjmujemy ze kolumna 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.
4
d) Kryminaly wypozyczone w 2024
▼
Laczymy 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%';
Dlaczego LIKE '2024%'? Daty sa w formacie YYYY-MM-DD, wiec kazda data z 2024 roku zaczyna sie od "2024". Znak % oznacza "cokolwiek dalej".
5
e) Najwyzszy rating (podzapytanie)
▼
Najpierw w podzapytaniu znajdujemy maksymalny rating. Potem wybieramy ksiazke (albo ksiazki), ktorej rating jest rowny temu maksimum.
-- 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);
Dlaczego nie 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.
Wzorzec "top 1" gdy remisy nie sa wazne: jesli zalezy nam tylko na jednym wyniku i nie obchodza nas remisy, mozemy uzyc krotszej formy ORDER BY kolumna DESC LIMIT 1. To wzorzec do zapamietania, przyda sie w cwiczeniach.
6
f) Raport per gatunek (dwa zapytania)
▼
Zadanie wymaga trzech informacji na gatunek: liczba ksiazek, sredni rating, liczba wypozyczen. Zamiast jednego skomplikowanego zapytania rozbijemy to na dwa proste, kazde robi swoja rzecz.

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;
Co robi ktora funkcja:
- 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)
Dlaczego dwa zapytania zamiast jednego? Polaczenie tego w jeden SELECT z trzema agregatami i LEFT JOIN dalo by wynik, ale liczba ksiazek byla by sztucznie zawyzona (kazda ksiazka byla by liczona tyle razy, ile ma wypozyczen). Obejscia (typu COUNT z DISTINCT) komplikuja kod ponad to, co potrzebne na poziomie podstaw. Dwa proste zapytania sa czytelniejsze i pewniejsze.

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.

Zadanie 1: Wypisz wszystkich autorow z Polski (kolumny: imie, nazwisko).
Pokaz odpowiedz
-- filtr po kraju
SELECT imie, nazwisko
FROM autorzy
WHERE kraj = 'Polska';
Zadanie 2: Pokaz wszystkie ksiazki z gatunku "fantasy", posortowane od najwyzszego ratingu.
Pokaz odpowiedz
-- WHERE + ORDER BY malejaco
SELECT tytul, rating
FROM ksiazki
WHERE gatunek = 'fantasy'
ORDER BY rating DESC;
Zadanie 3: Policz ile ksiazek jest w kazdym gatunku.
Pokaz odpowiedz
-- grupowanie po gatunku, liczenie wierszy
SELECT gatunek, COUNT(*) AS ile
FROM ksiazki
GROUP BY gatunek;
Zadanie 4: Wypisz tytuly ksiazek wraz z krajem autora (JOIN dwoch tabel).
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;
Zadanie 5: Znajdz autorow, ktorych ksiazki maja sredni rating powyzej 4.0 (kolumny: nazwisko, sredni rating).
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;
Zadanie 6: Wypisz tytul ksiazki, ktora byla wypozyczana najczesciej (jeden wynik, najwyzsza liczba wypozyczen).
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;
Wskazowka: jesli zapytanie zwraca pusty wynik, sprawdz po kolei: czy nazwa kolumny jest dokladnie taka jak w tabeli (case-sensitive w wartosciach tekstowych), czy JOIN laczy po prawidlowych id, czy WHERE nie odfiltrowal wszystkiego. Zacznij od SELECT * FROM tabela LIMIT 5; zeby zobaczyc dane.

Wskazowki dla prowadzacego

Cel lekcji w jednym zdaniu: uczen ma rozumiec roznice miedzy WHERE (filtruje wiersze) a HAVING (filtruje grupy) oraz wiedziec kiedy potrzebny jest JOIN, a kiedy podzapytanie.

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.

  1. "Co zwroci SELECT * FROM ksiazki WHERE rating > 4?" sprawdza czy rozumie filtrowanie wierszy.
  2. "Po co jest klucz obcy autor_id w tabeli ksiazki?" sprawdza model relacyjny. Jesli nie umie odpowiedziec, narysuj dwie tabele na kartce i pociagnij strzalke.
  3. "Jaka jest roznica miedzy COUNT(*) a COUNT(autor_id)?" wykrywa luke z NULL-ami i agregacjami.
  4. "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".
  5. "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.
Moment "aha!" #1, WHERE vs HAVING. Najlepiej dziala kiedy pokazesz to samo zapytanie dwa razy: raz z 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.
Moment "aha!" #2, podzapytanie jako wartosc. Pokaz najpierw 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.
Pulapka czasowa: nie tlumacz na tej lekcji CREATE VIEW, CTE (WITH), window functions ani CASE. Te tematy psuja flow i odciagaja od fundamentu. Jesli uczen pyta, odpowiedz "to na nastepnej lekcji, dzis trzymamy sie SELECT-a".
Sygnal opanowania tematu: uczen sam z siebie potrafi przerobic zadanie f (raport per gatunek) na "raport per autor" bez patrzenia w notatki. Jesli to potrafi, material zaliczony, mozesz przejsc do nastepnego cwiczenia.

📝 Sprawdzian — Bazy danych SQL

Sprawdź swoją wiedzę z SQL. Masz 10 minut na 5 pytań.

Czas: 10:00

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ź.

przypomnienie
Jak dostać się do pojedynczej cyfry napisu 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].
kod
Jak policzyć sumę cyfr napisu "907"?
Pętla po znakach: suma = 0, potem for i in range(len(kod)): suma = suma + int(kod[i]). Wynik: 9 + 0 + 7 = 16.
koncepcja
Co robi operator % (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.
matura
Jak sprawdzić, czy napis czyta się tak samo od przodu i od tyłu?
Porównujesz znak 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.
kod
Jak policzyć, ile razy każdy element powtarza się na liście — bez importowania bibliotek?
Słownik jako licznik: 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

Materiał własny MoreTutor · nie z arkusza CKEZadanie w formacie maturalnym, na zupełnie innych danych niż tegoroczny arkusz — te same umiejętności, świeży materiał.

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

0/9 kroków
0
Wczytanie kodów z pliku
▼
Co robimy: czytamy plik wiersz po wierszu. Każdy wiersz to jeden kod — zostawiamy go jako napis, nie zamieniamy na liczbę. Gdybyśmy zrobili 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])
Wynik: Wczytano 500 kodow, pierwszy kod to 5881399869875. Jeśli masz inną liczbę — plik wygenerował się inaczej, uruchom generator jeszcze raz w pustym folderze.
1
a) Ile kodów ma poprawną długość
▼
Co robimy: dwa liczniki i jedna pętla. 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))
Odpowiedź a): 476 kodów ma 13 cyfr, 24 kody mają inną długość (12 albo 14 cyfr).
2
b) Suma cyfr i podzielność przez 7
▼
Co robimy: pętla w pętli. Zewnętrzna idzie po kodach, wewnętrzna po cyfrach jednego kodu. Przy okazji zapamiętujemy rekordzistę — kod o największej sumie 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])      # 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 + ")")
Odpowiedź b): 74 kody mają sumę cyfr podzielną przez 7. Rekordzista to 7849996179788 — suma cyfr 92.
Pułapka: 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ą.
3
c) Cyfra kontrolna EAN-13 — serce zadania
▼
Co robimy: piszemy własną funkcję, która z 12 pierwszych cyfr liczy cyfrę kontrolną. Waga zależy od pozycji: 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))
Odpowiedź c): 364 kody mają poprawną cyfrę kontrolną, 112 błędną. Kontrola: 364 + 112 = 476, czyli dokładnie tyle, ile kodów 13-cyfrowych z podpunktu a).
Dlaczego zewnętrzne % 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.
Sprawdź się na kartce: weź kod 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.
4
d) Palindromy
▼
Co robimy: dla każdego kodu porównujemy pierwszy znak z ostatnim, drugi z przedostatnim i tak dalej. Wystarczy jedna niezgodna para, żeby przekreślić cały kod — dlatego zaczynamy od 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])
Odpowiedź d): 4 palindromy — 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.
Optymalizacja, o którą zapyta egzaminator: pętla robi dwa razy więcej porównań niż trzeba — para (0, 12) jest sprawdzana ponownie jako (12, 0). Wystarczy range(len(kod) // 2). Przy 500 kodach różnicy w czasie nie zobaczysz, ale warto o tym powiedzieć przy omawianiu.
5
e) Duplikaty — słownik jako licznik
▼
Co robimy: klasyczny słownik-licznik. Kluczem jest kod, wartością liczba wystąpień. Pierwsze spotkanie kodu zakłada licznik na 0, każde kolejne dokłada 1. Na końcu wybieramy tylko te klucze, których licznik przekroczył 1.
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)))
Odpowiedź e): powtarzają się 2 kody — 4321445653382 (3 razy) i 5199559392988 (2 razy). Unikalnych kodów jest 497 przy 500 wierszach w pliku.
Skąd wiadomo, że 497 się zgadza: jeden nadmiarowy wiersz z pary plus dwa nadmiarowe z trójki to 3 wiersze ponad stan. 500 − 3 = 497. Taka kontrola „na palcach" wyłapuje błąd w liczeniu szybciej niż czytanie kodu linia po linii.
6
f) Zapis raportu do pliku
▼
Co robimy: jeden 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")
Tryb "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.
7
Gotowy kod — cały program w jednym pliku
▼
Do przepisania na czysto: wszystkie podpunkty złożone w jeden program. Zapisz jako 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")
Oczekiwany wynik na konsoli — jeśli masz inne liczby, porównuj krok po kroku, gdzieś jest błąd:
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
8
Rozszerzenie INF.04 — ten sam program w oknie PyQt6 (form.ui)
▼
Po co GUI w zadaniu maturalnym:

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 konsolowaWersja z oknem
print(tekst)self.listaWynikow.addItem(tekst)
program liczy i kończy sięprogram czeka na kliknięcie użytkownika
wynik leci od razufunkcja 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:

PlikCo w nim jestCzy dotykasz
pyproject.tomlmetadane projektu dla Qt Creatoranie
form.uiwygląd okna — układasz go myszką w Designerzetak, w Designerze
mainwindow.pylogika zadania i podpięcie przyciskówtak, tu piszesz
Dwie drogi z 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żetobjectNameDo czego służy
QLabeletykietaStatusnapis „Wczytano N…"
QPushButtonprzyciskWczytajuruchamia obliczenia
QListWidgetlistaWynikowpokazuje wyniki zamiast print()
QPushButtonprzyciskZapiszzapisuje plik wynikowy

Na koniec zaznacz okno i wybierz Lay out Vertically, żeby widżety układały się jeden pod drugim przy zmianie rozmiaru.

To jest 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>
„Edytor nie podpowiada mi nazwy przycisku po 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.loadUipyuic6 + ui_form.py
dostęp do widżetuself.przyciskWczytajself.ui.przyciskWczytaj
podpowiadanie w edytorzenie majest
po zmianie formularzanic nie robiszkompilujesz ponownie
plików w projekciebez zmiandochodzi 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())
Cztery pułapki, na których wywraca się ten szablon:
  1. uic.loadUi() musi być pierwsze. Przed nim żaden widżet z formularza jeszcze nie istnieje — odwołanie do self.listaWynikow linijkę wyżej to AttributeError. Kolejność w __init__ jest zawsze ta sama: super(), loadUi(), dopiero potem własne pola i connect.
  2. 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 — dostaniesz SyntaxError: invalid syntax i czerwony znacznik na tej linii.
  3. 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.
  4. Ścieżka do form.ui. Samo 'form.ui' działa tylko wtedy, gdy terminal stoi w folderze projektu. Uruchomienie z innego katalogu daje FileNotFoundError. Dlatego w kodzie jest Path(__file__).parent — to zawsze folder, w którym leży mainwindow.py, niezależnie od tego, skąd odpalasz.
Dwie rzeczy, które zawsze sprawdza egzaminator INF.04: 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.
Oczekiwany wynik — 12 pozycji na liście, te same liczby co w wersji konsolowej: 476/24, 74, suma 92, 364/112, 4 palindromy, 2 duplikaty. Klik Zapisz tworzy raport.txt identyczny jak w kroku 6.
Jak to sprawdzić: wrzuć 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

PodpunktMechanizmGdzie jeszcze wraca na maturze
a)len() na napisie + licznik w pętliwalidacja 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, moduloPESEL, NIP, ISBN — CKE lubi cyfry kontrolne
d)indeks „od końca" len - 1 - i, flaga logicznapalindromy, 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"
Trzy błędy, które kosztują tu najwięcej punktów: (1) zamiana kodu na 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ń.

Czas: 10:00

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.

Do wklejenia na czat Meet — sprawdzian - 10 pytań
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

12345678910
cbdabcadbc

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.

Sprawdzian — 27.09 10:00
1
Ile wynosi 1011012 w systemie dziesiętnym?
2
Jaki zakres liczb całkowitych obejmuje 8-bitowy kod U2?
3
Ile bitów ma adres IPv6?
4
Ile adresów hostów daje podsieć z maską /26?
5
Wyrażenie NOT (A OR B) jest równoważne:
6
Które zdanie o kompresji RLE jest prawdziwe?
7
Nieskompresowany obraz 640 × 480 pikseli, 24 bity na piksel, zajmuje:
8
Która klauzula filtruje wyniki po grupowaniu?
9
Chcesz wypisać pacjentów, którzy nie mają ani jednej wizyty. Która konstrukcja to zrobi?
10
Data trzymana jest jako tekst '2026-04-17'. Które wyrażenie wyciągnie z niej '2026-04'?
Po sprawdzianie Każde pytanie, w którym uczeń się pomylił, wraca jako pierwsze zadanie następnej sesji — wpisz numer pytania do notatek. Pytania 1, 2, 7 liczą się na kartce w kilkanaście sekund; błąd w nich oznacza brak automatyzmu, nie brak wiedzy.

📝 Ś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!

❌ 1. Zły kierunek konwersji systemów liczbowych
❌ 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
❌ 2. Indeksowanie od 0 vs od 1
❌ 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)
❌ 3. div i mod vs / i %
# 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)
❌ 4. Zapomnienie strip() przy czytaniu pliku
# ❌ 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
❌ 5. SQL: WHERE zamiast HAVING z GROUP BY
-- ❌ 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
❌ 6. WYSZUKAJ.PIONOWO bez $ w tabeli
❌ 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
❌ 7. Zapominanie o ostatniej serii
# 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
❌ 8. Brak encoding="utf-8" przy polskich znakach
# ❌ 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!
❌ 9. SQL: brak DISTINCT w COUNT
-- ❌ 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;
❌ 10. Błędne przeliczanie jednostek
❌ 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)