📝 Notatki o uczniu

Uczeń: Maks

Technik programista · 4 rok · Przygotowanie do INF.04

Dodaj notatkę

Historia notatek

Dashboard

Narzędzie korepetytora — egzaminy, ćwiczenia, pełne prowadzenie lekcji.

Przedmioty

⚛️ React — INF-04

4 ćwiczenia · 12 modułów teorii · Egzamin zawodowy

🐍 Python — INF-04

5 ć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 sty 2025 · filter/map · Bootstrap switch · downloads++

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

INF-04 sty 2025 · 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

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

#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 — Następny

Wkrótce

📖 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 elementuNOWE
.find() — wyszukanie po idNOWE
.sort() — sortowanie tablicyNOWE
[...new Set()] — unikalne wartościNOWE
new Date().toISOString() — bieżąca dataNOWE
warunkowy tekst/styl przyciskuNOWE
select z dynamicznymi opcjamiNOWE

🔧 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)
  • handleSubmitaxios.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ę ReactDane zostają — to MySQL!
Dodaj wiersz w phpMyAdminPo refresh Reacta nowa świnka pojawia się w UI
Wpisz w URL /xyzStrona 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 — Galeria zdjęć

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

przypomnienie
Czym jest JSX i czym różni się od zwykłego HTML-a?
JSX to rozszerzenie składni JavaScript, które pozwala pisać kod wyglądający jak HTML wewnątrz plików .jsx. Główne różnice: używamy className zamiast class, wszystkie tagi muszą być zamknięte, a w nawiasach klamrowych {} możemy wstawiać wyrażenia JavaScript.
koncepcja
Co musi zwracać każdy komponent React?
Każdy komponent musi zwracać dokładnie jeden element JSX (lub null). Jeśli chcemy zwrócić kilka elementów obok siebie, opakowujemy je w <div> lub pusty fragment <>...</>.
kod
Co robi metoda .map() na tablicy i jak używamy jej w JSX?
Metoda .map() tworzy nową tablicę, wywołując podaną funkcję na każdym elemencie. W JSX używamy jej do renderowania listy elementów, np. {zdjecia.map(z => <img src={z.url} key={z.id} />)}. Każdy element listy potrzebuje unikalnego atrybutu key.
koncepcja
Czym są propsy i jak przekazujemy je do komponentu?
Propsy (properties) to dane przekazywane z komponentu rodzica do dziecka — działają jak atrybuty HTML. Przekazujemy je przy wywołaniu komponentu: <Karta tytul="Foto" />, a odbieramy jako obiekt w parametrze funkcji: function Karta({ tytul }).
kod
Jak zaimportować komponent z innego pliku?
Jeśli komponent jest eksportowany domyślnie (export default Karta), importujemy go przez import Karta from './Karta'. Przy eksporcie nazwanym (export function Karta) używamy nawiasów klamrowych: import { Karta } from './Karta'.

#01 — Instrukcja z arkusza egzaminacyjnego

INF.04-01-25.01-SG · Styczeń 2025 · Czas: 180 min · Część II: Aplikacja webowa

Kontekst

Z zastosowaniem biblioteki React.js wykonaj aplikację front-end realizującą funkcję kategoryzacji zdjęć w galerii.

Archiwum materiałów: pliki3.zip, hasło: K@tegorie)

Założenia aplikacji

  • Aplikacja z jednego komponentu
  • Dane z dane.txt: obiekty {id, alt, filename, category, downloads}
  • Obrazy w folderze assets
  • category: 1 = kwiaty, 2 = zwierzęta, 3 = samochody

Komponent zawiera

  • Nagłówek h1: „Kategorie zdjęć"
  • 3 pola switch (domyślnie ON): Kwiaty, Zwierzęta, Samochody
  • Bloki zdjęć obok siebie: img + h4 (pobrań) + button „Pobierz"

Stylowanie

  • Zdjęcia: marginesy 5px, border-radius
  • Switch + button: Bootstrap (Tabela 1)

Logika

  • Switche filtrują kategorie
  • Kliknięcie „Pobierz" → downloads + 1
  • Pętle + warunki, znaczące nazwy zmiennych

Tabela 1 — Bootstrap

<div class="form-check form-switch">
  <input class="form-check-input" type="checkbox" id="x">
  <label class="form-check-label" for="x">Label</label>
</div>
<!-- Inline: dodaj .form-check-inline -->
<button class="btn btn-primary">Primary</button>

Na lekcji: pokaż uczniowi arkusz, przeczytajcie razem, niech opowie własnymi słowami co trzeba zrobić.

#01 — Wprowadzenie dla nauczyciela

1. Otwarcie

„To zadanie z prawdziwego egzaminu INF-04 ze stycznia 2025. Budujemy galerię zdjęć z kategoryzacją. Masz gotowe zdjęcia i dane — my piszemy kod."

„Na egzaminie masz 180 minut na 3 części. Web to ~60-80 min. My to zrobimy w 25 minut."

2. Diagnoza wiedzy

„Co pamiętasz z Reacta? Cokolwiek."

Co mówi uczeńTwoja reakcja
„Komponenty, state"„Super — dokładnie tego potrzebujemy."
„Coś z JSX"„Dobrze. Za chwilę wrócisz do formy."
„Nic nie pamiętam"„Spoko, normalne po przerwie. Krok po kroku."
(cisza)„Ok, za 25 min będziesz miał gotową galerię."

3. Architektura

App:
  state: galeria    → tablica 12 obiektów
  state: kwiaty     → true/false
  state: zwierzeta  → true/false
  state: samochody  → true/false
  computed: widoczne = galeria.filter(...)
  JSX: h1 + 3× switch + flex-wrap { widoczne.map(...) }

„Ile useState będziemy potrzebować?"

Odpowiedź: 4 — galeria + kwiaty + zwierzeta + samochody.

4. Zasady prowadzenia

Zasada #1: Uczeń pisze, Ty obserwujesz. Podpowiadaj pytaniami.
Zasada #2: Niech zobaczy błąd. „Jak myślisz, to zadziała?"
Zasada #3: Czytajcie błędy razem. DevTools → Console.

5. Częste blokady

ProblemPodpowiedź
Zapomina import bootstrap„Na górze — co musisz zaimportować?"
Obrazy 404„Czy pliki są w public/assets/?"
Checkbox nie reaguje„Controlled input: checked + onChange. Masz oba?"
onClick={f()}„Z nawiasami = od razu. Bez = po kliknięciu."
z.downloads++„Mutacja! React tego nie zauważy. Nowy obiekt: spread."

6. Po zbudowaniu

„Dlaczego w pobierz() używamy .map() a nie galeria[id].downloads++?"

Odp: React porównuje referencje. Mutacja nie triggeruje re-render.

„Co by się stało gdybyś wyłączył wszystkie 3 switche?"

Odp: Pusta tablica → .map() nic nie zwraca → pusta strona.

#01 — Wymagania — checklist

WymaganieDetaleStatus
Jeden komponentApp.jsx
Nagłówek h1„Kategorie zdjęć"
3 pola switchKwiaty / Zwierzęta / Samochody, domyślnie ON
Dane z dane.txt12 obiektów
Obrazy w assetspublic/assets/
Bloki obok siebied-flex flex-wrap
Filtrowanieswitch → kategoria
Pobierz → +1downloads w tablicy
CSS zdjęciamargin 5px + border-radius
Bootstrap switchform-check form-switch
Bootstrap buttonbtn btn-primary
Pętle + warunki.map() + .filter()
Znaczące nazwygaleria, kwiaty, widoczne, pobierz

#01 — Budowa krok po kroku

0/8 kroków
0
Utwórz projekt Vite

„Użyjemy Vite — czyta się 'wit'. Tworzy za nas cały projekt React."

npm create vite@latest egzamin -- --template react
cd egzamin
npm install
npm install bootstrap

„Dlaczego folder 'egzamin'?"

Odp: Nazwa z arkusza.

1
Skopiuj obrazy do public/assets/
egzamin/public/assets/obraz1.jpg … obraz12.jpg

„Dlaczego public/assets/ a nie src/assets/?"

Odp: Vite serwuje public/ statycznie. Prostsze ścieżki.

2
Szkielet App.jsx

„Zaczynamy od minimum — import, pusta funkcja, nagłówek."

import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
import './App.css'

function App() {
  return (
    <div className="container mt-3">
      <h1>Kategorie zdjęć</h1>
    </div>
  )
}
export default App
3
Dane — tablica zdjęć

„Bierzemy dane z dane.txt. Na egzaminie kopiujesz 1:1."

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 },
]

Wewnątrz App():

const [galeria, setGaleria] = useState(zdjecia)

„Dlaczego useState(zdjecia) a nie const galeria = zdjecia?"

Odp: Bo kliknięcie Pobierz zmieni dane. React musi wiedzieć.

4
3 przełączniki (Bootstrap form-switch)
const [kwiaty, setKwiaty] = useState(true)
const [zwierzeta, setZwierzeta] = useState(true)
const [samochody, setSamochody] = useState(true)
<div className="mb-4">
  <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>

„Dlaczego htmlFor a nie for?"

Odp: for to słowo kluczowe JS.

Jeśli zapomni form-check-inline:

„Switche pod sobą? Sprawdź Tabelę 1 — co dodać do form-check?"

5
Filtrowanie (.filter)

„Jaka metoda tablicowa zwraca nową tablicę z elementami spełniającymi warunek?"

Odp: .filter()

const widoczne = galeria.filter((z) => {
  if (z.category === 1) return kwiaty
  if (z.category === 2) return zwierzeta
  if (z.category === 3) return samochody
})

Jeśli pisze === true:

„kwiaty to już boolean. return kwiaty wystarczy."

6
Wyświetlanie bloków (.map) + CSS
<div className="d-flex flex-wrap">
  {widoczne.map((z) => (
    <div key={z.id} className="zdjecie-blok">
      <img src={`/assets/${z.filename}`} alt={z.alt} />
      <h4>Pobrań: {z.downloads}</h4>
      <button className="btn btn-primary btn-sm">Pobierz</button>
    </div>
  ))}
</div>

App.css:

.zdjecie-blok { text-align: center; margin: 10px; }
.zdjecie-blok img { margin: 5px; border-radius: 10px; }

„Co to jest key={z.id}?"

Odp: React odróżnia elementy listy. Bez key → warning.

7
Przycisk Pobierz — aktualizacja stanu

„Najtrudniejszy krok. Kliknięcie Pobierz musi zwiększyć downloads o 1 — ale nie mutując!"

function pobierz(id) {
  setGaleria(galeria.map((z) =>
    z.id === id ? { ...z, downloads: z.downloads + 1 } : z
  ))
}
<button className="btn btn-primary btn-sm"
  onClick={() => pobierz(z.id)}>Pobierz</button>

„Dlaczego () => pobierz(z.id) a nie pobierz(z.id)?"

Odp: Z nawiasami = od razu. Arrow = po kliknięciu.

Jeśli pisze z.downloads++:

„Mutacja! React porównuje referencje. Musisz stworzyć NOWY obiekt."

Gotowe! Switche filtrują, Pobierz zwiększa licznik.

#01 — Gotowy kod

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 [galeria, setGaleria] = useState(zdjecia)
  const [kwiaty, setKwiaty] = useState(true)
  const [zwierzeta, setZwierzeta] = useState(true)
  const [samochody, setSamochody] = useState(true)

  const widoczne = galeria.filter((z) => {
    if (z.category === 1) return kwiaty
    if (z.category === 2) return zwierzeta
    if (z.category === 3) return samochody
  })

  function pobierz(id) {
    setGaleria(galeria.map((z) =>
      z.id === id ? { ...z, downloads: z.downloads + 1 } : z
    ))
  }

  return (
    <div className="container mt-3">
      <h1>Kategorie zdjęć</h1>
      <div className="mb-4">
        <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>
      <div className="d-flex flex-wrap">
        {widoczne.map((z) => (
          <div key={z.id} className="zdjecie-blok">
            <img src={`/assets/${z.filename}`} alt={z.alt} />
            <h4>Pobrań: {z.downloads}</h4>
            <button className="btn btn-primary btn-sm" onClick={() => pobierz(z.id)}>Pobierz</button>
          </div>
        ))}
      </div>
    </div>
  )
}
export default App

App.css

.zdjecie-blok { text-align: center; margin: 10px; }
.zdjecie-blok img { margin: 5px; border-radius: 10px; }

📝 Sprawdzian — Galeria zdjęć

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

Sprawdzian z lekcji 05:00
1
Jak poprawnie nadać klasę CSS elementowi w JSX?
2
Do czego służy prop key przy renderowaniu listy elementów metodą .map()?
3
Jak poprawnie przekazać props tytul do komponentu Karta?
4
Która reguła nazewnictwa komponentów React jest obowiązkowa?
5
Jak wygląda domyślny eksport komponentu w pliku Galeria.jsx?

🔥 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 CLInpm 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)

🔥 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 ConstraintLayoutConvert 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łainputType="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.txtWynikandroid: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"] = "[email protected]"    # 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.

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

INF.04-01-25.01-SG · Styczeń 2025 · Czas: 180 min · Część I: Aplikacja desktopowa

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 zadanie z tego samego egzaminu co React — INF-04 styczeń 2025, ale Część I: aplikacja desktopowa w Pythonie."

„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.666667f"{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.uipyuic6 mainwindow.ui -o mainwindow.pyfrom mainwindow import Ui_MainWindow
zaloguj.uipyuic6 zaloguj.ui -o zaloguj.pyfrom zaloguj import Ui_Zaloguj
formularz.uipyuic6 formularz.ui -o formularz.pyfrom formularz import Ui_Formularz
wyswietl.uipyuic6 wyswietl.ui -o wyswietl.pyfrom wyswietl import Ui_Wyswietl
dane.txtopen() → 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?

📝 Ś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)

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 % 10ostatnia 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.37 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 maksymalna liczba przełożonych (głębokość) = 22.

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.1Marcelino Kruk (8 transakcji)
  • 8.2689 kobiet, 651 mężczyzn bez zakupów
  • 8.328 różnych sklepów, łącznie 19 443,29 zł
  • 8.4sprzedawca 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

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.

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

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

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

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

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₁₆

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

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.
# Bubble Sort — O(n²)
def bubble_sort(T):
    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
Ś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.
# Selection Sort — O(n²)
def selection_sort(T):
    n = len(T)
    for i in range(n - 1):
        min_idx = i
        for j in range(i + 1, n):
            if T[j] < T[min_idx]:
                min_idx = j
        T[i], T[min_idx] = T[min_idx], T[i]
    return T
Ś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.
# Insertion Sort — O(n²)
def insertion_sort(T):
    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

Wyszukiwanie liniowe

# O(n) — przegląda element po elemencie
def szukaj_liniowo(T, szukana):
    for i in range(len(T)):
        if T[i] == szukana:
            return i     # znaleziono na pozycji i
    return -1            # nie znaleziono

Wyszukiwanie binarne (Binary Search)

Wymaganie: Tablica MUSI być posortowana! Złożoność O(log n) — drastycznie szybsze od liniowego.
# Binary Search — O(log n)
def szukaj_binarnie(T, szukana):
    lewy = 0
    prawy = len(T) - 1
    while lewy <= prawy:
        srodek = (lewy + prawy) // 2
        if T[srodek] == szukana:
            return srodek
        elif T[srodek] < szukana:
            lewy = srodek + 1
        else:
            prawy = srodek - 1
    return -1
Ś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.

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 pracujesz w Access lub Base.

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;

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 ✓

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

🔥 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

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 zaczniesz liczyć — przeczytaj kod. Co robi T[i] mod 2 = 1?"

„Kiedy reszta z dzielenia przez 2 wynosi 1?"

Odp: Gdy liczba jest nieparzysta.

Rozpoznanie: Algorytm przegląda tablicę i sumuje elementy spełniające warunek (nieparzyste). To suma elementów nieparzystych.
1
Tabelka śledzenia

„Narysuj tabelkę z kolumnami: krok, i, T[i], T[i] mod 2, warunek, wynik. Wypełnij wiersz po wierszu."

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: 25 (= 7 + 9 + 1 + 5 + 3)
2
Odpowiedź b) — opis algorytmu

„Opisz jednym zdaniem co robi algorytm."

Odp: Algorytm oblicza sumę wszystkich elementów nieparzystych
w tablicy T o n elementach.
Wskazówka maturalna: Opisuj PRECYZYJNIE. Nie „sumuje liczby" ale „sumuje elementy NIEPARZYSTE tablicy T". Punkty za dokładność!
3
Odpowiedź c) — modyfikacja

„Co trzeba zmienić, żeby sumować parzyste zamiast nieparzyste?"

Zmiana: w warunku zamień 1 na 0

BYŁO:    jeżeli T[i] mod 2 = 1 to:
JEST:    jeżeli T[i] mod 2 = 0 to:

Sprawdzenie: 4 + 2 + 8 = 14 ✓
4
Odpowiedź d) — złożoność

„Ile razy wykonuje się pętla?"

Odp: n razy — raz dla każdego elementu.

Złożoność: O(n)

Uzasadnienie: Pętla wykonuje się dokładnie n razy
(i idzie od 1 do n). W każdej iteracji wykonujemy
stałą liczbę operacji (mod, porównanie, dodawanie).
Brak zagnieżdżonych pętli.

Złożoność liniowa — proporcjonalna do rozmiaru danych.
5
Odpowiedź e) — Python

„Przepisz pseudokod na Pythona. Uwaga: w Pythonie tablice indeksowane od 0!"

T = [4, 7, 2, 9, 1, 5, 8, 3]

# Wersja 1 — wierna pseudokodowi
wynik = 0
for i in range(len(T)):
    if T[i] % 2 == 1:
        wynik += T[i]
print(wynik)  # 25

# Wersja 2 — Pythonowa (krótsza)
wynik = sum(x for x in T if x % 2 == 1)
print(wynik)  # 25

# Wersja 3 — z plikiem (jak na maturze)
with open("dane.txt") as f:
    T = [int(l.strip()) for l in f]
wynik = sum(x for x in T if x % 2 == 1)
print(wynik)

📝 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

Kontekst

Plik dane_pogoda.txt zawiera dane pogodowe z 365 dni (po jednym wierszu na dzień). Format: data;temperatura;opady;cisnienie

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/7 kroków
0
Wczytanie danych z pliku

„Pierwszy krok ZAWSZE: wczytaj plik i sprawdź, czy dane się poprawnie wczytały."

dane = []
with open("dane_pogoda.txt", "r", encoding="utf-8") as f:
    for linia in f:
        parts = linia.strip().split(";")
        dane.append({
            "data": parts[0],
            "temp": float(parts[1]),
            "opady": float(parts[2]),
            "cisnienie": int(parts[3])
        })

print(f"Wczytano {len(dane)} rekordów")
print(dane[0])  # sprawdź pierwszy
print(dane[-1]) # sprawdź ostatni
1
a) Min i max temperatura

„Jak znaleźć dzień z najniższą temp? Masz listę słowników..."

# Metoda 1 — ręcznie (bardziej maturalna)
min_d = dane[0]
max_d = dane[0]
for d in dane:
    if d["temp"] < min_d["temp"]:
        min_d = d
    if d["temp"] > max_d["temp"]:
        max_d = d

print(f"Najzimniej: {min_d['data']} → {min_d['temp']}°C")
print(f"Najcieplej: {max_d['data']} → {max_d['temp']}°C")

# Metoda 2 — Pythonowa
min_d = min(dane, key=lambda d: d["temp"])
max_d = max(dane, key=lambda d: d["temp"])
2
b) Średnia temperatura per miesiąc

„Jak wyciągnąć miesiąc z daty '2024-01-15'? Podpowiedź: split() albo slicing."

# Grupowanie per miesiąc
miesiace = {}
for d in dane:
    m = d["data"][5:7]  # "01", "02", ...
    if m not in miesiace:
        miesiace[m] = []
    miesiace[m].append(d["temp"])

# Oblicz średnie i zapisz
with open("wyniki_b.txt", "w", encoding="utf-8") as f:
    for m in sorted(miesiace):
        temps = miesiace[m]
        sr = sum(temps) / len(temps)
        linia = f"Miesiąc {m}: średnia = {sr:.2f}°C"
        print(linia)
        f.write(linia + "\n")
3
c) Dni z opadami
z_opadami = [d for d in dane if d["opady"] > 0]
ile = len(z_opadami)
procent = ile / len(dane) * 100

print(f"Dni z opadami: {ile}")
print(f"Procent: {procent:.1f}%")
4
d) Najdłuższa seria bez opadów

„To klasyczne zadanie na serię. Potrzebujesz dwóch zmiennych: bieżąca długość i maksymalna długość."

max_seria = 0
max_start = 0
biezaca = 0
biezacy_start = 0

for i, d in enumerate(dane):
    if d["opady"] == 0:
        if biezaca == 0:
            biezacy_start = i
        biezaca += 1
    else:
        if biezaca > max_seria:
            max_seria = biezaca
            max_start = biezacy_start
        biezaca = 0

# Sprawdź ostatnią serię
if biezaca > max_seria:
    max_seria = biezaca
    max_start = biezacy_start

data_od = dane[max_start]["data"]
data_do = dane[max_start + max_seria - 1]["data"]
print(f"Najdłuższa seria bez opadów: {max_seria} dni")
print(f"Od {data_od} do {data_do}")
Pułapka: Nie zapomnij sprawdzić ostatnią serię PO pętli! Jeśli najdłuższa seria jest na końcu danych, pętla nie wejdzie w else.
5
e) Ciśnienie > 1015 i temp < 0
wynik_e = [d for d in dane
           if d["cisnienie"] > 1015 and d["temp"] < 0]

print(f"Dni z wysokim ciśnieniem i ujemną temp: {len(wynik_e)}")
6
f) Zapis wyników do pliku
with open("wyniki.txt", "w", encoding="utf-8") as f:
    # Podpunkt c
    f.write(f"c) Dni z opadami: {ile} ({procent:.1f}%)\n")

    # Podpunkt d
    f.write(f"d) Najdłuższa seria bez opadów: {max_seria} dni\n")
    f.write(f"   Od {data_od} do {data_do}\n")

    # Podpunkt e
    f.write(f"e) Ciśnienie>1015 i temp<0: {len(wynik_e)} dni\n")

print("Wyniki zapisane do wyniki.txt")

Wskazówka: Na maturze ZAWSZE sprawdź, czy pliki wynikowe się utworzyły. Otwórz je i sprawdź zawartość!

📝 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

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

„Otwórz Excel → Plik → Otwórz → wybierz .txt → Kreator importu → separator średnik → zakończ."

Po imporcie arkusz wygląda tak:
   A        B           C     D    E    F
1  Anna     Kowalska    4A    72   85   68
2  Jan      Nowak       4B    55   92   71
...

Kolumny: A=imię, B=nazwisko, C=klasa, D=mat, E=inf, F=ang
Tip: Dostosuj szerokości kolumn i dodaj nagłówki w wierszu 1 (przesuń dane do wiersza 2).
1
a) Suma i średnia
G1: Nagłówek "Suma"
G2: =D2+E2+F2         (lub =SUMA(D2:F2))
     → skopiuj w dół do G201

H1: Nagłówek "Średnia"
H2: =ŚREDNIA(D2:F2)
     → skopiuj w dół do H201

Formatowanie: Zaznacz H2:H201 → Format komórek → Liczba → 2 m.p.
2
b) LICZ.WARUNKI

„LICZ.WARUNKI pozwala podać WIELE warunków jednocześnie."

Formuła:
=LICZ.WARUNKI(C2:C201;"4A";E2:E201;">=50")

Rozkład:
  C2:C201;"4A"     → klasa = 4A
  E2:E201;">=50"   → informatyka >= 50 pkt

Wynik: liczba uczniów z 4A, którzy zdali informatykę.
Uwaga: Warunek liczbowy w CUDZYSŁOWIE: ">=50", nie bez cudzysłowu!
3
c) MAX + WYSZUKAJ.PIONOWO
Krok 1: Najwyższy wynik z matematyki
=MAX(D2:D201)

Krok 2: WYSZUKAJ.PIONOWO — szukaj po matematyce
Trzeba stworzyć tabelę pomocniczą z matematyką w 1. kolumnie!

Alternatywa — INDEKS + PODAJ.POZYCJĘ (lepsza):
Imię:  =INDEKS(A2:A201;PODAJ.POZYCJĘ(MAX(D2:D201);D2:D201;0))
Nazw:  =INDEKS(B2:B201;PODAJ.POZYCJĘ(MAX(D2:D201);D2:D201;0))

Lub WYSZUKAJ.PIONOWO z pomocniczą tabelą:
1. Skopiuj D:A:B do nowego arkusza (mat jako 1. kolumna)
2. Posortuj po mat
3. =WYSZUKAJ.PIONOWO(MAX(D2:D201);Arkusz2!A:C;2;0)

„Dlaczego WYSZUKAJ.PIONOWO wymaga szukanej wartości w pierwszej kolumnie tabeli?"

Odp: Bo szuka wartości w 1. kolumnie i zwraca wartość z n-tej kolumny tego samego wiersza.

4
d) ŚREDNIA.JEŻELI
Średnia informatyki dla 4A:
=ŚREDNIA.JEŻELI(C2:C201;"4A";E2:E201)

Średnia informatyki dla 4B:
=ŚREDNIA.JEŻELI(C2:C201;"4B";E2:E201)

Składnia:
=ŚREDNIA.JEŻELI(zakres_warunku; warunek; zakres_średniej)
5
e) Wykres kolumnowy

„Najpierw przygotuj dane źródłowe wykresu w tabelce pomocniczej."

Tabelka pomocnicza (np. w J:M):
           Matematyka    Informatyka    Angielski
4A         =ŚRED.JEŻ..   =ŚRED.JEŻ..   =ŚRED.JEŻ..
4B         =ŚRED.JEŻ..   =ŚRED.JEŻ..   =ŚRED.JEŻ..

Tworzenie wykresu:
1. Zaznacz tabelkę pomocniczą (z nagłówkami!)
2. Wstaw → Wykres → Kolumnowy grupowany
3. Dodaj: Tytuł wykresu, opisy osi, legendę
4. Formatuj: czytelne etykiety, kolory
Wymagania wykresu na maturze: Tytuł + opisy osi + legenda. Bez nich tracisz punkty!

📝 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

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 zapytanie — narysuj schemat relacji. Które kolumny łączą tabele?"

autorzy (id) ──1:N──> ksiazki (autor_id)
ksiazki (id) ──1:N──> wypozyczenia (ksiazka_id)

Relacje:
• Jeden autor → wiele książek
• Jedna książka → wiele wypożyczeń

„Jak połączyć autorów z książkami?"

Odp: JOIN autorzy ON ksiazki.autor_id = autorzy.id

1
a) Tytuły + autorzy (JOIN)
SELECT k.tytul, a.imie, a.nazwisko
FROM ksiazki k
INNER JOIN autorzy a ON k.autor_id = a.id
ORDER BY a.nazwisko ASC;

„Dlaczego INNER JOIN a nie LEFT JOIN?"

Odp: Bo chcemy tylko książki, które MAJĄ autora. LEFT JOIN pokazałby też książki z NULL autorem.

2
b) Ile książek per autor (GROUP BY + HAVING)
SELECT a.imie, a.nazwisko, COUNT(k.id) AS ile_ksiazek
FROM autorzy a
INNER JOIN ksiazki k ON a.id = k.autor_id
GROUP BY a.id, a.imie, a.nazwisko
HAVING COUNT(k.id) >= 3
ORDER BY ile_ksiazek DESC;
Kolejność: SELECT → FROM → JOIN → WHERE → GROUP BY → HAVING → ORDER BY.
WHERE filtruje WIERSZE, HAVING filtruje GRUPY!
3
c) Nigdy nie wypożyczone (podzapytanie / LEFT JOIN)

„Dwa podejścia: NOT IN z podzapytaniem lub LEFT JOIN + IS NULL."

-- Metoda 1: NOT IN (podzapytanie)
SELECT tytul
FROM ksiazki
WHERE id NOT IN (
    SELECT DISTINCT ksiazka_id
    FROM wypozyczenia
);

-- Metoda 2: LEFT JOIN + IS NULL
SELECT k.tytul
FROM ksiazki k
LEFT JOIN wypozyczenia w ON k.id = w.ksiazka_id
WHERE w.id IS NULL;

„Dlaczego LEFT JOIN a nie INNER JOIN?"

Odp: INNER JOIN odrzuciłby książki bez wypożyczeń. LEFT JOIN zachowuje je z NULL-ami.

4
d) Kryminały wypożyczone w 2024
SELECT COUNT(DISTINCT w.id) AS ile_wypozyczen
FROM wypozyczenia w
INNER JOIN ksiazki k ON w.ksiazka_id = k.id
WHERE k.gatunek = 'kryminał'
  AND w.data_wyp LIKE '2024%';

-- Alternatywnie:
-- AND w.data_wyp BETWEEN '2024-01-01' AND '2024-12-31'
LIKE '2024%' — szybki sposób na filtrowanie dat po roku. Działa bo format ISO: YYYY-MM-DD.
5
e) Najwyższy rating (podzapytanie)
SELECT k.tytul, a.imie, a.nazwisko, k.rating
FROM ksiazki k
INNER JOIN autorzy a ON k.autor_id = a.id
WHERE k.rating = (SELECT MAX(rating) FROM ksiazki);

-- Lub z ORDER BY + LIMIT (prostsze, ale nie zawsze poprawne):
SELECT k.tytul, a.imie, a.nazwisko, k.rating
FROM ksiazki k
INNER JOIN autorzy a ON k.autor_id = a.id
ORDER BY k.rating DESC
LIMIT 1;

„Czemu podzapytanie jest lepsze od LIMIT 1?"

Odp: Jeśli dwie książki mają ten sam najwyższy rating, LIMIT 1 pokaże tylko jedną. Podzapytanie pokaże obie.

6
f) CREATE VIEW
CREATE VIEW raport_gatunki AS
SELECT
    k.gatunek,
    COUNT(DISTINCT k.id) AS ile_ksiazek,
    ROUND(AVG(k.rating), 2) AS sredni_rating,
    COUNT(w.id) AS ile_wypozyczen
FROM ksiazki k
LEFT JOIN wypozyczenia w ON k.id = w.ksiazka_id
GROUP BY k.gatunek;

-- Użycie widoku:
SELECT * FROM raport_gatunki
ORDER BY ile_wypozyczen DESC;
VIEW to wirtualna tabela — nie przechowuje danych, tylko zapytanie. Odświeża się automatycznie. Na maturze często proszą o CREATE VIEW!

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

📝 Ś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)