📝 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++
#02 — Lista zadań (Todo)
Ćwiczenie · controlled input · dodawanie/usuwanie · toggle · select
#03 — Tracker wydatków
Ćwiczenie · .reduce() · number input · wyszukiwarka · Bootstrap card
#04 — Dziennik ocen
Ćwiczenie · edycja elementu · .sort() · .find() · dynamic select
Python — egzaminy / ćwiczenia
#01 — Rejestr produktów
INF-04 sty 2025 · open/split · SQLite · PyQt6 GUI
#02 — Dziennik ucznia
Oceny · średnia · filtr przedmiot · SQLite · PyQt6
#03 — Wypożyczalnia książek
Katalog · wypożyczenie · UPDATE · SQLite · PyQt6
#04 — Stacja pogodowa
Pomiary · min/max/avg · daty · SQLite · PyQt6
#05 — Logowanie z nawigacją menu
QStackedWidget · QMenu · navbar · open() · sprawdzanie danych · PyQt6 GUI
Matura — egzaminy / ćwiczenia
#01 — Analiza algorytmu
Pseudokod · śledzenie zmiennych · wynik algorytmu · złożoność
#02 — Programowanie Python
Plik tekstowy · przetwarzanie danych · statystyki · wynik do pliku
#03 — Arkusz kalkulacyjny
Formuły · JEŻELI · LICZ.JEŻELI · WYSZUKAJ.PIONOWO · wykresy
#04 — Bazy danych SQL
SELECT · JOIN · GROUP BY · HAVING · podzapytania · CREATE VIEW
🔒 #05 — Następny
Wkrótce
📖 Baza wiedzy — React
| Moduł | Temat | Kluczowe pojęcia |
|---|---|---|
| 1 | Co to React? | Biblioteka UI, Virtual DOM, komponenty |
| 2 | Środowisko | Node.js, Vite, npm, struktura projektu |
| 3 | JSX | className, klamry {}, ternary, Fragment |
| 4 | Komponenty | Funkcja → JSX, export/import, PascalCase |
| 5 | Props | Dane rodzic→dziecko, destrukturyzacja, read-only |
| 6 | State | useState, setX, re-render, controlled input |
| 7 | Eventy | onClick, onChange, onSubmit, e.preventDefault() |
| 8 | Tablice | .map(), key, .filter(), .find(), .sort(), spread |
| 9 | Warunkowy rendering | ternary, &&, wczesny return, klasy warunkowe |
| 10 | useEffect | Efekty uboczne, zależności, cleanup, fetch |
| 11 | Formularze | Controlled inputs, select, checkbox, walidacja |
| 12 | Struktura projektu | Import/eksport, foldery, komendy npm, spread |
📝 Ściąga React
16 kart ze wzorcami kodu
🐛 Typowe błędy
10 najczęstszych błędów
📖 Baza wiedzy — Python
| Moduł | Temat | Kluczowe pojęcia |
|---|---|---|
| 1 | Podstawy Python | Interpreter, typy, zmienne, input/print, f-string |
| 2 | Instrukcje warunkowe | if/elif/else, and/or/not, in, zagnieżdżone |
| 3 | Pętle | for, while, range(), break/continue, enumerate |
| 4 | Funkcje | def, return, parametry domyślne, lambda |
| 5 | Stringi | Metody, f-string, slicing, split/join |
| 6 | Listy | append/sort/pop, comprehension, slicing |
| 7 | Słowniki i krotki | dict, tuple, set, .items(), unpacking |
| 8 | Pliki | with open(), read/write, CSV, split(";") |
| 9 | PyQt6 | QApplication, QLabel, QLineEdit, QPushButton, QListWidget, QGridLayout |
| 10 | SQLite | connect, CREATE, INSERT, SELECT, ?, fetchall |
📝 Ściąga Python
16 kart ze wzorcami kodu
🐛 Typowe błędy
10 najczęstszych błędów Python
📖 Baza wiedzy — Matura z informatyki
| Moduł | Temat | Kluczowe pojęcia |
|---|---|---|
| 1 | Systemy liczbowe | BIN, OCT, DEC, HEX, konwersje, arytmetyka |
| 2 | Logika i algebra Boole'a | AND, OR, NOT, XOR, tablice prawdy, bramki |
| 3 | Algorytmy i pseudokod | Śledzenie, zmienne, warunek stopu, złożoność |
| 4 | Sortowanie i wyszukiwanie | Bubble, selection, insertion, binary search |
| 5 | Rekurencja | Silnia, Fibonacci, wieże Hanoi, drzewo wywołań |
| 6 | Python — pliki i dane | open(), split(), listy, przetwarzanie tekstowe |
| 7 | Arkusz kalkulacyjny | JEŻELI, LICZ.JEŻELI, SUMA.JEŻELI, WYSZUKAJ.PIONOWO |
| 8 | Bazy danych i SQL | SELECT, JOIN, GROUP BY, HAVING, podzapytania |
| 9 | Sieci komputerowe | Adresacja IP, maski, podsieci, DNS, protokoły |
| 10 | Teoria informacji | Kodowanie, ASCII, UTF-8, kompresja, entropia |
📝 Ściąga Matura
Kluczowe wzory i formuły maturalne
🐛 Typowe błędy
10 najczęstszych błędów na maturze
📊 Mapa umiejętności — co uczy każde zadanie
| Umiejętność | #01 Galeria | #02 Todo | #03 Wydatki | #04 Oceny |
|---|---|---|---|---|
| useState (boolean) | ✅ | ✅ | — | — |
| useState (tablica obiektów) | ✅ | ✅ | ✅ | ✅ |
| useState (string — controlled input) | — | ✅ | ✅ | ✅ |
| useState (number — input type number) | — | — | ✅ | — |
| .filter() | ✅ | ✅ | ✅ | ✅ |
| .filter() po tekście (.includes) | — | — | ✅ | — |
| .map() w JSX | ✅ | ✅ | ✅ | ✅ |
| .reduce() — suma / średnia | — | — | ✅ | ✅ |
| spread — zmiana pola obiektu | ✅ | ✅ | — | ✅ |
| spread — dodanie do tablicy | — | ✅ | ✅ | ✅ |
| usuwanie z tablicy (.filter by id) | — | ✅ | ✅ | ✅ |
| toggle boolean | — | ✅ | — | — |
| warunkowe klasy CSS | — | ✅ | ✅ | ✅ |
| warunkowe renderowanie (alert) | — | — | ✅ | ✅ |
| <select> + onChange | — | ✅ | ✅ | ✅ |
| input type="number" + parseFloat | — | — | ✅ | — |
| .toFixed(2) formatowanie | — | — | ✅ | ✅ |
| .toLowerCase() + .includes() | — | — | ✅ | — |
| Bootstrap switch | ✅ | — | — | — |
| Bootstrap table / badge | — | ✅ | ✅ | ✅ |
| Bootstrap card / alert | — | — | ✅ | ✅ |
| Date.now() jako ID | — | ✅ | ✅ | ✅ |
| edycja istniejącego elementu | — | — | — | ✅ NOWE |
| .find() — wyszukanie po id | — | — | — | ✅ NOWE |
| .sort() — sortowanie tablicy | — | — | — | ✅ NOWE |
| [...new Set()] — unikalne wartości | — | — | — | ✅ NOWE |
| new Date().toISOString() — bieżąca data | — | — | — | ✅ NOWE |
| warunkowy tekst/styl przycisku | — | — | — | ✅ NOWE |
| select z dynamicznymi opcjami | — | — | — | ✅ NOWE |
🔧 Reset i narzędzia
🗑 Resetuj postęp kroków
Odznacz wszystkie checkboxy kroków we wszystkich ćwiczeniach
🔄 Resetuj sprawdziany
Wyczyść wyniki i odpowiedzi we wszystkich testach
📕 Ukryj rozwiązania
Schowaj wszystkie odsłonięte rozwiązania zadań w modułach teorii
⚠️ Reset WSZYSTKIEGO
Kroki + sprawdziany + rozwiązania — pełny reset do stanu początkowego
1. Co to jest React?
Stary sposób vs React
<div id="licznik">0</div>
<button onclick="zwieksz()">Kliknij</button>
<script>
let wartosc = 0;
function zwieksz() {
wartosc++;
document.getElementById('licznik').textContent = wartosc;
}
</script>
import { useState } from 'react';
function Licznik() {
const [wartosc, setWartosc] = useState(0);
return (
<div>
<p>{wartosc}</p>
<button onClick={() => setWartosc(wartosc + 1)}>Kliknij</button>
</div>
);
}
Virtual DOM
Zmiana danych → React tworzy nowy Virtual DOM
→ Porównuje: stary vs nowy → Aktualizuje TYLKO zmienione elementy
Architektura komponentowa
Strona
├── Navbar (Logo + Menu)
├── Treść (Post × 3)
└── Footer
📌 Zadanie: Opisz słownie
Napisz własnymi słowami (2-3 zdania na każdy punkt):
1. Co robi Virtual DOM i dlaczego jest szybszy niż bezpośrednia manipulacja DOM?
2. Na czym polega architektura komponentowa — dlaczego dzielimy interfejs na małe kawałki?
1. Virtual DOM to lekka kopia prawdziwego DOM-u trzymana w pamięci.
Gdy stan się zmienia, React tworzy nową wersję Virtual DOM,
porównuje ją ze starą (diffing) i aktualizuje w prawdziwym DOM
tylko te elementy, które faktycznie się zmieniły.
2. Architektura komponentowa polega na dzieleniu interfejsu na
niezależne, wielokrotnego użytku kawałki (np. Nagłówek, Przycisk,
Karta). Każdy komponent odpowiada za jedną rzecz — to ułatwia
pisanie, testowanie i utrzymanie aplikacji.
2. Środowisko
Sprawdzenie Node.js
node --version # potrzebne v18+
npm --version
Tworzenie projektu Vite
cd ~/Desktop
npm create vite@latest egzamin -- --template react
cd egzamin
npm install
npm install bootstrap
mkdir -p public/assets
npm run dev
Struktura projektu
egzamin/
├── node_modules/ ← pakiety (NIE dotykamy)
├── public/assets/ ← obrazy galerii
├── src/
│ ├── App.css ← style
│ ├── App.jsx ← główny komponent
│ └── index.js ← punkt wejścia
├── index.html
└── package.json
Punkt wejścia
// src/index.js
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(<App />)
📌 Zadanie: Stwórz projekt z pamięci
Bez zaglądania do notatek wpisz z pamięci komendy, które:
1. Tworzą nowy projekt React za pomocą Vite (szablon React)
2. Wchodzą do folderu projektu
3. Instalują zależności
4. Uruchamiają serwer deweloperski
npm create vite@latest moj-projekt -- --template react
cd moj-projekt
npm install
npm run dev
3. JSX
JSX = JavaScript XML — składnia HTML-podobna wewnątrz JS.
Transformacja
const el = <h1 className="tytul">Cześć!</h1>;
// kompiluje się do:
const el = React.createElement("h1", { className: "tytul" }, "Cześć!");
5 zasad JSX
// ❌ return ( <h1>A</h1> <p>B</p> );
// ✅ return ( <> <h1>A</h1> <p>B</p> </> );
// ❌ <br> ✅ <br />class jest słowem kluczowym JS → w JSX: className.
| HTML | JSX |
|---|---|
| onclick | onClick |
| for | htmlFor |
<p style={{ color: 'red', fontSize: '16px' }}>Tekst</p>Wyrażenia w klamrach {}
const imie = 'Maks';
return (
<div>
<h1>Cześć, {imie}!</h1>
<p>Wynik: {2 + 2}</p>
{zalogowany ? 'Tak' : 'Nie'}
{show && <p>Widoczne!</p>}
</div>
);
📌 Zadanie: Napraw błędy JSX
Poniższy kod zawiera 3 błędy JSX. Znajdź je i napisz poprawioną wersję:
function Wizytowka() {
return (
<div>
<h1 class="tytul">Jan Kowalski</h1>
<img src="foto.jpg">
<label for="email">Email:</label>
</div>
);
}
function Wizytowka() {
return (
<div>
<h1 className="tytul">Jan Kowalski</h1>
<img src="foto.jpg" />
<label htmlFor="email">Email:</label>
</div>
);
}
// Błąd 1: class → className
// Błąd 2: <img> zamknięty → <img />
// Błąd 3: for → htmlFor
📌 Zadanie: Wyrażenia w JSX
Napisz komponent Powitanie, który:
— ma zmienną imie ze swoim imieniem
— ma zmienną godzina z aktualną godziną (new Date().getHours())
— wyświetla w JSX: „Cześć, {imię}! Jest godzina {godzina}."
function Powitanie() {
const imie = "Maks";
const godzina = new Date().getHours();
return <p>Cześć, {imie}! Jest godzina {godzina}.</p>;
}
4. Komponenty
Komponent = funkcja JS zwracająca JSX. Nazwa od wielkiej litery.
function Naglowek() {
return <header><h1>Moja Aplikacja</h1></header>;
}
function App() {
return <div><Naglowek /><p>Treść</p></div>;
}
Podział na pliki
// src/components/Naglowek.jsx
export default function Naglowek() {
return <header><h1>App</h1></header>;
}
// src/App.jsx
import Naglowek from './components/Naglowek';
📌 Zadanie: Komponent Karta
Napisz komponent Karta, który renderuje div z klasą "karta" zawierający:
— nagłówek <h2> z tytułem „Mój projekt"
— paragraf <p> z opisem „To jest opis mojego projektu."
— przycisk z tekstem „Szczegóły"
function Karta() {
return (
<div className="karta">
<h2>Mój projekt</h2>
<p>To jest opis mojego projektu.</p>
<button>Szczegóły</button>
</div>
);
}
export default Karta;
5. Props
Props = dane przekazywane do komponentu z zewnątrz.
<Karta nazwa="Laptop" cena={3999} />
function Karta({ nazwa, cena }) {
return <div><h3>{nazwa}</h3><p>{cena} zł</p></div>;
}
state.📌 Zadanie: Komponent z propsami
Stwórz komponent Produkt, który przyjmuje dwa propsy: nazwa i cena.
Następnie użyj go 3 razy w komponencie App z różnymi danymi.
function Produkt({ nazwa, cena }) {
return (
<div className="produkt">
<h3>{nazwa}</h3>
<p>Cena: {cena} zł</p>
</div>
);
}
function App() {
return (
<div>
<Produkt nazwa="Laptop" cena={3500} />
<Produkt nazwa="Myszka" cena={89} />
<Produkt nazwa="Klawiatura" cena={249} />
</div>
);
}
6. State (useState)
import { useState } from 'react';
const [wartosc, setWartosc] = useState(0);
// ↑ ↑ ↑
// aktualna setter start
Licznik
function Licznik() {
const [n, setN] = useState(0);
return (
<div>
<p>{n}</p>
<button onClick={() => setN(n + 1)}>+</button>
</div>
);
}
Controlled input
const [imie, setImie] = useState('');
<input value={imie} onChange={(e) => setImie(e.target.value)} />
Zasady Hooks
- Tylko na górze komponentu
- Konwencja:
[coś, setCoś] - Można mieć wiele useState
📌 Zadanie: Licznik z przyciskami
Zbuduj komponent Licznik, który:
— przechowuje liczbę w stanie (początkowa wartość 0)
— ma przycisk + zwiększający o 1
— ma przycisk − zmniejszający o 1
— ma przycisk Reset ustawiający na 0
— wyświetla aktualną wartość między przyciskami
import { useState } from "react";
function Licznik() {
const [liczba, setLiczba] = useState(0);
return (
<div>
<button onClick={() => setLiczba(liczba - 1)}>−</button>
<span> {liczba} </span>
<button onClick={() => setLiczba(liczba + 1)}>+</button>
<br />
<button onClick={() => setLiczba(0)}>Reset</button>
</div>
);
}
export default Licznik;
7. Eventy (onClick, onChange, onSubmit)
React obsługuje zdarzenia przez camelCase propsy: onClick, onChange, onSubmit, onFocus, onKeyDown…
Klik — handler inline vs referencyjna
// inline (krótka akcja)
<button onClick={() => console.log('klik')}>Klik</button>
// referencyjna (lepsza czytelność)
function App() {
function handleClick() {
alert('Kliknięto!');
}
return <button onClick={handleClick}>Klik</button>;
}
onClick={handleClick} — bez nawiasów! Gdybyś napisał onClick={handleClick()}, funkcja wykonałaby się od razu przy renderze.onChange — wartość inputa
function Szukaj() {
const [q, setQ] = useState('');
return (
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Szukaj…"
/>
);
}
onSubmit — formularze
function Form() {
function handleSubmit(e) {
e.preventDefault(); // ← blokuje przeładowanie
console.log('wysłano!');
}
return (
<form onSubmit={handleSubmit}>
<input />
<button type="submit">Wyślij</button>
</form>
);
}
Przekazywanie argumentów do handlera
// Gdy potrzebujesz przekazać id lub inne dane:
<button onClick={() => usunElement(id)}>Usuń</button>
Lista ważnych eventów
| Event | Opis | Typowe użycie |
|---|---|---|
onClick | Kliknięcie | Przyciski, karty |
onChange | Zmiana wartości | Input, select |
onSubmit | Wysłanie formularza | Form |
onKeyDown | Naciśnięcie klawisza | Enter do zatwierdzenia |
onFocus / onBlur | Focus / utrata | Walidacja pól |
onMouseEnter / onMouseLeave | Hover | Tooltipy |
📌 Zadanie: Podgląd tekstu na żywo
Stwórz komponent Podglad, który:
— ma pole <input> tekstowe
— pod spodem wyświetla w <p> dokładnie to, co użytkownik wpisuje w czasie rzeczywistym
Użyj zdarzenia onChange i hooka useState.
import { useState } from "react";
function Podglad() {
const [tekst, setTekst] = useState("");
return (
<div>
<input
type="text"
value={tekst}
onChange={(e) => setTekst(e.target.value)}
placeholder="Wpisz coś..."
/>
<p>Podgląd: {tekst}</p>
</div>
);
}
export default Podglad;
8. Tablice i .map()
Renderowanie list w React = .map() na tablicy + key na każdym elemencie.
Podstawowy .map()
const owoce = ['Jabłko', 'Banan', 'Gruszka'];
function Lista() {
return (
<ul>
{owoce.map((owoc, i) => (
<li key={i}>{owoc}</li>
))}
</ul>
);
}
id z danych. index → tylko gdy lista się nie zmienia..map() z obiektami
const uzytkownicy = [
{ id: 1, imie: 'Jan', wiek: 25 },
{ id: 2, imie: 'Ola', wiek: 22 },
];
function Uzytkownicy() {
return (
<div>
{uzytkownicy.map(u => (
<div key={u.id}>
<h3>{u.imie}</h3>
<p>Wiek: {u.wiek}</p>
</div>
))}
</div>
);
}
.filter() — filtrowanie listy
// Pełnoletni użytkownicy
{uzytkownicy
.filter(u => u.wiek >= 18)
.map(u => <p key={u.id}>{u.imie}</p>)}
.find() — jeden element
const jan = uzytkownicy.find(u => u.imie === 'Jan');
// → { id: 1, imie: 'Jan', wiek: 25 }
.reduce() — agregacja
const suma = zamowienia.reduce((acc, z) => acc + z.kwota, 0);
[...new Set()] — unikalne wartości
const kategorie = [...new Set(produkty.map(p => p.kategoria))];
// → ['elektronika', 'ubrania', 'jedzenie']
.sort() — sortowanie
// numeryczne rosnąco
[...tablica].sort((a, b) => a.cena - b.cena)
// alfabetyczne
[...tablica].sort((a, b) => a.nazwa.localeCompare(b.nazwa))
.sort() mutuje tablicę! W React zawsze rób kopię: [...arr].sort()📌 Zadanie: Lista produktów
Mając tablicę produktów, wyrenderuj listę <ul> pokazującą nazwę i cenę każdego produktu. Pamiętaj o key.
const produkty = [
{ id: 1, nazwa: "Laptop", cena: 3500 },
{ id: 2, nazwa: "Telefon", cena: 1200 },
{ id: 3, nazwa: "Słuchawki", cena: 150 },
{ id: 4, nazwa: "Ładowarka", cena: 45 },
];
function ListaProduktow() {
const produkty = [
{ id: 1, nazwa: "Laptop", cena: 3500 },
{ id: 2, nazwa: "Telefon", cena: 1200 },
{ id: 3, nazwa: "Słuchawki", cena: 150 },
{ id: 4, nazwa: "Ładowarka", cena: 45 },
];
return (
<ul>
{produkty.map((p) => (
<li key={p.id}>{p.nazwa} — {p.cena} zł</li>
))}
</ul>
);
}
📌 Zadanie: Filtrowanie produktów
Rozszerz poprzednie zadanie — wyświetl tylko produkty, których cena jest wyższa niż 100 zł. Użyj .filter() przed .map().
function DrogieProdukty() {
const produkty = [
{ id: 1, nazwa: "Laptop", cena: 3500 },
{ id: 2, nazwa: "Telefon", cena: 1200 },
{ id: 3, nazwa: "Słuchawki", cena: 150 },
{ id: 4, nazwa: "Ładowarka", cena: 45 },
];
return (
<ul>
{produkty
.filter((p) => p.cena > 100)
.map((p) => (
<li key={p.id}>{p.nazwa} — {p.cena} zł</li>
))}
</ul>
);
}
9. Warunkowy rendering
W React decydujesz co wyświetlić w zależności od stanu. Trzy główne techniki:
Ternary operator (? :)
function Status({ zalogowany }) {
return (
<p>
{zalogowany ? 'Witaj z powrotem!' : 'Proszę się zalogować'}
</p>
);
}
Short-circuit &&
function Alert({ wiadomosc }) {
return (
<div>
{wiadomosc && <p className="alert">{wiadomosc}</p>}
</div>
);
}
// Jeśli wiadomosc = '' lub null → nic nie renderuje
0 && <Cos/> — React wyświetli 0. Lepiej: {count > 0 && ...}Wczesny return
function Karta({ dane }) {
if (!dane) return <p>Ładowanie...</p>;
return (
<div>
<h2>{dane.tytul}</h2>
<p>{dane.opis}</p>
</div>
);
}
Klasy warunkowe
// Dynamiczna klasa CSS
<div className={`karta ${aktywna ? 'aktywna' : ''}`}>
...
</div>
// Styl inline warunkowy
<p style={{ color: blad ? 'red' : 'green' }}>{tekst}</p>
Renderowanie listy z warunkiem
// Pokaż "brak wyników" gdy lista pusta
{elementy.length === 0
? <p>Brak wyników</p>
: elementy.map(el => <Karta key={el.id} {...el} />)
}
📌 Zadanie: Warunkowa wiadomość
Napisz komponent StatusZamowienia, który przyjmuje prop oplacone (boolean).
— Jeśli true → wyświetla zielony tekst „✅ Zamówienie opłacone"
— Jeśli false → wyświetla czerwony tekst „❌ Oczekuje na płatność"
Użyj operatora trójargumentowego (ternary).
function StatusZamowienia({ oplacone }) {
return (
<p style={{ color: oplacone ? "green" : "red" }}>
{oplacone
? "✅ Zamówienie opłacone"
: "❌ Oczekuje na płatność"}
</p>
);
}
// Użycie:
// <StatusZamowienia oplacone={true} />
// <StatusZamowienia oplacone={false} />
10. useEffect
useEffect obsługuje efekty uboczne: pobieranie danych, subskrypcje, timery, zmiana tytułu strony.
Składnia
import { useEffect } from 'react';
useEffect(() => {
// kod efektu
return () => {
// cleanup (opcjonalny)
};
}, [zależności]);
Tablica zależności
| Zapis | Kiedy się odpala |
|---|---|
useEffect(() => {...}) | Po każdym renderze |
useEffect(() => {...}, []) | Tylko raz — mount |
useEffect(() => {...}, [x, y]) | Gdy zmieni się x lub y |
Przykład: tytuł strony
useEffect(() => {
document.title = `Masz ${count} wiadomości`;
}, [count]);
Przykład: fetch danych
function Posty() {
const [posty, setPosty] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(res => res.json())
.then(data => {
setPosty(data.slice(0, 10));
setLoading(false);
});
}, []); // pusta tablica = raz na mount
if (loading) return <p>Ładowanie...</p>;
return (
<ul>{posty.map(p => <li key={p.id}>{p.title}</li>)}</ul>
);
}
Cleanup — sprzątanie
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(timer); // ← cleanup
}, []);
📌 Zadanie: Dynamiczny tytuł strony
Napisz komponent TytulStrony, który:
— ma licznik (useState) z przyciskiem +
— używa useEffect, żeby przy każdej zmianie licznika aktualizować document.title na „Kliknięcia: X"
Pamiętaj o tablicy zależności!
import { useState, useEffect } from "react";
function TytulStrony() {
const [licznik, setLicznik] = useState(0);
useEffect(() => {
document.title = `Kliknięcia: ${licznik}`;
}, [licznik]);
return (
<div>
<p>Licznik: {licznik}</p>
<button onClick={() => setLicznik(licznik + 1)}>+</button>
</div>
);
}
export default TytulStrony;
11. Formularze
W React formularze opierają się na controlled components — wartość pola jest w state, a zmiany obsługuje onChange.
Input tekstowy
const [imie, setImie] = useState('');
<input
type="text"
value={imie}
onChange={(e) => setImie(e.target.value)}
placeholder="Wpisz imię"
/>
Select (dropdown)
const [kolor, setKolor] = useState('niebieski');
<select value={kolor} onChange={(e) => setKolor(e.target.value)}>
<option value="niebieski">Niebieski</option>
<option value="zielony">Zielony</option>
<option value="czerwony">Czerwony</option>
</select>
Dynamiczny select
const opcje = ['A', 'B', 'C'];
<select value={wybrany} onChange={(e) => setWybrany(e.target.value)}>
{opcje.map(o => <option key={o} value={o}>{o}</option>)}
</select>
Textarea
const [opis, setOpis] = useState('');
<textarea value={opis} onChange={(e) => setOpis(e.target.value)} />
Checkbox
const [zgoda, setZgoda] = useState(false);
<label>
<input
type="checkbox"
checked={zgoda}
onChange={(e) => setZgoda(e.target.checked)}
/>
Akceptuję regulamin
</label>
Kompletny formularz
function Formularz() {
const [dane, setDane] = useState({ imie: '', email: '', wiadomosc: '' });
function handleChange(e) {
setDane({ ...dane, [e.target.name]: e.target.value });
}
function handleSubmit(e) {
e.preventDefault();
console.log(dane);
}
return (
<form onSubmit={handleSubmit}>
<input name="imie" value={dane.imie} onChange={handleChange} />
<input name="email" value={dane.email} onChange={handleChange} />
<textarea name="wiadomosc" value={dane.wiadomosc} onChange={handleChange} />
<button type="submit">Wyślij</button>
</form>
);
}
[e.target.name] to computed property name — jeden handler do wielu pól!Prosta walidacja
const [blad, setBlad] = useState('');
function handleSubmit(e) {
e.preventDefault();
if (!dane.imie.trim()) {
setBlad('Imię jest wymagane');
return;
}
setBlad('');
// wyślij dane...
}
{blad && <p style={{color:'red'}}>{blad}</p>}
📌 Zadanie: Formularz rejestracji
Zbuduj komponent Rejestracja z formularzem zawierającym:
— pole „Imię" (input text)
— pole „Email" (input email)
— przycisk „Zarejestruj się"
Po wysłaniu formularza wyświetl w konsoli obiekt z danymi: { imie, email }. Użyj kontrolowanych inputów.
import { useState } from "react";
function Rejestracja() {
const [imie, setImie] = useState("");
const [email, setEmail] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
console.log({ imie, email });
};
return (
<form onSubmit={handleSubmit}>
<label>Imię:
<input type="text" value={imie}
onChange={(e) => setImie(e.target.value)} />
</label><br />
<label>Email:
<input type="email" value={email}
onChange={(e) => setEmail(e.target.value)} />
</label><br />
<button type="submit">Zarejestruj się</button>
</form>
);
}
export default Rejestracja;
12. Struktura projektu React
Dobra organizacja plików to klucz do utrzymywalnego kodu. Oto standard Vite + React:
Typowa struktura
moj-projekt/
├── node_modules/ ← zależności (nie ruszać)
├── public/ ← pliki statyczne (favicon, obrazy)
├── src/
│ ├── assets/ ← obrazy, czcionki, dane
│ ├── components/ ← komponenty (.jsx)
│ │ ├── Header.jsx
│ │ ├── Footer.jsx
│ │ └── Card.jsx
│ ├── App.jsx ← główny komponent
│ ├── App.css ← style główne
│ ├── index.js ← punkt wejścia
│ └── index.css ← style globalne
├── index.html ← szablon HTML
├── package.json ← zależności + skrypty
└── vite.config.js ← konfiguracja Vite
Cykl życia plików
| Plik | Rola |
|---|---|
index.js | Montuje <App/> do drzewa DOM |
App.jsx | Główny komponent — tu składasz całą aplikację |
index.html | Jedyny HTML — <div id="root"> |
package.json | Lista zależności + skrypty npm run dev |
Import komponentu
// App.jsx
import Header from './components/Header';
import Card from './components/Card';
function App() {
return (
<>
<Header />
<Card title="Hello" />
</>
);
}
Import danych z pliku
// src/assets/dane.js (eksport)
export const produkty = [
{ id: 1, nazwa: 'Laptop', cena: 3500 },
{ id: 2, nazwa: 'Mysz', cena: 120 },
];
// App.jsx (import)
import { produkty } from './assets/dane';
Import CSS
import './App.css'; // ← import globalny
// albo CSS Modules:
import styles from './Card.module.css';
<div className={styles.card}>...</div>
Komendy do zapamiętania
| Komenda | Co robi |
|---|---|
npm create vite@latest | Tworzy nowy projekt |
npm install | Instaluje zależności |
npm run dev | Uruchamia serwer dev |
npm run build | Buduje produkcję |
npm run preview | Podgląd buildu |
Spread operator w React
// Kopiowanie tablicy (immutable update)
const nowa = [...stara, nowyElement];
// Kopiowanie obiektu
const nowy = { ...stary, imie: 'Nowe' };
// Usunięcie elementu
const bez = lista.filter(el => el.id !== idDoUsuniecia);
// Aktualizacja jednego elementu
const zaktualizowane = lista.map(el =>
el.id === id ? { ...el, gotowe: true } : el
);
📌 Zadanie: Import i eksport
Masz następującą strukturę plików:
src/
App.jsx
components/
Naglowek.jsx
Stopka.jsx
ui/
Przycisk.jsx
Napisz:
1. Eksport domyślny w każdym z trzech komponentów
2. Poprawne importy wszystkich trzech komponentów w App.jsx
// components/Naglowek.jsx
function Naglowek() {
return <header>Nagłówek strony</header>;
}
export default Naglowek;
// components/Stopka.jsx
function Stopka() {
return <footer>Stopka strony</footer>;
}
export default Stopka;
// components/ui/Przycisk.jsx
function Przycisk() {
return <button>Kliknij</button>;
}
export default Przycisk;
// App.jsx
import Naglowek from "./components/Naglowek";
import Stopka from "./components/Stopka";
import Przycisk from "./components/ui/Przycisk";
function App() {
return (
<>
<Naglowek />
<Przycisk />
<Stopka />
</>
);
}
export default App;
13. React Router
React to SPA (Single Page Application) — cała strona to jeden plik HTML. React Router pozwala robić wiele "podstron" bez przeładowania strony.
Po co router?
| Bez routera | Z routerem |
|---|---|
| Jedna strona, wszystko w App.js | Osobne "podstrony": /home, /about, /contact |
| Ukrywasz/pokazujesz komponenty stanem | URL zmienia się automatycznie |
| Przycisk "Wstecz" nie działa | Przycisk "Wstecz" działa normalnie |
| Nie można udostępnić linka do konkretnej strony | Każda podstrona ma swój URL |
Instalacja
npm install react-router-dom
To jedyna dodatkowa paczka — React nie ma routera wbudowanego (w odróżnieniu od Angulara).
.js. Vite obsługuje to automatycznie. W naszych projektach używamy .js — tak jak widać w edytorze po utworzeniu projektu.Konfiguracja w index.js
import { BrowserRouter } from 'react-router-dom';
import App from './App';
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Definiowanie tras w App.js
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
function App() {
return (
<div>
<h1>Moja strona</h1>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
);
}
| Element | Co robi |
|---|---|
<Routes> | Kontener — sprawdza URL i renderuje pasującą trasę |
<Route path="/" element={...} /> | Jedna trasa: gdy URL = "/", pokaż komponent Home |
path="/about" | Ścieżka URL (to co widać w pasku przeglądarki) |
element={<About />} | Komponent do wyrenderowania |
Nawigacja — <Link> zamiast <a>
import { Link } from 'react-router-dom';
function Navbar() {
return (
<nav>
<Link to="/">Strona główna</Link>
<Link to="/about">O nas</Link>
<Link to="/contact">Kontakt</Link>
</nav>
);
}
Strona 404 — gdy nic nie pasuje
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<h2>404 — Nie znaleziono strony</h2>} />
</Routes>
path="*" — łapie wszystkie adresy które nie pasują do żadnej trasy.
Aktywny link — NavLink
import { NavLink } from 'react-router-dom';
<NavLink to="/" className={({ isActive }) => isActive ? "active" : ""}>
Strona główna
</NavLink>
NavLink działa jak Link, ale wie czy jest aktywny. Dzięki temu możesz podświetlić aktualny link w menu.
useEffect + Router — tytuł strony
Częsty pattern: zmiana document.title po wejściu na podstronę. Do tego potrzebujemy useEffect + hooka useLocation:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function About() {
const location = useLocation();
useEffect(() => {
document.title = "O nas — MiniStrona";
}, []); // [] = tylko przy montowaniu
return <h2>O nas</h2>;
}
useEffect(() => {...}, []) odpala się raz — gdy komponent się pojawia. Idealny moment na zmianę tytułu, pobranie danych z API, itp.useEffect + fetch — dane z API
Na podstronie możesz pobrać dane z zewnętrznego API przy wejściu:
import { useState, useEffect } from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <p>Ładowanie...</p>;
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
| Element | Co robi |
|---|---|
useEffect(() => {...}, []) | Odpala fetch raz — przy wejściu na stronę |
useState(true) dla loading | Pokazuje "Ładowanie..." dopóki dane nie przyjdą |
.then(res => res.json()) | Zamienia odpowiedź HTTP na obiekt JS |
setLoading(false) | Ukrywa "Ładowanie..." i pokazuje listę |
Podsumowanie
| Import | Co robi |
|---|---|
BrowserRouter | Opakowuje aplikację — włącza routing |
Routes | Kontener na trasy |
Route | Jedna trasa: path + element |
Link | Nawigacja bez przeładowania strony |
NavLink | Link z info czy jest aktywny |
Zadanie: Opisz słowami
Masz aplikację z 3 podstronami: Strona główna (/), Produkty (/products), Kontakt (/contact).
Wymień z pamięci:
1. Co trzeba zainstalować?
2. Co trzeba zmienić w index.js?
3. Jak wygląda App.js z trasami?
4. Jak zrobić menu nawigacyjne?
1. npm install react-router-dom
2. Opakowac <App /> w <BrowserRouter> w index.js
3. W App.js:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/contact" element={<Contact />} />
</Routes>
4. Uzyc <Link to="/products">Produkty</Link> zamiast <a>
14. Biblioteki w React + CRUD (axios)
W tej lekcji nauczysz się instalować biblioteki z npm i używać ich w React, oraz wykonać kompletny CRUD — czyli 4 operacje na danych z serwera (Create, Read, Update, Delete).
Co to jest biblioteka?
Biblioteka to gotowy kod który ktoś napisał i udostępnił przez npm (Node Package Manager). Zamiast pisać wszystko od zera — instalujesz paczkę i jej używasz.
| Biblioteka | Co robi |
|---|---|
react | Sam React (już mamy) |
react-router-dom | Routing (poprzednia lekcja) |
bootstrap | Gotowe style CSS |
axios | Wysyłanie zapytań HTTP do API ← dziś |
moment / dayjs | Praca z datami |
chart.js | Wykresy |
Instalacja biblioteki
npm install nazwa-biblioteki
Po instalacji biblioteka pojawia się w package.json w sekcji dependencies. Importujemy ją w plikach .js:
import axios from 'axios';
Co to jest API?
API (Application Programming Interface) to "punkt kontaktu" z serwerem. Wysyłamy zapytanie HTTP i dostajemy dane w formacie JSON.
Twoja apka ──── GET /posts ────► Serwer
◄──── [JSON dane] ────
Mock API — JSONPlaceholder
Do nauki używamy JSONPlaceholder — darmowe fałszywe API, które udaje że ma dane. Idealne do ćwiczeń:
https://jsonplaceholder.typicode.com/posts
Można robić wszystkie operacje CRUD — serwer odpowiada tak jakby zapisał, ale w rzeczywistości nic nie zmienia. Idealny do nauki.
CRUD — 4 operacje
CRUD to skrót od 4 podstawowych operacji na danych:
| Litera | Operacja | HTTP Method | Co robi |
|---|---|---|---|
| C | Create | POST | Tworzy nowy zasób |
| R | Read | GET | Czyta dane (lista lub jeden) |
| U | Update | PUT / PATCH | Aktualizuje zasób |
| D | Delete | DELETE | Usuwa zasób |
axios vs fetch — czemu axios?
| Cecha | fetch (wbudowany) | axios (biblioteka) |
|---|---|---|
| Krócej | 2 kroki: fetch().then(r=>r.json()) | 1 krok: axios.get() — od razu daje dane |
| JSON automatycznie | Ręcznie res.json() | Automatycznie |
| Obsługa błędów | 404 NIE rzuca błędu (musisz sprawdzić) | 404/500 automatycznie rzuca błąd |
| POST/PUT | Trzeba ręcznie ustawić headers + JSON.stringify | Wystarczy obiekt |
Operacja READ — GET (axios.get)
import axios from 'axios';
import { useState, useEffect } from 'react';
function ListaPostow() {
const [posty, setPosty] = useState([]);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/posts')
.then(response => setPosty(response.data));
}, []);
return (
<ul>
{posty.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
response.data — axios od razu rozpakowuje JSON. Nie musisz robić .json() jak w fetch.Operacja CREATE — POST (axios.post)
function dodajPost() {
const nowy = {
title: "Mój nowy post",
body: "Treść posta",
userId: 1
};
axios.post('https://jsonplaceholder.typicode.com/posts', nowy)
.then(response => {
console.log("Utworzono:", response.data);
// response.data ma wszystko + nowe id z serwera
setPosty([...posty, response.data]);
});
}
Drugi argument (nowy) to dane do wysłania. Serwer odpowiada utworzonym zasobem (z dodanym id).
Operacja UPDATE — PUT (axios.put)
function edytujPost(id) {
const zmieniony = {
title: "Zmieniony tytuł",
body: "Nowa treść",
userId: 1
};
axios.put(`https://jsonplaceholder.typicode.com/posts/${id}`, zmieniony)
.then(response => {
// Zaktualizuj listę lokalnie
setPosty(posty.map(p => p.id === id ? response.data : p));
});
}
/posts/5 — to ten post zostanie nadpisany.Operacja DELETE — DELETE (axios.delete)
function usunPost(id) {
axios.delete(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then(() => {
// Usuń lokalnie z listy
setPosty(posty.filter(p => p.id !== id));
});
}
DELETE nie wysyła danych — tylko URL z ID. Serwer odpowiada pustą odpowiedzią (status 200).
Loading state — pokaż "Ładowanie..."
Zapytania do API trwają czas. Powinniśmy pokazać użytkownikowi że coś się dzieje:
const [posty, setPosty] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/posts')
.then(response => {
setPosty(response.data);
setLoading(false); // koniec ładowania
});
}, []);
if (loading) return <p>Ładowanie...</p>;
return <ul>{posty.map(...)}</ul>;
Obsługa błędów — try / catch (async/await)
Jeśli serwer padnie albo nie ma internetu — zapytanie wywali błąd. Trzeba go obsłużyć:
// Wersja z .then().catch()
axios.get(url)
.then(response => setPosty(response.data))
.catch(error => alert("Błąd: " + error.message));
// Wersja z async/await + try/catch
async function pobierz() {
try {
const response = await axios.get(url);
setPosty(response.data);
} catch (error) {
alert("Błąd: " + error.message);
}
}
await czeka na odpowiedź zanim przejdzie dalej.Pełen wzorzec CRUD
// READ
axios.get(URL).then(r => setPosty(r.data));
// CREATE
axios.post(URL, dane).then(r => setPosty([...posty, r.data]));
// UPDATE
axios.put(`${URL}/${id}`, dane).then(r =>
setPosty(posty.map(p => p.id === id ? r.data : p))
);
// DELETE
axios.delete(`${URL}/${id}`).then(() =>
setPosty(posty.filter(p => p.id !== id))
);
Podsumowanie
| Pojęcie | Co warto pamiętać |
|---|---|
npm install | Pobiera bibliotekę i zapisuje w package.json |
import x from 'lib' | Włącza bibliotekę w pliku |
| API | "Punkt kontaktu" z serwerem przez HTTP |
| JSON | Format danych (wygląda jak obiekt JS) |
| CRUD | 4 operacje: Create / Read / Update / Delete |
axios.get/post/put/delete | 4 metody — po jednej na każdą operację |
response.data | Tu są dane z serwera (axios automatycznie parsuje JSON) |
| Loading state | Pokaż użytkownikowi że trwa ładowanie |
| try / catch | Obsługa błędów (brak internetu, 404, 500) |
Zadanie: Opisz co zrobi ten kod
function App() {
const [users, setUsers] = useState([]);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users')
.then(r => setUsers(r.data));
}, []);
function dodajUsera() {
axios.post('https://jsonplaceholder.typicode.com/users', {
name: "Jan Kowalski"
}).then(r => setUsers([...users, r.data]));
}
return (
<div>
<button onClick={dodajUsera}>Dodaj</button>
{users.map(u => <p key={u.id}>{u.name}</p>)}
</div>
);
}
Co robi:
- Przy starcie (
useEffectz []) pobiera listę użytkowników z API i zapisuje w stanie - Renderuje przycisk "Dodaj" oraz listę imion użytkowników
- Po kliknięciu "Dodaj" wysyła POST do API z nowym userem ("Jan Kowalski")
- Gdy serwer odpowie — dodaje nowego usera do listy (z otrzymanym
id)
Operacje CRUD: READ (pobieranie listy) + CREATE (dodawanie). Brakuje: UPDATE i DELETE.
Rozgrzewka — Mini-strona (Router)
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
1. Czym jest SPA?
Single Page Application — jedna strona HTML, zawartość zmienia się dynamicznie bez przeładowania.
2. Dlaczego używamy <Link> zamiast <a>?
<a> przeładowuje całą stronę. <Link> zmienia URL i komponent bez przeładowania — szybciej, bez migania.
3. Jak złapać adres który nie pasuje do żadnej trasy?
Dodajemy <Route path="*" element={<NotFound />} /> — gwiazdka łapie wszystko.
4. Jaka jest różnica między Link a NavLink?
NavLink wie czy jest aktywny (isActive) — można podświetlić aktualny link w menu. Link tego nie umie.
5. Kiedy odpala się useEffect z pustą tablicą zależności []?
Raz — przy montowaniu komponentu (gdy pojawia się na ekranie). Idealny do: zmiany tytułu strony, pobrania danych z API.
Mini-strona — Instrukcja
Treść zadania
Wykonaj mini-stronę z nawigacją używając React Router:
- 3 podstrony: Strona główna (/), O nas (/about), Kontakt (/contact)
- Menu nawigacyjne widoczne na każdej stronie
- Aktywny link podświetlony innym kolorem (NavLink)
- Strona 404 dla nieistniejących adresów
- Na stronie Kontakt — formularz z polami: imię, email, wiadomość
- useEffect — zmiana tytułu karty przeglądarki na każdej podstronie
- Na stronie "O nas" — pobranie danych z API (fetch + useEffect)
- Bootstrap do stylowania
Mini-strona — Budowa
npm create vite@latest mini-strona -- --template react
cd mini-strona
npm install
npm install react-router-dom bootstrap
W index.js dodaj import Bootstrap:
import 'bootstrap/dist/css/bootstrap.min.css';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Utwórz folder src/pages/ i 4 pliki:
pages/Home.js:
import { useEffect } from 'react';
function Home() {
useEffect(() => {
document.title = "Home — MiniStrona";
}, []);
return (
<div>
<h2>Strona główna</h2>
<p>Witaj na naszej stronie! Używamy React Router.</p>
</div>
);
}
export default Home;
pages/About.js — z useEffect (pobiera użytkowników z API):
import { useState, useEffect } from 'react';
function About() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
document.title = "O nas — MiniStrona";
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return (
<div>
<h2>O nas</h2>
<p>Jesteśmy zespołem uczącym się Reacta!</p>
<h3>Nasz zespół</h3>
{loading ? <p>Ładowanie...</p> : (
<ul className="list-group">
{users.slice(0, 5).map(u => (
<li key={u.id} className="list-group-item">
<strong>{u.name}</strong> — {u.email}
</li>
))}
</ul>
)}
</div>
);
}
export default About;
pages/Contact.js: (z formularzem — krok 5)
pages/NotFound.js:
function NotFound() {
return (
<div className="text-center mt-5">
<h2>404</h2>
<p>Nie znaleziono strony!</p>
</div>
);
}
export default NotFound;
Utwórz src/components/Navbar.js:
import { NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav className="navbar navbar-expand navbar-dark bg-dark mb-4">
<div className="container">
<span className="navbar-brand">MiniStrona</span>
<div className="navbar-nav">
<NavLink to="/" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Home</NavLink>
<NavLink to="/about" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>O nas</NavLink>
<NavLink to="/contact" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Kontakt</NavLink>
</div>
</div>
</nav>
);
}
export default Navbar;
active podświetla aktywny link. NavLink dostarcza info czy dany link jest aktualnie aktywny.
Utwórz src/pages/Contact.js:
import { useState, useEffect } from 'react';
function Contact() {
const [dane, setDane] = useState({ imie: '', email: '', wiadomosc: '' });
const [wyslano, setWyslano] = useState(false);
useEffect(() => {
document.title = "Kontakt — MiniStrona";
}, []);
function handleChange(e) {
setDane({ ...dane, [e.target.name]: e.target.value });
}
function handleSubmit(e) {
e.preventDefault();
setWyslano(true);
}
if (wyslano) {
return (
<div className="alert alert-success">
Dziękujemy, {dane.imie}! Wiadomość została wysłana.
</div>
);
}
return (
<div>
<h2>Kontakt</h2>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Imię</label>
<input type="text" className="form-control" name="imie"
value={dane.imie} onChange={handleChange} required />
</div>
<div className="mb-3">
<label className="form-label">Email</label>
<input type="email" className="form-control" name="email"
value={dane.email} onChange={handleChange} required />
</div>
<div className="mb-3">
<label className="form-label">Wiadomość</label>
<textarea className="form-control" name="wiadomosc" rows="4"
value={dane.wiadomosc} onChange={handleChange} required />
</div>
<button type="submit" className="btn btn-primary">Wyślij</button>
</form>
</div>
);
}
export default Contact;
[e.target.name].
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
function App() {
return (
<div>
<Navbar />
<div className="container">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export default App;
npm run dev
Sprawdź:
| Test | Oczekiwany wynik |
|---|---|
| Otwórz http://localhost:5173 | Strona główna + menu |
| Kliknij "O nas" | Lista użytkowników z API, tytuł karty = "O nas — MiniStrona" |
| Kliknij "Kontakt" | Formularz, URL = /contact |
| Wypełnij formularz i wyślij | Zielony alert "Dziękujemy" |
| Wpisz w URL /xyz | Strona 404 |
| Przycisk "Wstecz" w przeglądarce | Wraca do poprzedniej strony |
| Aktywny link w menu | Podświetlony na biało |
Mini-strona — Gotowy kod
Struktura plików
src/
├── components/
│ └── Navbar.js
├── pages/
│ ├── Home.js
│ ├── About.js
│ ├── Contact.js
│ └── NotFound.js
├── App.js
└── index.js
index.js
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
App.js
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
function App() {
return (
<div>
<Navbar />
<div className="container">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export default App;
components/Navbar.js
import { NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav className="navbar navbar-expand navbar-dark bg-dark mb-4">
<div className="container">
<span className="navbar-brand">MiniStrona</span>
<div className="navbar-nav">
<NavLink to="/" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Home</NavLink>
<NavLink to="/about" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>O nas</NavLink>
<NavLink to="/contact" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Kontakt</NavLink>
</div>
</div>
</nav>
);
}
export default Navbar;
pages/Home.js
import { useEffect } from 'react';
function Home() {
useEffect(() => {
document.title = "Home — MiniStrona";
}, []);
return (
<div>
<h2>Strona główna</h2>
<p>Witaj na naszej stronie! Używamy React Router.</p>
</div>
);
}
export default Home;
pages/About.js
import { useState, useEffect } from 'react';
function About() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
document.title = "O nas — MiniStrona";
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return (
<div>
<h2>O nas</h2>
<p>Jesteśmy zespołem uczącym się Reacta!</p>
<h3>Nasz zespół</h3>
{loading ? <p>Ładowanie...</p> : (
<ul className="list-group">
{users.slice(0, 5).map(u => (
<li key={u.id} className="list-group-item">
<strong>{u.name}</strong> — {u.email}
</li>
))}
</ul>
)}
</div>
);
}
export default About;
pages/Contact.js
import { useState, useEffect } from 'react';
function Contact() {
const [dane, setDane] = useState({ imie: '', email: '', wiadomosc: '' });
const [wyslano, setWyslano] = useState(false);
useEffect(() => {
document.title = "Kontakt — MiniStrona";
}, []);
function handleChange(e) {
setDane({ ...dane, [e.target.name]: e.target.value });
}
function handleSubmit(e) {
e.preventDefault();
setWyslano(true);
}
if (wyslano) {
return (
<div className="alert alert-success">
Dziękujemy, {dane.imie}! Wiadomość została wysłana.
</div>
);
}
return (
<div>
<h2>Kontakt</h2>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Imię</label>
<input type="text" className="form-control" name="imie"
value={dane.imie} onChange={handleChange} required />
</div>
<div className="mb-3">
<label className="form-label">Email</label>
<input type="email" className="form-control" name="email"
value={dane.email} onChange={handleChange} required />
</div>
<div className="mb-3">
<label className="form-label">Wiadomość</label>
<textarea className="form-control" name="wiadomosc" rows="4"
value={dane.wiadomosc} onChange={handleChange} required />
</div>
<button type="submit" className="btn btn-primary">Wyślij</button>
</form>
</div>
);
}
export default Contact;
pages/NotFound.js
function NotFound() {
return (
<div className="text-center mt-5">
<h2>404</h2>
<p>Nie znaleziono strony!</p>
</div>
);
}
export default NotFound;
Sprawdzian — Mini-strona (Router)
5 pytań · 5 minut · minimum 60% żeby zdać
<Link> zamiast <a>?path="*" w <Route>?NavLink różni się od Link?<Navbar /> żeby był widoczny na każdej podstronie?Rozgrzewka — Hodowla świnek morskich
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
1. Czemu React nie może gadać bezpośrednio z MySQL?
Bo to dwa różne protokoły. Przeglądarka mówi tylko HTTP, MySQL używa swojego protokołu binarnego. Plus dane logowania do bazy nie mogą trafić do przeglądarki (każdy by je zobaczył w F12). Potrzebny pośrednik = backend (PHP/Node/Python).
2. Co oznacza skrót CRUD?
Create (tworzenie), Read (czytanie), Update (aktualizacja), Delete (usuwanie). 4 podstawowe operacje na danych — w HTTP to POST / GET / PUT / DELETE.
3. Co to PDO i czemu nie używamy mysql_query?
PDO (PHP Data Objects) to nowoczesny sposób łączenia z bazą — bezpieczny przed SQL injection dzięki prepare() + execute([params]). mysql_query jest stary, niebezpieczny i usunięty z PHP 7+.
4. Co to CORS i czemu PHP musi go ustawić?
Przeglądarka domyślnie blokuje zapytania z jednej domeny (np. localhost:3000) do innej (localhost:80). Backend musi powiedzieć: "spokojnie, pozwalam" przez header Access-Control-Allow-Origin: *.
5. Co to useNavigate?
Hook z react-router-dom — pozwala programowo zmienić stronę. Np. po dodaniu świnki: navigate('/swinki') wraca na listę bez przeładowania.
6. Co robi useParams?
Hook z react-router-dom — czyta parametry z URL. Dla trasy /swinki/:id/edytuj i URL /swinki/5/edytuj daje { id: "5" } (uwaga — jako string!).
Hodowla świnek morskich — Instrukcja
Treść zadania
Wykonaj aplikację React do zarządzania hodowlą świnek morskich. Aplikacja używa:
- React Router + Navbar — 5 podstron z nawigacją
- axios — komunikacja z backendem REST API
- PHP + MySQL (XAMPP) — realny backend i baza, nie mock
- Dwie tabele MySQL:
rasyiswinki(relacjarasy_id → rasy.id) - CRUD na świnkach: lista / dodaj / edytuj / usuń
- Read-only lista ras (z drugiej tabeli)
- Bootstrap do stylowania
Schemat bazy danych (z hodowla.sql)
Tabela rasy | Tabela swinki |
|---|---|
id (int, PK)rasa (string)
|
id (int, PK)rasy_id (int, FK → rasy.id)data_ur (date)miot (string)opis (string)imie (string)cena (int)
|
Strony aplikacji (Router)
| URL | Komponent | Co robi |
|---|---|---|
/ | Home | Powitanie + statystyki (ile świnek, ile ras, średnia cena) |
/swinki | ListaSwinek | READ — tabela świnek z nazwą rasy + Edytuj/Usuń |
/swinki/dodaj | DodajSwinke | CREATE — formularz nowej świnki |
/swinki/:id/edytuj | EdytujSwinke | UPDATE — formularz z wypełnionymi danymi |
/rasy | Rasy | READ — lista ras (read-only) |
* | NotFound | 404 |
Czego się nauczysz
| Pojęcie | Co robi |
|---|---|
| Architektura 3-warstwowa | Frontend (React) → Backend (PHP) → Baza (MySQL) |
| PHP + PDO | Bezpieczne łączenie się z MySQL i obsługa metod HTTP |
| CORS | Header pozwalający Reactowi (port 3000) gadać z Apache (port 80) |
Query string ?id= | Przekazanie ID do PHP bez konfiguracji serwera — działa wszędzie |
| axios + Router | CRUD rozłożony na osobne strony Reacta |
useNavigate | Programowa zmiana strony (po zapisie wraca na listę) |
useParams | Czytanie ID z URL (np. /swinki/5/edytuj) |
| Foreign key + JOIN | "JOIN" w React: rasy.find(r => r.id === swinka.rasy_id) |
Hodowla świnek morskich — Budowa
npx create-react-app hodowla
cd hodowla
npm install axios bootstrap react-router-dom
W src/index.js dodaj imports + opakuj App w BrowserRouter:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Wymagania: XAMPP uruchomiony (Apache + MySQL ze startu) i baza hodowla z tabelami rasy + swinki (już masz w phpMyAdmin).
W folderze Apache (htdocs) utwórz folder hodowla-api i 3 pliki:
htdocs/hodowla-api/
├── db.php ← połączenie z MySQL (PDO)
├── swinki.php ← CRUD na tabeli swinki
└── rasy.php ← READ na tabeli rasy
C:\xampp\htdocs\, macOS XAMPP /Applications/XAMPP/xamppfiles/htdocs/, MAMP /Applications/MAMP/htdocs/.
db.php — połączenie z bazą:
<?php
$host = '127.0.0.1';
$db = 'hodowla';
$user = 'root';
$pass = ''; // domyślnie XAMPP nie ma hasła
$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";
try {
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => $e->getMessage()]);
exit;
}
swinki.php — pełen CRUD (4 metody HTTP):
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
require 'db.php';
$method = $_SERVER['REQUEST_METHOD'];
$id = $_GET['id'] ?? null;
switch ($method) {
case 'GET':
if ($id) {
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
} else {
$stmt = $pdo->query('SELECT * FROM swinki');
echo json_encode($stmt->fetchAll());
}
break;
case 'POST':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare('INSERT INTO swinki (rasy_id, data_ur, miot, opis, imie, cena) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->execute([$data['rasy_id'], $data['data_ur'], $data['miot'], $data['opis'], $data['imie'], $data['cena']]);
$newId = $pdo->lastInsertId();
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$newId]);
echo json_encode($stmt->fetch());
break;
case 'PUT':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare('UPDATE swinki SET rasy_id=?, data_ur=?, miot=?, opis=?, imie=?, cena=? WHERE id=?');
$stmt->execute([$data['rasy_id'], $data['data_ur'], $data['miot'], $data['opis'], $data['imie'], $data['cena'], $id]);
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
break;
case 'DELETE':
$stmt = $pdo->prepare('DELETE FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode(['ok' => true]);
break;
}
rasy.php — read-only:
<?php
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json; charset=utf-8');
require 'db.php';
$stmt = $pdo->query('SELECT * FROM rasy ORDER BY rasa');
echo json_encode($stmt->fetchAll());
swinki.php oraz swinki.php?id=5. To działa identycznie na każdym serwerze (XAMPP/MAMP, Windows/macOS/Linux) bez konfiguracji Apache — query string jest uniwersalny, a URL rewrite jest kruchy i zależny od ustawień hostingu.
Test backendu — otwórz w przeglądarce http://localhost/hodowla-api/swinki.php — powinieneś zobaczyć JSON z listą świnek z bazy MySQL.
Access-Control-Allow-Origin: * mówi przeglądarce: "React z portu 3000 może się ze mną łączyć". Bez tego React dostałby błąd CORS w F12 Console.
Utwórz src/components/Navbar.js:
import { NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav className="navbar navbar-expand navbar-dark bg-dark mb-4">
<div className="container">
<span className="navbar-brand">🐹 Hodowla</span>
<div className="navbar-nav">
<NavLink to="/" end className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Home</NavLink>
<NavLink to="/swinki" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Świnki</NavLink>
<NavLink to="/rasy" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>Rasy</NavLink>
<NavLink to="/swinki/dodaj" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")
}>+ Dodaj</NavLink>
</div>
</div>
</nav>
);
}
export default Navbar;
end przy NavLink "Home" — bez tego "Home" byłby zawsze aktywny (bo / matchuje wszystkie URL-e).
Wyczyść src/App.js i wpisz:
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import ListaSwinek from './pages/ListaSwinek';
import DodajSwinke from './pages/DodajSwinke';
import EdytujSwinke from './pages/EdytujSwinke';
import Rasy from './pages/Rasy';
import NotFound from './pages/NotFound';
const API_URL = 'http://localhost/hodowla-api';
function App() {
return (
<div>
<Navbar />
<div className="container">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/swinki" element={<ListaSwinek />} />
<Route path="/swinki/dodaj" element={<DodajSwinke />} />
<Route path="/swinki/:id/edytuj" element={<EdytujSwinke />} />
<Route path="/rasy" element={<Rasy />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export { API_URL };
export default App;
API_URL wskazuje na backend PHP w XAMPP. Cała reszta Reacta nie wie skąd są dane — to magia separacji warstw.
Utwórz src/pages/Home.js:
import { useState, useEffect } from 'react';
import axios from 'axios';
import { API_URL } from '../App';
function Home() {
const [swinki, setSwinki] = useState([]);
const [rasy, setRasy] = useState([]);
useEffect(() => {
axios.get(`${API_URL}/swinki.php`).then(r => setSwinki(r.data));
axios.get(`${API_URL}/rasy.php`).then(r => setRasy(r.data));
}, []);
const sredniaCena = swinki.length === 0 ? 0 :
swinki.reduce((suma, s) => suma + s.cena, 0) / swinki.length;
return (
<div>
<h1>🐹 Hodowla świnek morskich</h1>
<p className="lead">Witaj w panelu zarządzania hodowlą.</p>
<div className="row">
<div className="col-md-4">
<div className="card text-center p-3">
<h2>{swinki.length}</h2>
<p className="text-muted">świnek w hodowli</p>
</div>
</div>
<div className="col-md-4">
<div className="card text-center p-3">
<h2>{rasy.length}</h2>
<p className="text-muted">dostępnych ras</p>
</div>
</div>
<div className="col-md-4">
<div className="card text-center p-3">
<h2>{sredniaCena.toFixed(0)} zł</h2>
<p className="text-muted">średnia cena</p>
</div>
</div>
</div>
</div>
);
}
export default Home;
.reduce() sumuje wszystkie ceny. Dzielenie przez liczbę świnek = średnia. .toFixed(0) = bez miejsc po przecinku.
Utwórz src/pages/ListaSwinek.js:
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import { API_URL } from '../App';
function ListaSwinek() {
const [swinki, setSwinki] = useState([]);
const [rasy, setRasy] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
axios.get(`${API_URL}/swinki.php`),
axios.get(`${API_URL}/rasy.php`)
]).then(([s, r]) => {
setSwinki(s.data);
setRasy(r.data);
setLoading(false);
});
}, []);
// "JOIN" przez find — szukamy rasy po rasy_id
function nazwaRasy(rasy_id) {
const r = rasy.find(x => x.id === rasy_id);
return r ? r.rasa : '?';
}
function usunSwinke(id) {
if (!window.confirm('Na pewno usunąć?')) return;
axios.delete(`${API_URL}/swinki.php?id=${id}`)
.then(() => setSwinki(swinki.filter(s => s.id !== id)));
}
if (loading) return <p>Ładowanie...</p>;
return (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<h2>Lista świnek</h2>
<Link to="/swinki/dodaj" className="btn btn-success">+ Dodaj świnkę</Link>
</div>
<table className="table table-striped">
<thead>
<tr>
<th>Imię</th><th>Rasa</th><th>Data ur.</th><th>Miot</th><th>Cena</th><th></th>
</tr>
</thead>
<tbody>
{swinki.map(s => (
<tr key={s.id}>
<td><strong>{s.imie}</strong></td>
<td>{nazwaRasy(s.rasy_id)}</td>
<td>{s.data_ur}</td>
<td>{s.miot}</td>
<td>{s.cena} zł</td>
<td>
<Link to={`/swinki/${s.id}/edytuj`} className="btn btn-sm btn-warning me-2">Edytuj</Link>
<button className="btn btn-sm btn-danger" onClick={() => usunSwinke(s.id)}>Usuń</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default ListaSwinek;
nazwaRasy(rasy_id) szuka rasy o danym id przez .find(). To samo co SQL JOIN, tylko po stronie frontu.
Utwórz src/pages/DodajSwinke.js. Pełny kod jest w zakładce „Gotowy kod" (sekcja FRONTEND — src/pages/DodajSwinke.js) — skopiuj go stamtąd.
Co ten komponent robi (zrozum zanim skopiujesz):
useStatetrzyma obiektdanez 6 polami formularza (imie, rasy_id, data_ur, miot, opis, cena)useEffectpobiera listę ras (rasy.php) do wypełnienia<select>handleChange— jeden handler na wszystkie pola (poname); dlarasy_idicenarobiNumber(value)handleSubmit—axios.postdoswinki.php, potemnavigate('/swinki')
<input> i <select> zawsze dają string. Dla rasy_id i cena trzeba zamienić na liczbę, inaczej „JOIN" w liście się rozjedzie (4 ≠ "4").
navigate('/swinki') — programowo wracamy na listę. Tak samo działa „Anuluj".
Utwórz src/pages/EdytujSwinke.js. Pełny kod jest w zakładce „Gotowy kod" (sekcja FRONTEND — src/pages/EdytujSwinke.js) — skopiuj go stamtąd.
EdytujSwinke to prawie ten sam formularz co DodajSwinke — różnice:
| DodajSwinke (CREATE) | EdytujSwinke (UPDATE) |
|---|---|
| pusty formularz na start | najpierw GET swinki.php?id=X wypełnia formularz |
axios.post | axios.put z tym samym id |
| — | useParams() czyta :id z URL |
| — | if (!dane) return Ładowanie dopóki nie przyjdą dane |
useParams()— czyta:idz URL (zawsze string)useEffect([id])— pobiera dane świnki PO ID przy wejściu na stronęuseNavigate()— po PUT wraca na listę
/swinki/5/edytuj. EdytujSwinke czyta id=5, pobiera GET swinki.php?id=5, wypełnia formularz. Po zapisie PUT swinki.php?id=5.
Utwórz src/pages/Rasy.js (lista ras read-only):
import { useState, useEffect } from 'react';
import axios from 'axios';
import { API_URL } from '../App';
function Rasy() {
const [rasy, setRasy] = useState([]);
useEffect(() => {
axios.get(`${API_URL}/rasy.php`).then(r => setRasy(r.data));
}, []);
return (
<div>
<h2>Dostępne rasy świnek morskich</h2>
<ul className="list-group">
{rasy.map(r => (
<li key={r.id} className="list-group-item">
<strong>#{r.id}</strong> — {r.rasa}
</li>
))}
</ul>
</div>
);
}
export default Rasy;
Utwórz src/pages/NotFound.js:
import { Link } from 'react-router-dom';
function NotFound() {
return (
<div className="text-center mt-5">
<h1>404</h1>
<p>Nie znaleziono strony!</p>
<Link to="/" className="btn btn-primary">Wróć do Home</Link>
</div>
);
}
export default NotFound;
Wymagane uruchomione:
- XAMPP — Apache + MySQL (zielone)
- React:
npm start(port 3000)
Otwórz dwa okna obok siebie:
- Lewe: React na
http://localhost:3000/swinki - Prawe: phpMyAdmin →
hodowla.swinki
F12 → Network i sprawdź każdy scenariusz:
| Akcja w React | Network | phpMyAdmin (po refresh) |
|---|---|---|
| Otwórz / | GET /swinki + GET /rasy | — |
| Kliknij "Świnki" | GET /swinki + GET /rasy | Tabela tych samych świnek |
| Dodaj świnkę "Pinki" | POST /swinki (200) | Nowy wiersz w tabeli swinki |
| Edytuj cenę świnki | PUT /swinki/{id} (200) | Zaktualizowana wartość w kolumnie cena |
| Usuń świnkę | DELETE /swinki/{id} (200) | Wiersz znika z tabeli |
| Odśwież stronę React | — | Dane zostają — to MySQL! |
| Dodaj wiersz w phpMyAdmin | — | Po refresh Reacta nowa świnka pojawia się w UI |
| Wpisz w URL /xyz | — | Strona 404 |
Hodowla świnek morskich — Gotowy kod
Struktura projektu (2 części)
FRONTEND (React):
hodowla/
├── package.json
└── src/
├── index.js ← BrowserRouter
├── App.js ← Routes + Navbar
├── components/
│ └── Navbar.js
└── pages/
├── Home.js
├── ListaSwinek.js
├── DodajSwinke.js
├── EdytujSwinke.js
├── Rasy.js
└── NotFound.js
BACKEND (PHP w htdocs):
htdocs/hodowla-api/
├── db.php ← połączenie PDO z MySQL
├── swinki.php ← CRUD na tabeli swinki
└── rasy.php ← READ na tabeli rasy
BACKEND — db.php
<?php
$host = '127.0.0.1';
$db = 'hodowla';
$user = 'root';
$pass = '';
$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";
try {
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => $e->getMessage()]);
exit;
}
BACKEND — swinki.php
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
require 'db.php';
$method = $_SERVER['REQUEST_METHOD'];
$id = $_GET['id'] ?? null;
switch ($method) {
case 'GET':
if ($id) {
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
} else {
$stmt = $pdo->query('SELECT * FROM swinki');
echo json_encode($stmt->fetchAll());
}
break;
case 'POST':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare('INSERT INTO swinki (rasy_id, data_ur, miot, opis, imie, cena) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->execute([$data['rasy_id'], $data['data_ur'], $data['miot'], $data['opis'], $data['imie'], $data['cena']]);
$newId = $pdo->lastInsertId();
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$newId]);
echo json_encode($stmt->fetch());
break;
case 'PUT':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare('UPDATE swinki SET rasy_id=?, data_ur=?, miot=?, opis=?, imie=?, cena=? WHERE id=?');
$stmt->execute([$data['rasy_id'], $data['data_ur'], $data['miot'], $data['opis'], $data['imie'], $data['cena'], $id]);
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
break;
case 'DELETE':
$stmt = $pdo->prepare('DELETE FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode(['ok' => true]);
break;
}
BACKEND — rasy.php
<?php
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json; charset=utf-8');
require 'db.php';
$stmt = $pdo->query('SELECT * FROM rasy ORDER BY rasa');
echo json_encode($stmt->fetchAll());
FRONTEND — src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
FRONTEND — src/App.js
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import ListaSwinek from './pages/ListaSwinek';
import DodajSwinke from './pages/DodajSwinke';
import EdytujSwinke from './pages/EdytujSwinke';
import Rasy from './pages/Rasy';
import NotFound from './pages/NotFound';
const API_URL = 'http://localhost/hodowla-api';
function App() {
return (
<div>
<Navbar />
<div className="container">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/swinki" element={<ListaSwinek />} />
<Route path="/swinki/dodaj" element={<DodajSwinke />} />
<Route path="/swinki/:id/edytuj" element={<EdytujSwinke />} />
<Route path="/rasy" element={<Rasy />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export { API_URL };
export default App;
FRONTEND — src/components/Navbar.js
import { NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav className="navbar navbar-expand navbar-dark bg-dark mb-4">
<div className="container">
<span className="navbar-brand">🐹 Hodowla</span>
<div className="navbar-nav">
<NavLink to="/" end className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")}>Home</NavLink>
<NavLink to="/swinki" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")}>Świnki</NavLink>
<NavLink to="/rasy" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")}>Rasy</NavLink>
<NavLink to="/swinki/dodaj" className={({ isActive }) =>
"nav-link" + (isActive ? " active" : "")}>+ Dodaj</NavLink>
</div>
</div>
</nav>
);
}
export default Navbar;
FRONTEND — src/pages/Home.js
import { useState, useEffect } from 'react';
import axios from 'axios';
import { API_URL } from '../App';
function Home() {
const [swinki, setSwinki] = useState([]);
const [rasy, setRasy] = useState([]);
useEffect(() => {
axios.get(`${API_URL}/swinki.php`).then(r => setSwinki(r.data));
axios.get(`${API_URL}/rasy.php`).then(r => setRasy(r.data));
}, []);
const sredniaCena = swinki.length === 0 ? 0 :
swinki.reduce((s, x) => s + x.cena, 0) / swinki.length;
return (
<div>
<h1>🐹 Hodowla świnek morskich</h1>
<p className="lead">Witaj w panelu zarządzania hodowlą.</p>
<div className="row">
<div className="col-md-4"><div className="card text-center p-3">
<h2>{swinki.length}</h2><p className="text-muted">świnek</p>
</div></div>
<div className="col-md-4"><div className="card text-center p-3">
<h2>{rasy.length}</h2><p className="text-muted">ras</p>
</div></div>
<div className="col-md-4"><div className="card text-center p-3">
<h2>{sredniaCena.toFixed(0)} zł</h2><p className="text-muted">średnia cena</p>
</div></div>
</div>
</div>
);
}
export default Home;
FRONTEND — src/pages/ListaSwinek.js
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import { API_URL } from '../App';
function ListaSwinek() {
const [swinki, setSwinki] = useState([]);
const [rasy, setRasy] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
axios.get(`${API_URL}/swinki.php`),
axios.get(`${API_URL}/rasy.php`)
]).then(([s, r]) => {
setSwinki(s.data);
setRasy(r.data);
setLoading(false);
});
}, []);
function nazwaRasy(rasy_id) {
const r = rasy.find(x => x.id === rasy_id);
return r ? r.rasa : '?';
}
function usunSwinke(id) {
if (!window.confirm('Na pewno usunąć?')) return;
axios.delete(`${API_URL}/swinki.php?id=${id}`)
.then(() => setSwinki(swinki.filter(s => s.id !== id)));
}
if (loading) return <p>Ładowanie...</p>;
return (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<h2>Lista świnek</h2>
<Link to="/swinki/dodaj" className="btn btn-success">+ Dodaj świnkę</Link>
</div>
<table className="table table-striped">
<thead><tr>
<th>Imię</th><th>Rasa</th><th>Data ur.</th><th>Miot</th><th>Cena</th><th></th>
</tr></thead>
<tbody>
{swinki.map(s => (
<tr key={s.id}>
<td><strong>{s.imie}</strong></td>
<td>{nazwaRasy(s.rasy_id)}</td>
<td>{s.data_ur}</td>
<td>{s.miot}</td>
<td>{s.cena} zł</td>
<td>
<Link to={`/swinki/${s.id}/edytuj`} className="btn btn-sm btn-warning me-2">Edytuj</Link>
<button className="btn btn-sm btn-danger" onClick={() => usunSwinke(s.id)}>Usuń</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default ListaSwinek;
FRONTEND — src/pages/DodajSwinke.js
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { API_URL } from '../App';
function DodajSwinke() {
const [rasy, setRasy] = useState([]);
const [dane, setDane] = useState({
imie: '', rasy_id: 1, data_ur: '', miot: '', opis: '', cena: 0
});
const navigate = useNavigate();
useEffect(() => {
axios.get(`${API_URL}/rasy.php`).then(r => setRasy(r.data));
}, []);
function handleChange(e) {
const { name, value } = e.target;
// rasy_id i cena to liczby
const wartosc = (name === 'rasy_id' || name === 'cena') ? Number(value) : value;
setDane({ ...dane, [name]: wartosc });
}
function handleSubmit(e) {
e.preventDefault();
axios.post(`${API_URL}/swinki.php`, dane).then(() => {
navigate('/swinki');
});
}
return (
<div>
<h2>Dodaj nową świnkę</h2>
<form onSubmit={handleSubmit}>
<div className="mb-2">
<label className="form-label">Imię</label>
<input name="imie" value={dane.imie} onChange={handleChange}
className="form-control" required />
</div>
<div className="mb-2">
<label className="form-label">Rasa</label>
<select name="rasy_id" value={dane.rasy_id} onChange={handleChange}
className="form-select">
{rasy.map(r => (
<option key={r.id} value={r.id}>{r.rasa}</option>
))}
</select>
</div>
<div className="mb-2">
<label className="form-label">Data urodzenia</label>
<input type="date" name="data_ur" value={dane.data_ur}
onChange={handleChange} className="form-control" required />
</div>
<div className="mb-2">
<label className="form-label">Miot</label>
<input name="miot" value={dane.miot} onChange={handleChange}
className="form-control" />
</div>
<div className="mb-2">
<label className="form-label">Opis</label>
<textarea name="opis" value={dane.opis} onChange={handleChange}
className="form-control" rows="2" />
</div>
<div className="mb-3">
<label className="form-label">Cena (zł)</label>
<input type="number" name="cena" value={dane.cena}
onChange={handleChange} className="form-control" />
</div>
<button type="submit" className="btn btn-success me-2">Zapisz</button>
<button type="button" className="btn btn-secondary"
onClick={() => navigate('/swinki')}>Anuluj</button>
</form>
</div>
);
}
export default DodajSwinke;
FRONTEND — src/pages/EdytujSwinke.js
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import axios from 'axios';
import { API_URL } from '../App';
function EdytujSwinke() {
const { id } = useParams(); // czyta :id z URL (string!)
const navigate = useNavigate();
const [rasy, setRasy] = useState([]);
const [dane, setDane] = useState(null);
useEffect(() => {
Promise.all([
axios.get(`${API_URL}/swinki.php?id=${id}`),
axios.get(`${API_URL}/rasy.php`)
]).then(([s, r]) => {
setDane(s.data);
setRasy(r.data);
});
}, [id]);
function handleChange(e) {
const { name, value } = e.target;
const wartosc = (name === 'rasy_id' || name === 'cena') ? Number(value) : value;
setDane({ ...dane, [name]: wartosc });
}
function handleSubmit(e) {
e.preventDefault();
axios.put(`${API_URL}/swinki.php?id=${id}`, dane).then(() => {
navigate('/swinki');
});
}
if (!dane) return <p>Ładowanie...</p>;
return (
<div>
<h2>Edytuj świnkę: {dane.imie}</h2>
<form onSubmit={handleSubmit}>
<div className="mb-2">
<label className="form-label">Imię</label>
<input name="imie" value={dane.imie} onChange={handleChange}
className="form-control" required />
</div>
<div className="mb-2">
<label className="form-label">Rasa</label>
<select name="rasy_id" value={dane.rasy_id} onChange={handleChange}
className="form-select">
{rasy.map(r => (
<option key={r.id} value={r.id}>{r.rasa}</option>
))}
</select>
</div>
<div className="mb-2">
<label className="form-label">Data urodzenia</label>
<input type="date" name="data_ur" value={dane.data_ur}
onChange={handleChange} className="form-control" />
</div>
<div className="mb-2">
<label className="form-label">Miot</label>
<input name="miot" value={dane.miot} onChange={handleChange}
className="form-control" />
</div>
<div className="mb-2">
<label className="form-label">Opis</label>
<textarea name="opis" value={dane.opis} onChange={handleChange}
className="form-control" rows="2" />
</div>
<div className="mb-3">
<label className="form-label">Cena</label>
<input type="number" name="cena" value={dane.cena}
onChange={handleChange} className="form-control" />
</div>
<button type="submit" className="btn btn-primary me-2">Zapisz zmiany</button>
<button type="button" className="btn btn-secondary"
onClick={() => navigate('/swinki')}>Anuluj</button>
</form>
</div>
);
}
export default EdytujSwinke;
FRONTEND — src/pages/Rasy.js
import { useState, useEffect } from 'react';
import axios from 'axios';
import { API_URL } from '../App';
function Rasy() {
const [rasy, setRasy] = useState([]);
useEffect(() => {
axios.get(`${API_URL}/rasy.php`).then(r => setRasy(r.data));
}, []);
return (
<div>
<h2>Dostępne rasy</h2>
<ul className="list-group">
{rasy.map(r => (
<li key={r.id} className="list-group-item">
<strong>#{r.id}</strong> — {r.rasa}
</li>
))}
</ul>
</div>
);
}
export default Rasy;
FRONTEND — src/pages/NotFound.js
import { Link } from 'react-router-dom';
function NotFound() {
return (
<div className="text-center mt-5">
<h1>404</h1>
<p>Nie znaleziono strony!</p>
<Link to="/" className="btn btn-primary">Wróć do Home</Link>
</div>
);
}
export default NotFound;
Co warto rozszerzyć (zadania domowe)
- Dodaj filtrowanie świnek po rasie (select w nagłówku tabeli)
- Sortowanie po cenie (rosnąco/malejąco)
- Walidacja: cena nie może być ujemna, imię min. 2 znaki
- Strona szczegółów świnki
/swinki/:id(cały opis + duże zdjęcie) - CRUD też dla ras (dodaj/edytuj/usuń rasę)
- Wynieś logikę API do
src/api.js— funkcjegetSwinki(),dodajSwinke()itd. - Backend w innym języku (Node+Express, Python+Flask) — React zostaje bez zmian, tylko
API_URL
Backend PHP + MySQL — szczegóły i debugging
Główne kroki budowy zawierają już pełen kod PHP. Tutaj znajdziesz diagram architektury, tabelę typowych błędów i głębsze wyjaśnienia. Otwórz tę sekcję jeśli coś nie działa albo chcesz lepiej zrozumieć co się dzieje pod spodem.
Architektura — kto z kim gada
[React :3000] ──HTTP──► [Apache/PHP :80] ──SQL──► [MySQL :3306]
(frontend) (pośrednik) (baza)
axios.get('/swinki')
↓
swinki.php (PHP czyta tabelę)
↓
SELECT * FROM swinki
↓
zwraca JSON do React
Wymagania (XAMPP — uczeń już ma)
- Apache uruchomiony (XAMPP Control Panel → Start)
- MySQL uruchomiony (XAMPP Control Panel → Start)
- Baza
hodowlaz tabelamirasyiswinki(już jest) - Folder Apache:
C:\xampp\htdocs\— tu trafią pliki PHP
W folderze htdocs utwórz folder hodowla-api. To będzie nasz backend. Powstaną 3 pliki:
htdocs/hodowla-api/
├── db.php ← połączenie z MySQL (PDO)
├── swinki.php ← CRUD na tabeli swinki
└── rasy.php ← READ na tabeli rasy
http://localhost/hodowla-api/ w przeglądarce. Jeśli pokaże listę plików (lub 403) — Apache działa.
<?php
$host = '127.0.0.1';
$db = 'hodowla';
$user = 'root';
$pass = ''; // domyślnie XAMPP nie ma hasła
$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";
try {
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => 'Database connection failed: ' . $e->getMessage()]);
exit;
}
PDO— nowoczesny sposób łączenia z bazą w PHP (bezpieczny przed SQL injection)$user='root',$pass=''— domyślne dane XAMPP. Na produkcji nigdy!FETCH_ASSOC— wyniki jako tablice asocjacyjne (klucze = nazwy kolumn)try/catch— jeśli baza padnie, zwracamy 500 + JSON z błędem (zamiast crasha)
<?php
// CORS — pozwól Reactowi (z portu 3000) gadać do Apache (port 80)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Type: application/json; charset=utf-8');
// Preflight (przeglądarka wysyła OPTIONS przed PUT/DELETE — odpowiadamy 204)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
require 'db.php';
$method = $_SERVER['REQUEST_METHOD'];
$id = $_GET['id'] ?? null;
switch ($method) {
case 'GET':
if ($id) {
// GET /swinki?id=5 — jedna świnka
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
} else {
// GET /swinki — wszystkie
$stmt = $pdo->query('SELECT * FROM swinki');
echo json_encode($stmt->fetchAll());
}
break;
case 'POST':
// POST /swinki — dodaj nową
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare(
'INSERT INTO swinki (rasy_id, data_ur, miot, opis, imie, cena)
VALUES (?, ?, ?, ?, ?, ?)'
);
$stmt->execute([
$data['rasy_id'], $data['data_ur'], $data['miot'],
$data['opis'], $data['imie'], $data['cena']
]);
$newId = $pdo->lastInsertId();
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$newId]);
echo json_encode($stmt->fetch());
break;
case 'PUT':
// PUT /swinki?id=5 — edytuj
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare(
'UPDATE swinki SET rasy_id=?, data_ur=?, miot=?, opis=?, imie=?, cena=?
WHERE id=?'
);
$stmt->execute([
$data['rasy_id'], $data['data_ur'], $data['miot'],
$data['opis'], $data['imie'], $data['cena'], $id
]);
$stmt = $pdo->prepare('SELECT * FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode($stmt->fetch());
break;
case 'DELETE':
// DELETE /swinki?id=5 — usuń
$stmt = $pdo->prepare('DELETE FROM swinki WHERE id = ?');
$stmt->execute([$id]);
echo json_encode(['ok' => true]);
break;
default:
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
prepare() + execute([...]). Wartości są przekazywane jako parametry, nie wklejane do tekstu zapytania. To jest bezpieczne.
json_decode(..., true) zamienia na tablicę asocjacyjną.
<?php
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json; charset=utf-8');
require 'db.php';
$stmt = $pdo->query('SELECT * FROM rasy ORDER BY rasa');
echo json_encode($stmt->fetchAll());
Krócej, bo rasy są tylko do czytania — nie dodajemy/edytujemy/usuwamy ras z tej apki.
Można by zrobić "ładne URL-e" (/swinki/5) przez Apache mod_rewrite i plik .htaccess. Świadomie tego nie robimy.
| Podejście | URL | Zależność |
|---|---|---|
| .htaccess + mod_rewrite | /swinki/5 | Wymaga AllowOverride All + mod_rewrite + restart Apache. Różni się Windows/macOS/MAMP. Kruche. |
| Query string (nasze) | /swinki.php?id=5 | Zero konfiguracji. Działa na każdym serwerze, zawsze. |
?id=) jest częścią standardu HTTP i działa bez wyjątku wszędzie. W realnym projekcie wybieramy rozwiązanie odporne na środowisko, nie ładniejsze ale kruche.
Dlatego React woła wprost: axios.get(`${API_URL}/swinki.php`) oraz axios.get(`${API_URL}/swinki.php?id=${id}`). PHP czyta ID przez $_GET['id'] — i to wszystko, żadnego pliku .htaccess.
Zanim podpinamy React — sprawdź że PHP samo działa. Otwórz w przeglądarce:
| URL | Co powinno pokazać |
|---|---|
http://localhost/hodowla-api/rasy.php | JSON z listą wszystkich ras |
http://localhost/hodowla-api/swinki.php | JSON z wszystkimi świnkami |
http://localhost/hodowla-api/swinki.php?id=1 | JSON jednej świnki (Crejzy) |
Jeśli widzisz błąd: sprawdź czy Apache + MySQL działają (XAMPP Control Panel), czy plik db.php ma poprawne dane logowania.
W src/App.js:
// było (json-server):
const API_URL = 'http://localhost:3001';
// teraz (PHP + MySQL):
const API_URL = 'http://localhost/hodowla-api';
Wyłącz json-server (nie potrzebujemy) i uruchom React:
npm start
Otwórz dwa okna obok siebie:
- Lewe: React na
http://localhost:3000/swinki - Prawe: phpMyAdmin →
hodowla.swinki
| Akcja w React | Co zobaczysz w phpMyAdmin (po odświeżeniu) |
|---|---|
| Dodaj świnkę "Pinki" | Nowy wiersz w tabeli swinki |
| Edytuj cenę świnki | Zaktualizowana wartość w kolumnie cena |
| Usuń świnkę | Wiersz znika z tabeli |
| Odśwież stronę React | Dane zostają — to NIE jest pamięć przeglądarki, to MySQL! |
| Dodaj wiersz w phpMyAdmin | Po refreshu Reacta — nowa świnka pojawia się w UI |
Typowe błędy i jak je naprawić
| Błąd | Przyczyna | Naprawa |
|---|---|---|
| CORS error w F12 Console | PHP nie wysłał headera Access-Control-Allow-Origin | Sprawdź czy header('Access-Control-Allow-Origin: *') jest na początku PHP |
| 404 Not Found | Zły URL — brak .php albo zła ścieżka folderu | Sprawdź że wołasz /hodowla-api/swinki.php (z .php!), folder w htdocs poprawny |
| 500 co jakiś czas (działa→sypie→refresh) | Limit połączeń MySQL pod serią requestów | W my.cnf: max_connections=300 + restart MySQL. Usuń <React.StrictMode> (dubluje requesty w dev) |
| "Database connection failed" | MySQL nie działa lub złe hasło | XAMPP → Start MySQL, sprawdź $user/$pass w db.php |
| 500 Internal Server Error | Błąd składni PHP lub w SQL | Otwórz C:\xampp\apache\logs\error.log — jest tam dokładny komunikat |
| POST/PUT zwraca null w PHP | Brak php://input + json_decode | axios wysyła JSON w body — czytamy: json_decode(file_get_contents('php://input'), true) |
| Polskie znaki = krzaki | Brak charset=utf8mb4 | Sprawdź DSN w db.php oraz Content-Type: application/json; charset=utf-8 |
Co teraz wiesz
- Jak React + axios łączy się z dowolnym backendem REST (json-server, PHP, Node, Python — wszystkie identycznie)
- Jak napisać REST API w PHP używając PDO (bezpiecznie, bez SQL injection)
- Co to CORS i czemu PHP musi go ustawić
- Czemu query string (
?id=) jest pewniejszy niż URL rewrite (.htaccess) - Architektura 3-warstwowa: Frontend → Backend → Baza
- Możesz teraz spiąć React z dowolną tabelą MySQL — schemat zawsze taki sam
Sprawdzian — Hodowla świnek (CRUD + Router)
5 pytań · 5 minut · minimum 60% żeby zdać
swinka.rasy_id i tablicę rasy?useParams() z react-router-dom dla URL /swinki/5/edytuj i trasy /swinki/:id/edytuj?<select name="rasy_id">. Bez konwersji wartość po onChange to "4" (string), a w db.json rasy_id to liczba 4. Co się dzieje przy find(r => r.id === swinka.rasy_id)?🔥 Rozgrzewka — Galeria zdjęć
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
.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.null). Jeśli chcemy zwrócić kilka elementów obok siebie, opakowujemy je w <div> lub pusty fragment <>...</>..map() na tablicy i jak używamy jej w JSX?.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.<Karta tytul="Foto" />, a odbieramy jako obiekt w parametrze funkcji: function Karta({ tytul }).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
5. Częste blokady
| Problem | Podpowiedź |
|---|---|
| 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
| Wymaganie | Detale | Status |
|---|---|---|
| Jeden komponent | App.jsx | ✅ |
| Nagłówek h1 | „Kategorie zdjęć" | ✅ |
| 3 pola switch | Kwiaty / Zwierzęta / Samochody, domyślnie ON | ✅ |
| Dane z dane.txt | 12 obiektów | ✅ |
| Obrazy w assets | public/assets/ | ✅ |
| Bloki obok siebie | d-flex flex-wrap | ✅ |
| Filtrowanie | switch → kategoria | ✅ |
| Pobierz → +1 | downloads w tablicy | ✅ |
| CSS zdjęcia | margin 5px + border-radius | ✅ |
| Bootstrap switch | form-check form-switch | ✅ |
| Bootstrap button | btn btn-primary | ✅ |
| Pętle + warunki | .map() + .filter() | ✅ |
| Znaczące nazwy | galeria, kwiaty, widoczne, pobierz | ✅ |
#01 — Budowa krok po kroku
„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.
egzamin/public/assets/obraz1.jpg … obraz12.jpg
„Dlaczego public/assets/ a nie src/assets/?"
Odp: Vite serwuje public/ statycznie. Prostsze ścieżki.
„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
„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ć.
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?"
„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."
<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.
„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."
#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ć
key przy renderowaniu listy elementów metodą .map()?tytul do komponentu Karta?Galeria.jsx?🔥 Rozgrzewka — Lista zadań
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
useState(wartośćPoczątkowa)?useState zwraca tablicę z dwoma elementami: aktualną wartością stanu oraz funkcją do jej zmiany. Zwykle destrukturyzujemy to tak: const [lista, setLista] = useState([]). Funkcja setLista powoduje ponowne wyrenderowanie komponentu z nową wartością.<form> dodajemy atrybut onSubmit={handleSubmit}. Wewnątrz funkcji handleSubmit wywołujemy e.preventDefault(), co blokuje domyślne przeładowanie strony. Następnie wykonujemy naszą logikę, np. dodanie elementu do stanu.push) — tworzymy nową za pomocą operatora spread: setLista([...lista, nowyElement]). Dzięki temu React wykrywa zmianę i przerenderowuje komponent. Aby dodać na początek: setLista([nowyElement, ...lista]).value={tekst} oraz onChange={e => setTekst(e.target.value)}. Dzięki temu React jest „jedynym źródłem prawdy" o zawartości pola — możemy ją walidować lub czyścić w dowolnym momencie.onChange na elemencie <input>?onChange jest wywoływane przy każdej zmianie wartości pola — czyli po każdym wpisanym lub usuniętym znaku. Obiekt zdarzenia e zawiera e.target.value z aktualną zawartością inputa. Typowy wzorzec: <input onChange={e => setTekst(e.target.value)} />.#02 — Lista zadań (Todo App)
Ćwiczenie autorskie w stylu egzaminacyjnym — uczy nowych koncepcji brakujących w #01.
Nowe umiejętności (vs #01 Galeria)
Treść zadania (styl egzaminacyjny)
Z zastosowaniem biblioteki React.js wykonaj aplikację internetową typu front-end realizującą funkcję listy zadań do zrobienia.
Założenia aplikacji
- Aplikacja składa się z jednego komponentu
- Dane początkowe — tablica 4 obiektów (poniżej), każdy zawiera:
- id (unikalna liczba)
- tytul (tekst zadania)
- priorytet (1 = niski, 2 = średni, 3 = wysoki)
- zrobione (true/false)
const poczatkowe = [
{ id: 1, tytul: 'Kupić mleko', priorytet: 1, zrobione: false },
{ id: 2, tytul: 'Zrobić projekt React', priorytet: 3, zrobione: false },
{ id: 3, tytul: 'Odpowiedzieć na maila', priorytet: 2, zrobione: true },
{ id: 4, tytul: 'Posprzątać biurko', priorytet: 1, zrobione: false },
]
Komponent składa się z
- Nagłówka h1: „Lista zadań"
- Formularza dodawania: pole tekstowe (controlled input), select z opcjami priorytetu, przycisk „Dodaj"
- Tabeli Bootstrap (
table table-striped) z kolumnami: Zadanie, Priorytet, Status, Akcje - W kolumnie Priorytet: badge Bootstrap (zielony=niski, żółty=średni, czerwony=wysoki)
- W kolumnie Status: tekst „Zrobione" / „Do zrobienia"
- W kolumnie Akcje: przycisk „✓ Zrobione" (toggle) i przycisk „✕ Usuń"
Stylowanie
- Zadania zrobione: tekst przekreślony (
text-decoration: line-through) i wyszarzony (opacity: 0.5) - Formularz, tabela, badge, przyciski: stylowane Bootstrap
Logika
- Dodaj: wpisz tytuł + wybierz priorytet → kliknij „Dodaj" → nowe zadanie w tablicy, pole się czyści
- Nie dodawaj pustego zadania (walidacja)
- „✓ Zrobione" toggleuje pole
zrobione(true↔false) - „✕ Usuń" usuwa zadanie z tablicy
- Na dole: licznik — „Zrobione: X z Y"
- Znaczące nazwy zmiennych i funkcji
Bootstrap — przykłady klas
<table class="table table-striped">
<span class="badge bg-success">niski</span>
<span class="badge bg-warning text-dark">średni</span>
<span class="badge bg-danger">wysoki</span>
<input class="form-control" type="text">
<select class="form-select">
<button class="btn btn-success btn-sm">
<button class="btn btn-danger btn-sm">
Dlaczego to zadanie? #01 Galeria uczyła filtrowania i zmiany pola. #02 Todo uczy DODAWANIA, USUWANIA, TOGGLE i controlled input — kompletuje zestaw CRUD. Po obu zadaniach uczeń umie wszystko co potrzebne na egzaminie.
#02 — Wprowadzenie dla nauczyciela
1. Otwarcie
„Ostatnio zrobiliśmy galerię — filtrowanie i zmiana pola w obiekcie. Dzisiaj robimy listę zadań. Nauczysz się: dodawać nowe elementy, usuwać, i toggleować stan. Po tym będziesz umiał wszystko co potrzebne na egzaminie."
2. Diagnoza — co pamięta z #01
„Przypomnij mi — jak zmienialiśmy downloads w galerii? Dlaczego nie mogliśmy zrobić z.downloads++?"
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „Bo React porównuje referencje, trzeba nowy obiekt" | „Idealnie. Dzisiaj użyjemy tego samego wzorca + nowe." |
| „Coś ze spreadem?" | „Tak! {...z, pole: nowa} — dzisiaj to powtórzymy." |
| Nie pamięta | „Ok, wrócimy do tego przy toggleowaniu. Spokojnie." |
3. Architektura
Pokaż schemat:
App:
state: zadania → tablica obiektów {id, tytul, priorytet, zrobione}
state: nowyTytul → string (controlled input)
state: nowyPriorytet → number (controlled select)
dodaj() → [...zadania, nowyObiekt] ← NOWE
usun(id) → zadania.filter(...) ← NOWE
toggle(id)→ zadania.map(z => ...) ← jak pobierz() w galerii
JSX: h1 + formularz + tabela { zadania.map(...) } + licznik
„Ile useState będziemy potrzebować?"
Odp: 3 — zadania (tablica), nowyTytul (string), nowyPriorytet (number).
„W galerii zmienialiśmy istniejący obiekt (.map + spread). Tu mamy 3 operacje — dodanie, usunięie, zmiana. Jaka metoda tablicowa służy do: a) dodania? b) usunięcia? c) zmiany?"
Odp: a) spread [...arr, nowy], b) .filter(), c) .map() + spread
4. Częste blokady
| Problem | Podpowiedź |
|---|---|
| Nie wie jak controlled input | „value={stan} + onChange. Pamiętasz z modułu 6?" |
| Dodaje ale pole się nie czyści | „Po dodaniu co trzeba zrobić z nowyTytul?" |
| Nie wie jak generować ID | „Date.now() daje unikalną liczbę — ms od 1970." |
| Usuwa ale deleteuje cały stan | „.filter() ZWRACA nową tablicę. setZadania(zadania.filter(...))" |
| Toggle nie działa | „To jest map+spread jak w galerii. Zmień pole zrobione na !z.zrobione" |
| Badge nie ma koloru | „Sprawdź klasy: bg-success, bg-warning text-dark, bg-danger" |
| Przekreślenie nie działa | „Warunkowa klasa: className={z.zrobione ? 'zrobione' : ''}" |
5. Po zbudowaniu
„Porównaj galerię z listą zadań. Co mają wspólnego, a co innego?"
Odp: Wspólne: tablica w useState, .map() w JSX, spread do zmiany. Nowe: dodawanie ([...arr, x]), usuwanie (.filter), controlled input/select, toggle.
„Dlaczego Date.now() a nie Math.random() albo po prostu zadania.length + 1?"
Odp: length+1 się powtarza po usunięciu. Math.random() teoretycznie OK ale brzydkie. Date.now() jest prosty i unikalny (ms od 1970).
„Jak byś dodał filtrowanie — pokaż tylko niezrobione?"
Odp: Dodatkowy useState(false) + .filter(z => !filtr || !z.zrobione) — jak switche w galerii!
#02 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Jeden komponent | App.jsx | ✅ |
| Nagłówek h1 | „Lista zadań" | ✅ |
| Formularz dodawania | input (text) + select (priorytet) + button „Dodaj" | ✅ |
| Controlled input | value={nowyTytul} + onChange | ✅ |
| Controlled select | value={nowyPriorytet} + onChange + Number() | ✅ |
| Dane początkowe | 4 obiekty {id, tytul, priorytet, zrobione} | ✅ |
| Tabela Bootstrap | table table-striped, 4 kolumny | ✅ |
| Badge priorytet | bg-success (niski), bg-warning (średni), bg-danger (wysoki) | ✅ |
| Dodawanie zadania | [...zadania, nowe] + Date.now() jako id | ✅ |
| Walidacja | Nie dodaje pustego (trim === '') | ✅ |
| Toggle zrobione | .map() + spread + !z.zrobione | ✅ |
| Usuwanie zadania | .filter(z => z.id !== id) | ✅ |
| CSS — przekreślenie | text-decoration: line-through + opacity: 0.5 | ✅ |
| Warunkowe klasy | className={z.zrobione ? 'zrobione' : ''} | ✅ |
| Licznik | „Zrobione: X z Y" — filter().length | ✅ |
| Znaczące nazwy | zadania, nowyTytul, dodaj, toggle, usun | ✅ |
#02 — Budowa krok po kroku
„Możemy użyć tego samego projektu Vite — wystarczy wyczyścić App.jsx i App.css."
# Jeśli nowy projekt:
npm create vite@latest todo -- --template react
cd todo
npm install
npm install bootstrap
npm run dev
„Jak w galerii — importy, dane poza komponentem, useState."
import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
import './App.css'
const poczatkowe = [
{ id: 1, tytul: 'Kupić mleko', priorytet: 1, zrobione: false },
{ id: 2, tytul: 'Zrobić projekt React', priorytet: 3, zrobione: false },
{ id: 3, tytul: 'Odpowiedzieć na maila', priorytet: 2, zrobione: true },
{ id: 4, tytul: 'Posprzątać biurko', priorytet: 1, zrobione: false },
]
function App() {
const [zadania, setZadania] = useState(poczatkowe)
return (
<div className="container mt-3">
<h1>Lista zadań</h1>
</div>
)
}
export default App
„Porównaj z galerią — widzisz ten sam wzorzec? Tablica poza komponentem, useState wewnątrz."
„Tu jest nowość — controlled input dla tekstu i select dla priorytetu. Pamiętasz formułkę? value={stan} + onChange."
Stan:
const [nowyTytul, setNowyTytul] = useState('')
const [nowyPriorytet, setNowyPriorytet] = useState(2)
JSX:
<div className="d-flex gap-2 mb-4">
<input
className="form-control"
type="text"
placeholder="Wpisz zadanie..."
value={nowyTytul}
onChange={(e) => setNowyTytul(e.target.value)}
/>
<select
className="form-select"
style={{ width: '180px' }}
value={nowyPriorytet}
onChange={(e) => setNowyPriorytet(Number(e.target.value))}
>
<option value={1}>Niski</option>
<option value={2}>Średni</option>
<option value={3}>Wysoki</option>
</select>
<button className="btn btn-primary">Dodaj</button>
</div>
„Dlaczego Number(e.target.value) w select?"
Odp: e.target.value ZAWSZE zwraca string. Priorytet to number → musimy skonwertować.
„Pamiętasz co to controlled input? Które dwa atrybuty musi mieć?"
Odp: value={stan} + onChange={handler}. React kontroluje pole.
„W galerii zmienialiśmy obiekt w tablicy. Teraz DODAJEMY nowy. Jak to zrobisz bez mutacji?"
„Masz tablicę [a, b, c]. Jak dodasz d na koniec bez push()?"
Odp: [...tablica, d] — spread + nowy element.
function dodaj() {
if (nowyTytul.trim() === '') return
const nowe = {
id: Date.now(),
tytul: nowyTytul,
priorytet: nowyPriorytet,
zrobione: false,
}
setZadania([...zadania, nowe])
setNowyTytul('')
}
Podepnij do przycisku:
<button className="btn btn-primary" onClick={dodaj}>Dodaj</button>
„Dlaczego po dodaniu robimy setNowyTytul('')?"
Odp: Czyścimy pole po dodaniu — lepszy UX.
„Dlaczego Date.now() jako id?"
Odp: Daje unikalną liczbę — milisekundy od 1970. Nie powtarza się.
Jeśli pisze zadania.push(nowe):
„push() mutuje tablicę. React tego nie zauważy! Trzeba stworzyć NOWĄ tablicę: [...zadania, nowe]"
„Teraz wyświetlamy zadania w tabeli Bootstrap. .map() jak w galerii — zamiast div z obrazem, robimy <tr> z kolumnami."
<table className="table table-striped">
<thead>
<tr>
<th>Zadanie</th>
<th>Priorytet</th>
<th>Status</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{zadania.map((z) => (
<tr key={z.id} className={z.zrobione ? 'zrobione' : ''}>
<td>{z.tytul}</td>
<td>
{z.priorytet === 1 && <span className="badge bg-success">niski</span>}
{z.priorytet === 2 && <span className="badge bg-warning text-dark">średni</span>}
{z.priorytet === 3 && <span className="badge bg-danger">wysoki</span>}
</td>
<td>{z.zrobione ? 'Zrobione' : 'Do zrobienia'}</td>
<td>
<button className="btn btn-success btn-sm me-1">✓</button>
<button className="btn btn-danger btn-sm">✕</button>
</td>
</tr>
))}
</tbody>
</table>
„Co robi className={z.zrobione ? 'zrobione' : ''}?"
Odp: Warunkowa klasa CSS — jeśli zrobione, dodaje klasę 'zrobione'. W CSS nadamy jej styl.
„Jak działają trzy warunki z badge? Co zwraca {z.priorytet === 1 && <span>}?"
Odp: Short-circuit: jeśli lewe jest true → renderuje prawe. Jeśli false → nic.
„Dwie funkcje. toggle() to ten sam wzorzec co pobierz() w galerii — .map() + spread. usun() to nowość — .filter()."
function toggle(id) {
setZadania(zadania.map((z) =>
z.id === id ? { ...z, zrobione: !z.zrobione } : z
))
}
function usun(id) {
setZadania(zadania.filter((z) => z.id !== id))
}
Podepnij do przycisków:
<button className="btn btn-success btn-sm me-1"
onClick={() => toggle(z.id)}>✓</button>
<button className="btn btn-danger btn-sm"
onClick={() => usun(z.id)}>✕</button>
„toggle() — rozpoznajesz wzorzec z galerii? Co jest inne?"
Odp: To samo: map + spread + ternary. Ale zamiast downloads+1 robimy !z.zrobione (negacja boolean).
„usun() — co zwraca .filter(z => z.id !== id)?"
Odp: Nową tablicę BEZ elementu o danym id. !== znaczy: zachowaj wszystkie OPRÓCZ tego.
Jeśli pisze splice/delete:
„splice() mutuje! .filter() zwraca NOWĄ tablicę — React to zobaczy."
„Ostatnie szlify — styl dla zrobionych zadań i licznik na dole."
App.css:
.zrobione td {
text-decoration: line-through;
opacity: 0.5;
}
Licznik (przed zamknięciem </div>):
<p className="mt-3 text-muted">
Zrobione: {zadania.filter((z) => z.zrobione).length} z {zadania.length}
</p>
„Jak działa ten licznik? Co robi .filter().length?"
Odp: filter zwraca tablicę tylko zrobionych. .length liczy ile ich jest. Computed value — przeliczane przy każdym renderze.
„Gotowe! Masz pełną aplikację Todo — dodawanie, usuwanie, toggle, filtrowanie, badge, tabela. Wszystkie operacje CRUD."
#02 — Gotowy kod
App.jsx
import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
import './App.css'
const poczatkowe = [
{ id: 1, tytul: 'Kupić mleko', priorytet: 1, zrobione: false },
{ id: 2, tytul: 'Zrobić projekt React', priorytet: 3, zrobione: false },
{ id: 3, tytul: 'Odpowiedzieć na maila', priorytet: 2, zrobione: true },
{ id: 4, tytul: 'Posprzątać biurko', priorytet: 1, zrobione: false },
]
function App() {
const [zadania, setZadania] = useState(poczatkowe)
const [nowyTytul, setNowyTytul] = useState('')
const [nowyPriorytet, setNowyPriorytet] = useState(2)
function dodaj() {
if (nowyTytul.trim() === '') return
const nowe = {
id: Date.now(),
tytul: nowyTytul,
priorytet: nowyPriorytet,
zrobione: false,
}
setZadania([...zadania, nowe])
setNowyTytul('')
}
function toggle(id) {
setZadania(zadania.map((z) =>
z.id === id ? { ...z, zrobione: !z.zrobione } : z
))
}
function usun(id) {
setZadania(zadania.filter((z) => z.id !== id))
}
return (
<div className="container mt-3">
<h1>Lista zadań</h1>
<div className="d-flex gap-2 mb-4">
<input className="form-control" type="text" placeholder="Wpisz zadanie..."
value={nowyTytul} onChange={(e) => setNowyTytul(e.target.value)} />
<select className="form-select" style={{ width: '180px' }}
value={nowyPriorytet} onChange={(e) => setNowyPriorytet(Number(e.target.value))}>
<option value={1}>Niski</option>
<option value={2}>Średni</option>
<option value={3}>Wysoki</option>
</select>
<button className="btn btn-primary" onClick={dodaj}>Dodaj</button>
</div>
<table className="table table-striped">
<thead>
<tr><th>Zadanie</th><th>Priorytet</th><th>Status</th><th>Akcje</th></tr>
</thead>
<tbody>
{zadania.map((z) => (
<tr key={z.id} className={z.zrobione ? 'zrobione' : ''}>
<td>{z.tytul}</td>
<td>
{z.priorytet === 1 && <span className="badge bg-success">niski</span>}
{z.priorytet === 2 && <span className="badge bg-warning text-dark">średni</span>}
{z.priorytet === 3 && <span className="badge bg-danger">wysoki</span>}
</td>
<td>{z.zrobione ? 'Zrobione' : 'Do zrobienia'}</td>
<td>
<button className="btn btn-success btn-sm me-1" onClick={() => toggle(z.id)}>✓</button>
<button className="btn btn-danger btn-sm" onClick={() => usun(z.id)}>✕</button>
</td>
</tr>
))}
</tbody>
</table>
<p className="mt-3 text-muted">
Zrobione: {zadania.filter((z) => z.zrobione).length} z {zadania.length}
</p>
</div>
)
}
export default App
App.css
.zrobione td {
text-decoration: line-through;
opacity: 0.5;
}
📝 Sprawdzian — Lista zadań
5 pytań · 5 minut · minimum 60% żeby zdać
useState("")?e.preventDefault()?id z tablicy w stanie?🔥 Rozgrzewka — Tracker wydatków
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
useEffect i do czego służy?useEffect to hook pozwalający wykonać „efekt uboczny" po wyrenderowaniu komponentu — np. pobranie danych, zapis do localStorage czy ustawienie timera. Przyjmuje dwa argumenty: funkcję do wykonania oraz tablicę zależności, która kontroluje, kiedy efekt się uruchomi.localStorage w przeglądarce?localStorage pozwala zapisywać dane w przeglądarce, które przetrwają odświeżenie strony. Zapisujemy przez localStorage.setItem("klucz", JSON.stringify(dane)), a odczytujemy przez JSON.parse(localStorage.getItem("klucz")). Przechowuje tylko stringi, dlatego obiekty i tablice musimy serializować przez JSON..filter() i jak jej użyć do usunięcia elementu?.filter() tworzy nową tablicę zawierającą tylko elementy spełniające warunek. Aby usunąć element o danym id: setWydatki(wydatki.filter(w => w.id !== idDoUsuniecia)). Oryginalna tablica nie jest modyfikowana — to ważne w React..reduce() i jak obliczyć sumę wartości?.reduce() „redukuje" tablicę do pojedynczej wartości, przetwarzając elementy po kolei. Aby zsumować kwoty: wydatki.reduce((suma, w) => suma + w.kwota, 0). Drugi argument (0) to wartość początkowa akumulatora.useEffect uruchomił się tylko raz, przy pierwszym renderze?useEffect(() => { ... }, []). Pusta tablica oznacza, że efekt nie zależy od żadnej wartości, więc wykona się tylko po pierwszym wyrenderowaniu komponentu — idealnie do wczytania danych z localStorage.#03 — Tracker Wydatków (Expense Tracker)
Ćwiczenie autorskie w stylu egzaminacyjnym — uczy nowych koncepcji brakujących w #01 i #02.
Nowe umiejętności (vs #01 + #02)
Treść zadania (styl egzaminacyjny)
Z zastosowaniem biblioteki React.js wykonaj aplikację internetową typu front-end realizującą funkcję trackera wydatków osobistych.
Założenia aplikacji
- Aplikacja składa się z jednego komponentu
- Dane początkowe — tablica 5 obiektów (poniżej), każdy zawiera:
- id (unikalna liczba)
- nazwa (opis wydatku)
- kwota (liczba zmiennoprzecinkowa, np. 127.50)
- kategoria (string: „jedzenie" / „transport" / „rozrywka" / „edukacja")
const poczatkowe = [
{ id: 1, nazwa: 'Zakupy spożywcze', kwota: 127.50, kategoria: 'jedzenie' },
{ id: 2, nazwa: 'Bilet miesięczny', kwota: 99.00, kategoria: 'transport' },
{ id: 3, nazwa: 'Netflix', kwota: 43.00, kategoria: 'rozrywka' },
{ id: 4, nazwa: 'Obiad w restauracji', kwota: 65.00, kategoria: 'jedzenie' },
{ id: 5, nazwa: 'Książka React', kwota: 89.00, kategoria: 'edukacja' },
]
Komponent składa się z
- Nagłówka h1: „Tracker wydatków"
- Karty podsumowania (
Bootstrap card): łączna suma wydatków, liczba pozycji, średnia kwota - Pola wyszukiwania: input tekstowy filtrujący wydatki po nazwie (case-insensitive)
- Formularza dodawania: pole tekstowe (nazwa), pole liczbowe (kwota, type="number"), select (kategoria), przycisk „Dodaj"
- Tabeli Bootstrap (
table table-hover) z kolumnami: Nazwa, Kwota, Kategoria, Akcje - W kolumnie Kategoria: badge Bootstrap (zielony=jedzenie, niebieski=transport, fioletowy=rozrywka, żółty=edukacja)
- W kolumnie Akcje: przycisk „Usuń" (btn-danger)
- Alert Bootstrap (warning): wyświetlany gdy suma wydatków przekroczy 1000 zł
Stylowanie
- Karta podsumowania: Bootstrap
card, wyśrodkowane wartości - Kwoty wyświetlane z 2 miejscami po przecinku i dopiskiem „zł"
- Formularz, tabela, badge, przyciski, alert: stylowane Bootstrap
Logika
- Dodaj: wpisz nazwę + kwotę + wybierz kategorię → kliknij „Dodaj" → nowy wydatek, pola się czyszczą
- Nie dodawaj pustej nazwy ani kwoty ≤ 0 (walidacja)
- „Usuń" — usuwa wydatek z tablicy
- Wyszukiwarka filtruje po nazwie (case-insensitive,
.toLowerCase().includes()) - Suma:
.reduce()— oblicza łączną kwotę, liczbę pozycji, średnią - Alert: jeśli suma > 1000 → Bootstrap alert-warning
- Znaczące nazwy zmiennych i funkcji
Bootstrap — przykłady klas
<div class="card">
<div class="card-body">
<h5 class="card-title">...</h5>
<p class="card-text">...</p>
</div>
</div>
<div class="alert alert-warning">Uwaga!</div>
<input class="form-control" type="number" step="0.01">
<span class="badge bg-success">jedzenie</span>
Dlaczego to zadanie? #01 uczyła filtrowania + zmiany pola, #02 uczyła CRUD (dodawanie/usuwanie/toggle). #03 uczy OBLICZEŃ na tablicy (.reduce), wyszukiwarki tekstowej, number input i warunkowego renderowania alertu. Po trzech zadaniach uczeń pokrywa 100% wzorców potrzebnych na egzaminie.
#03 — Wprowadzenie dla nauczyciela
1. Otwarcie
„W galerii robiliśmy filtrowanie i zmianę pola. W liście zadań — dodawanie, usuwanie, toggle. Dziś robimy tracker wydatków, który uczy OBLICZEŃ na tablicy — sumowania, średniej — i wyszukiwarki tekstowej. To ostatni brakujący klocek."
2. Diagnoza — co pamięta z #01 i #02
„Szybkie powtórzenie. Jak dodajesz element do tablicy w React bez mutacji?"
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „[...zadania, nowy]" | „Idealnie — dzisiaj użyjemy tego samego." |
| „setZadania coś tam..." | „Tak, a wewnątrz: spread + nowy element. Powtórzymy." |
| Nie pamięta | „Ok, wrócimy do tego. To ten sam wzorzec co w Todo." |
„A jak usuwałeś element?"
Odp: .filter(z => z.id !== id)
3. Architektura
Pokaż schemat:
App:
state: wydatki → tablica obiektów {id, nazwa, kwota, kategoria}
state: nowaNazwa → string (controlled input)
state: nowaKwota → string (controlled input type number)
state: nowaKategoria → string (controlled select)
state: szukaj → string (wyszukiwarka)
computed: widoczne = wydatki.filter(w => w.nazwa.toLowerCase().includes(...))
computed: suma = wydatki.reduce((s, w) => s + w.kwota, 0) ← NOWE!
computed: srednia = suma / wydatki.length
dodaj() → [...wydatki, nowy] + parseFloat(nowaKwota)
usun(id) → wydatki.filter(...)
JSX: h1 + card(suma) + wyszukiwarka + formularz + tabela + alert(suma>1000)
„Ile useState tym razem?"
Odp: 5 — wydatki (tablica), nowaNazwa (string), nowaKwota (string), nowaKategoria (string), szukaj (string).
„Znasz z JS metodę, która przerabia całą tablicę w JEDNĄ wartość? Np. sumuje wszystkie kwoty?"
Odp: .reduce() — to nowość. Jeśli nie zna, wytłumacz na następnym kroku.
4. Mini-lekcja: .reduce()
„.reduce() bierze tablicę i 'redukuje' ją do jednej wartości. Np. sumuje."
// Pseudokod:
[10, 20, 30].reduce((suma, el) => suma + el, 0)
// Krok 1: suma=0, el=10 → return 10
// Krok 2: suma=10, el=20 → return 30
// Krok 3: suma=30, el=30 → return 60
// Wynik: 60
„Co robi to '0' na końcu?"
Odp: Wartość startowa akumulatora. Zaczynamy sumę od 0.
5. Częste blokady
| Problem | Podpowiedź |
|---|---|
| Nie zna .reduce() | „Pokaż na prostym przykładzie: [1,2,3].reduce((s,n)=>s+n, 0)" |
| Kwota jest stringiem | „input type='number' zwraca string! parseFloat() zamienia na liczbę." |
| .toFixed() nie działa | „.toFixed(2) działa tylko na number, nie string." |
| Wyszukiwarka case-sensitive | „.toLowerCase() na obu stronach — szukanym i porównywanym." |
| Alert zawsze widoczny | „Warunkowe renderowanie: {suma > 1000 && <div...>}" |
| NaN przy dodawaniu | „parseFloat('') daje NaN. Dlatego walidacja: if (!nowaKwota || ...)" |
| Dzielenie przez 0 | „Gdy tablica pusta, wydatki.length === 0. Sprawdź przed dzieleniem!" |
6. Po zbudowaniu
„Porównaj trzy zadania: galeria, todo, tracker. Co mają wspólnego, a co nowego?"
Odp: Wspólne: tablica + .map() + .filter(). Galeria: zmiana pola (switch+downloads). Todo: dodawanie/usuwanie/toggle. Tracker: reduce (obliczenia), wyszukiwarka, number input, warunkowy alert.
„Gdybyś chciał dodać sortowanie — np. od najdroższego — jak?"
Odp: [...widoczne].sort((a,b) => b.kwota - a.kwota) — sortuj kopię, nie oryginał.
#03 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Jeden komponent | App.jsx | ✅ |
| Nagłówek h1 | „Tracker wydatków" | ✅ |
| Dane początkowe | 5 obiektów {id, nazwa, kwota, kategoria} | ✅ |
| Formularz dodawania | input text + input number + select + button | ✅ |
| Controlled input (text) | value={nowaNazwa} + onChange | ✅ |
| Controlled input (number) | value={nowaKwota} + onChange + parseFloat() | ✅ |
| Controlled select | value={nowaKategoria} + onChange | ✅ |
| Walidacja | Nie dodaje pustej nazwy ani kwoty ≤ 0 | ✅ |
| Wyszukiwarka | .toLowerCase().includes() — case-insensitive | ✅ |
| Tabela Bootstrap | table table-hover, 4 kolumny | ✅ |
| Badge kategoria | 4 kolory — jedzenie/transport/rozrywka/edukacja | ✅ |
| Usuwanie | .filter(w => w.id !== id) | ✅ |
| Karta podsumowania | Bootstrap card — suma, ilość, średnia | ✅ |
| .reduce() suma | wydatki.reduce((s, w) => s + w.kwota, 0) | ✅ |
| .toFixed(2) | Kwoty z 2 miejscami po przecinku + „zł" | ✅ |
| Alert warunkowy | suma > 1000 → alert-warning | ✅ |
| Znaczące nazwy | wydatki, nowaNazwa, dodaj, usun, szukaj, suma | ✅ |
#03 — Budowa krok po kroku
„Możemy użyć tego samego projektu — wyczyść App.jsx i App.css."
# Jeśli nowy:
npm create vite@latest tracker -- --template react
cd tracker
npm install
npm install bootstrap
npm run dev
„Ten sam schemat co galeria i todo — importy, dane poza komponentem, useState."
import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
import './App.css'
const poczatkowe = [
{ id: 1, nazwa: 'Zakupy spożywcze', kwota: 127.50, kategoria: 'jedzenie' },
{ id: 2, nazwa: 'Bilet miesięczny', kwota: 99.00, kategoria: 'transport' },
{ id: 3, nazwa: 'Netflix', kwota: 43.00, kategoria: 'rozrywka' },
{ id: 4, nazwa: 'Obiad w restauracji', kwota: 65.00, kategoria: 'jedzenie' },
{ id: 5, nazwa: 'Książka React', kwota: 89.00, kategoria: 'edukacja' },
]
function App() {
const [wydatki, setWydatki] = useState(poczatkowe)
return (
<div className="container mt-3">
<h1>Tracker wydatków</h1>
</div>
)
}
export default App
„Widzisz wzorzec? Trzeci raz z rzędu — dane na zewnątrz, useState w środku. Dlaczego zawsze tak robimy?"
Odp: Dane początkowe się nie zmieniają — nie muszą być w komponencie. useState kopiuje je jako punkt startowy.
„Formularz jak w Todo, ale teraz mamy trzy pola — text, number i select. I nowość: input type='number'."
Stan:
const [nowaNazwa, setNowaNazwa] = useState('')
const [nowaKwota, setNowaKwota] = useState('')
const [nowaKategoria, setNowaKategoria] = useState('jedzenie')
JSX:
<div className="d-flex gap-2 mb-4">
<input className="form-control" type="text" placeholder="Nazwa wydatku..."
value={nowaNazwa} onChange={(e) => setNowaNazwa(e.target.value)} />
<input className="form-control" type="number" step="0.01" placeholder="Kwota"
style={{ width: '150px' }}
value={nowaKwota} onChange={(e) => setNowaKwota(e.target.value)} />
<select className="form-select" style={{ width: '180px' }}
value={nowaKategoria} onChange={(e) => setNowaKategoria(e.target.value)}>
<option value="jedzenie">Jedzenie</option>
<option value="transport">Transport</option>
<option value="rozrywka">Rozrywka</option>
<option value="edukacja">Edukacja</option>
</select>
<button className="btn btn-primary">Dodaj</button>
</div>
„Dlaczego nowaKwota to string ('') a nie number (0)?"
Odp: Bo controlled input z type='number' nadal operuje na stringach w value. Konwertujemy na number przy dodawaniu (parseFloat).
„Co robi step='0.01' w input number?"
Odp: Pozwala wpisywać grosze — 127.50 zamiast tylko 127.
„Wzorzec znany z Todo. Ale tu mamy dwie walidacje i konwersję stringa na liczbę."
function dodaj() {
if (nowaNazwa.trim() === '') return
if (!nowaKwota || parseFloat(nowaKwota) <= 0) return
const nowy = {
id: Date.now(),
nazwa: nowaNazwa,
kwota: parseFloat(nowaKwota),
kategoria: nowaKategoria,
}
setWydatki([...wydatki, nowy])
setNowaNazwa('')
setNowaKwota('')
}
Podepnij do przycisku:
<button className="btn btn-primary" onClick={dodaj}>Dodaj</button>
„Dlaczego parseFloat() a nie Number()?"
Odp: Oba działają. parseFloat jest bardziej tolerancyjny — ignoruje znaki na końcu. Ale Number() też ok.
Jeśli zapomni konwersji:
„Dodaj wydatek 50 + 30. Ile wyszło? '5030'? To nie suma — to konkatenacja stringów. parseFloat()!"
„Tabela jak w Todo — .map() na tablicy, badge jak z priorytetami."
Funkcja usun (znana z Todo):
function usun(id) {
setWydatki(wydatki.filter((w) => w.id !== id))
}
Helper dla kolorów badge:
function kolorBadge(kat) {
if (kat === 'jedzenie') return 'bg-success'
if (kat === 'transport') return 'bg-primary'
if (kat === 'rozrywka') return 'bg-purple'
if (kat === 'edukacja') return 'bg-warning text-dark'
return 'bg-secondary'
}
JSX tabeli:
<table className="table table-hover">
<thead>
<tr><th>Nazwa</th><th>Kwota</th><th>Kategoria</th><th>Akcje</th></tr>
</thead>
<tbody>
{wydatki.map((w) => (
<tr key={w.id}>
<td>{w.nazwa}</td>
<td>{w.kwota.toFixed(2)} zł</td>
<td><span className={`badge ${kolorBadge(w.kategoria)}`}>{w.kategoria}</span></td>
<td><button className="btn btn-danger btn-sm" onClick={() => usun(w.id)}>Usuń</button></td>
</tr>
))}
</tbody>
</table>
„Co robi .toFixed(2)?"
Odp: Formatuje liczbę do 2 miejsc po przecinku. 99 → „99.00", 127.5 → „127.50".
„Template literal w className: \`badge \${kolorBadge(w.kategoria)}\`. Dlaczego backticki?"
Odp: Łączymy stały tekst „badge " ze zmienną wartością. Template literal = backticki + ${wyrażenie}.
„Nowość: filtrowanie po tekście. W galerii filtrowaliśmy po kategorii (number). Tu filtrujemy po nazwie — string."
Stan:
const [szukaj, setSzukaj] = useState('')
Filtr:
const widoczne = wydatki.filter((w) =>
w.nazwa.toLowerCase().includes(szukaj.toLowerCase())
)
JSX (nad formularzem):
<input className="form-control mb-3" type="text" placeholder="🔍 Szukaj wydatku..."
value={szukaj} onChange={(e) => setSzukaj(e.target.value)} />
Zmień .map() w tabeli: zamień wydatki.map na widoczne.map
„Dlaczego .toLowerCase() na OBU stronach?"
Odp: „Netflix" vs „netflix". Bez toLowerCase() szukanie „net" nie znajdzie „Netflix". Porównujemy małe z małymi.
„Co robi .includes()?"
Odp: Sprawdza czy string zawiera podciąg. 'netflix'.includes('net') → true. 'netflix'.includes('xyz') → false.
Jeśli szukanie nie działa:
„Wypisz w konsoli co porównujesz: console.log(w.nazwa.toLowerCase(), szukaj.toLowerCase())"
„Najtrudniejszy krok — .reduce(). Bierze tablicę i zwraca JEDNĄ wartość. Np. sumę."
„Analogia: masz stos paragonów. Bierzesz kalkulator (zaczynasz od 0), dodajesz kwotę z każdego paragonu, na końcu masz sumę."
const suma = wydatki.reduce((s, w) => s + w.kwota, 0)
const srednia = wydatki.length > 0 ? suma / wydatki.length : 0
JSX (pod h1, nad wyszukiwarką):
<div className="card mb-4">
<div className="card-body d-flex justify-content-around text-center">
<div>
<h5 className="card-title">Suma</h5>
<p className="card-text fs-4">{suma.toFixed(2)} zł</p>
</div>
<div>
<h5 className="card-title">Pozycji</h5>
<p className="card-text fs-4">{wydatki.length}</p>
</div>
<div>
<h5 className="card-title">Średnia</h5>
<p className="card-text fs-4">{srednia.toFixed(2)} zł</p>
</div>
</div>
</div>
„Wytłumacz mi co robi: wydatki.reduce((s, w) => s + w.kwota, 0)"
| Krok | s (akumulator) | w.kwota | wynik |
|---|---|---|---|
| start | 0 | — | — |
| 1 | 0 | 127.50 | 127.50 |
| 2 | 127.50 | 99.00 | 226.50 |
| 3 | 226.50 | 43.00 | 269.50 |
| 4 | 269.50 | 65.00 | 334.50 |
| 5 | 334.50 | 89.00 | 423.50 |
„Dlaczego wydatki.length > 0 przy średniej?"
Odp: Dzielenie przez 0! Jeśli tablica pusta → NaN. Zabezpieczamy.
„Warunkowe renderowanie — alert pojawia się TYLKO gdy suma > 1000."
{suma > 1000 && (
<div className="alert alert-warning">
⚠️ Uwaga! Przekroczono budżet 1000 zł — suma: {suma.toFixed(2)} zł
</div>
)}
App.css:
.bg-purple {
background-color: #6f42c1;
color: white;
}
„Jak działa {suma > 1000 && <div>...</div>}?"
Odp: Short-circuit: jeśli lewe jest false → nic się nie renderuje. Jeśli true → renderuje prawe. Ten sam wzorzec co badge w Todo.
„Przetestujmy! Dodaj parę drogich wydatków — jak suma przekroczy 1000 → alert się pojawi."
#03 — Gotowy kod
App.jsx
import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
import './App.css'
const poczatkowe = [
{ id: 1, nazwa: 'Zakupy spożywcze', kwota: 127.50, kategoria: 'jedzenie' },
{ id: 2, nazwa: 'Bilet miesięczny', kwota: 99.00, kategoria: 'transport' },
{ id: 3, nazwa: 'Netflix', kwota: 43.00, kategoria: 'rozrywka' },
{ id: 4, nazwa: 'Obiad w restauracji', kwota: 65.00, kategoria: 'jedzenie' },
{ id: 5, nazwa: 'Książka React', kwota: 89.00, kategoria: 'edukacja' },
]
function App() {
const [wydatki, setWydatki] = useState(poczatkowe)
const [nowaNazwa, setNowaNazwa] = useState('')
const [nowaKwota, setNowaKwota] = useState('')
const [nowaKategoria, setNowaKategoria] = useState('jedzenie')
const [szukaj, setSzukaj] = useState('')
const widoczne = wydatki.filter((w) =>
w.nazwa.toLowerCase().includes(szukaj.toLowerCase())
)
const suma = wydatki.reduce((s, w) => s + w.kwota, 0)
const srednia = wydatki.length > 0 ? suma / wydatki.length : 0
function dodaj() {
if (nowaNazwa.trim() === '') return
if (!nowaKwota || parseFloat(nowaKwota) <= 0) return
const nowy = {
id: Date.now(),
nazwa: nowaNazwa,
kwota: parseFloat(nowaKwota),
kategoria: nowaKategoria,
}
setWydatki([...wydatki, nowy])
setNowaNazwa('')
setNowaKwota('')
}
function usun(id) {
setWydatki(wydatki.filter((w) => w.id !== id))
}
function kolorBadge(kat) {
if (kat === 'jedzenie') return 'bg-success'
if (kat === 'transport') return 'bg-primary'
if (kat === 'rozrywka') return 'bg-purple'
if (kat === 'edukacja') return 'bg-warning text-dark'
return 'bg-secondary'
}
return (
<div className="container mt-3">
<h1>Tracker wydatków</h1>
<div className="card mb-4">
<div className="card-body d-flex justify-content-around text-center">
<div>
<h5 className="card-title">Suma</h5>
<p className="card-text fs-4">{suma.toFixed(2)} zł</p>
</div>
<div>
<h5 className="card-title">Pozycji</h5>
<p className="card-text fs-4">{wydatki.length}</p>
</div>
<div>
<h5 className="card-title">Średnia</h5>
<p className="card-text fs-4">{srednia.toFixed(2)} zł</p>
</div>
</div>
</div>
{suma > 1000 && (
<div className="alert alert-warning">
⚠️ Uwaga! Przekroczono budżet 1000 zł — suma: {suma.toFixed(2)} zł
</div>
)}
<input className="form-control mb-3" type="text" placeholder="🔍 Szukaj wydatku..."
value={szukaj} onChange={(e) => setSzukaj(e.target.value)} />
<div className="d-flex gap-2 mb-4">
<input className="form-control" type="text" placeholder="Nazwa wydatku..."
value={nowaNazwa} onChange={(e) => setNowaNazwa(e.target.value)} />
<input className="form-control" type="number" step="0.01" placeholder="Kwota"
style={{ width: '150px' }}
value={nowaKwota} onChange={(e) => setNowaKwota(e.target.value)} />
<select className="form-select" style={{ width: '180px' }}
value={nowaKategoria} onChange={(e) => setNowaKategoria(e.target.value)}>
<option value="jedzenie">Jedzenie</option>
<option value="transport">Transport</option>
<option value="rozrywka">Rozrywka</option>
<option value="edukacja">Edukacja</option>
</select>
<button className="btn btn-primary" onClick={dodaj}>Dodaj</button>
</div>
<table className="table table-hover">
<thead>
<tr><th>Nazwa</th><th>Kwota</th><th>Kategoria</th><th>Akcje</th></tr>
</thead>
<tbody>
{widoczne.map((w) => (
<tr key={w.id}>
<td>{w.nazwa}</td>
<td>{w.kwota.toFixed(2)} zł</td>
<td><span className={`badge ${kolorBadge(w.kategoria)}`}>{w.kategoria}</span></td>
<td><button className="btn btn-danger btn-sm" onClick={() => usun(w.id)}>Usuń</button></td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default App
App.css
.bg-purple {
background-color: #6f42c1;
color: white;
}
📝 Sprawdzian — Tracker wydatków
5 pytań · 5 minut · minimum 60% żeby zdać
[] jako drugi argument useEffect?localStorage?.reduce((suma, el) => suma + el.kwota, 0) na tablicy wydatków?useEffect z zależnością [wydatki]?🔥 Rozgrzewka — Dziennik ocen
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
.find() na tablicy?.find() zwraca pierwszy element tablicy spełniający podany warunek — lub undefined, jeśli takiego nie ma. Przykład: uczniowie.find(u => u.id === szukaneId). W odróżnieniu od .filter(), zwraca pojedynczy obiekt, a nie tablicę.Set?new Set(tablica) tworzy zbiór unikalnych wartości. Aby uzyskać z powrotem tablicę: [...new Set(przedmioty)]. Przydaje się np. do wyciągnięcia listy unikalnych przedmiotów z tablicy ocen: [...new Set(oceny.map(o => o.przedmiot))]..sort() z funkcją porównującą: uczniowie.sort((a, b) => a.nazwisko.localeCompare(b.nazwisko)). Uwaga: .sort() modyfikuje oryginalną tablicę, więc w React najpierw tworzymy kopię: [...uczniowie].sort(...)..map() — tworzymy nową tablicę, w której zamieniamy tylko wybrany element: setOceny(oceny.map(o => o.id === edytowanaId ? { ...o, wartosc: nowaWartosc } : o)). Spread { ...o } kopiuje obiekt, a potem nadpisujemy zmienione pole..map() od .filter()?.map() przekształca każdy element tablicy i zwraca tablicę tej samej długości z nowymi wartościami. .filter() wybiera tylko elementy spełniające warunek — zwraca tablicę o tej samej lub mniejszej długości, bez zmieniania samych elementów. Często łączymy je w łańcuch: tablica.filter(...).map(...).#04 — Dziennik ocen
📄 Instrukcja egzaminacyjna
Treść zadania
Napisz aplikację Dziennik ocen w React + Bootstrap. Aplikacja wyświetla listę ocen w tabeli, pozwala dodawać nowe, edytować istniejące, usuwać, sortować po kolumnach i filtrować po przedmiocie.
Dane początkowe
const poczatkowe = [
{ id: 1, uczen: 'Anna Kowalska', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-15' },
{ id: 2, uczen: 'Jan Nowak', przedmiot: 'Fizyka', ocena: 2, data: '2025-01-16' },
{ id: 3, uczen: 'Anna Kowalska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-17' },
{ id: 4, uczen: 'Marta Wiśniewska', przedmiot: 'Matematyka', ocena: 3, data: '2025-01-18' },
{ id: 5, uczen: 'Jan Nowak', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-20' },
{ id: 6, uczen: 'Marta Wiśniewska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-22' },
]
Wymagania funkcjonalne
- Wyświetlanie ocen w tabeli z kolumnami: Uczeń, Przedmiot, Ocena (badge kolorowy), Data, Akcje
- Dodawanie nowej oceny — formularz: imię ucznia (text), przedmiot (text), ocena (select 1-6)
- Edycja istniejącej oceny — kliknięcie ✎ wypełnia formularz, przycisk zmienia tekst na „Zapisz"
- Usuwanie oceny przyciskiem ✕
- Sortowanie — kliknięcie w nagłówek kolumny sortuje rosnąco/malejąco (▲/▼)
- Filtrowanie po przedmiocie — select z dynamicznie generowanymi opcjami
- Karta podsumowania — liczba ocen, średnia (.toFixed(2)), liczba przedmiotów
- Alert ostrzegawczy gdy średnia < 3.0
Przykłady Bootstrap do użycia
| Element | Klasy Bootstrap |
|---|---|
| Tabela | table table-striped |
| Badge oceny | badge bg-danger / bg-warning / bg-success / bg-primary |
| Karta podsumowania | card, card-body, card-title |
| Alert | alert alert-danger |
| Przycisk dodaj | btn btn-primary |
| Przycisk zapisz (edycja) | btn btn-warning |
| Przycisk anuluj | btn btn-secondary |
#04 — Wprowadzenie do lekcji
Diagnoza — co już umiesz z #01–#03
| Umiejętność | Skąd znasz |
|---|---|
| useState + tablica obiektów | #01, #02, #03 |
| .map() → wyświetlanie listy | #01, #02, #03 |
| .filter() → usuwanie / filtrowanie | #02, #03 |
| spread [...arr, nowy] | #02, #03 |
| .reduce() → obliczanie sumy | #03 |
| Bootstrap tabela + badge | #02, #03 |
| Date.now() jako ID | #02, #03 |
Co nowego w #04
.find() i trybu edycji.| Nowa umiejętność | Do czego |
|---|---|
.find() | Znajduje obiekt w tablicy po id — żeby wypełnić formularz edycji |
edytowanyId state | Przechowuje id edytowanego elementu (null = tryb dodawania) |
.sort() | Sortowanie tablicy — klikalne nagłówki kolumn |
[...new Set()] | Wyciąga unikalne wartości (np. listę przedmiotów) |
new Date().toISOString().slice(0,10) | Generuje dzisiejszą datę w formacie YYYY-MM-DD |
| Warunkowy tekst przycisku | Ten sam przycisk mówi „Dodaj" lub „Zapisz" zależnie od trybu |
Mini-lekcja: .find()
const oceny = [
{ id: 1, uczen: 'Anna', ocena: 4 },
{ id: 2, uczen: 'Jan', ocena: 5 },
]
const znaleziony = oceny.find(o => o.id === 2)
// { id: 2, uczen: 'Jan', ocena: 5 }
.find() zwraca pierwszy element spełniający warunek. Jak nie znajdzie — zwraca undefined.Mini-lekcja: .sort()
const liczby = [3, 1, 4, 1, 5]
// ⚠️ .sort() MUTUJE oryginalną tablicę!
// Dlatego zawsze robimy kopię: [...tablica].sort()
const rosnaco = [...liczby].sort((a, b) => a - b) // [1, 1, 3, 4, 5]
const malejaco = [...liczby].sort((a, b) => b - a) // [5, 4, 3, 1, 1]
// Sortowanie stringów:
const imiona = ['Jan', 'Anna', 'Marta']
const posortowane = [...imiona].sort((a, b) => {
if (a < b) return -1
if (a > b) return 1
return 0
})
[...arr].sort() — kopia. React potrzebuje nowej referencji żeby przerendrować.Mini-lekcja: [...new Set()]
const przedmioty = ['Matma', 'Fizyka', 'Matma', 'Informatyka', 'Fizyka']
const unikalne = [...new Set(przedmioty)]
// ['Matma', 'Fizyka', 'Informatyka']
new Set(tablica) tworzy zbiór (bez duplikatów). [...set] zamienia go z powrotem na tablicę.Architektura aplikacji
App
├── state: oceny[], nowyUczen, nowyPrzedmiot, nowaOcena, edytowanyId
├── state: sortKolumna, sortRosnaco, filtrPrzedmiot
├── computed: przedmioty = [...new Set(oceny.map(...))]
├── computed: przefiltrowane = oceny.filter(...)
├── computed: posortowane = [...przefiltrowane].sort(...)
├── computed: srednia = oceny.reduce(...) / oceny.length
├── dodaj() → add NEW or SAVE EDIT (based on edytowanyId)
├── edytuj(id) → .find() → fill form → set edytowanyId
├── usun(id) → .filter() → clear edit if needed
├── zmienSort() → toggle direction or change column
└── kolorOceny()→ badge class by grade
1. Czym się różni
.find() od .filter()?2. Dlaczego
.sort() wymaga kopii tablicy [...arr]?3. Co robi
new Set()?4. Jak byś zrobił, żeby ten sam formularz służył do dodawania I edycji?
#04 — Lista wymagań
| # | Wymaganie | Krok | ✓ |
|---|---|---|---|
| 1 | Projekt Vite + Bootstrap działa | 0 | ☐ |
| 2 | Dane początkowe (6 obiektów) zdefiniowane poza komponentem | 1 | ☐ |
| 3 | useState dla oceny[], nowyUczen, nowyPrzedmiot, nowaOcena | 1 | ☐ |
| 4 | Formularz — 2× input text + select 1-6 + przycisk | 2 | ☐ |
| 5 | Controlled inputs — value + onChange | 2 | ☐ |
| 6 | dodaj() — walidacja pustych pól | 3 | ☐ |
| 7 | dodaj() — nowy obiekt z Date.now() id + toISOString date | 3 | ☐ |
| 8 | dodaj() — spread [...oceny, nowy] + czyszczenie formularza | 3 | ☐ |
| 9 | Tabela z .map() — uczeń, przedmiot, ocena (badge), data, akcje | 4 | ☐ |
| 10 | kolorOceny() — badge czerwony/żółty/zielony/niebieski | 4 | ☐ |
| 11 | usun(id) — .filter() + clear edit state | 4 | ☐ |
| 12 | edytuj(id) — .find() wypełnia formularz | 5 | ☐ |
| 13 | edytowanyId state — null = dodawanie, id = edycja | 5 | ☐ |
| 14 | dodaj() obsługuje oba tryby (add / save edit) | 5 | ☐ |
| 15 | Przycisk „Dodaj" / „Zapisz" + „Anuluj" w trybie edycji | 5 | ☐ |
| 16 | zmienSort() — sortKolumna + sortRosnaco state | 6 | ☐ |
| 17 | Klikalne nagłówki tabeli z ▲/▼ | 6 | ☐ |
| 18 | [...przefiltrowane].sort() — kopia! | 6 | ☐ |
| 19 | Filtr po przedmiocie — dynamiczny select z [...new Set()] | 7 | ☐ |
| 20 | Karta podsumowania — ilość, średnia toFixed(2), ile przedmiotów | 7 | ☐ |
| 21 | Alert ostrzegawczy gdy średnia < 3.0 | 7 | ☐ |
#04 — Budowa krok po kroku
npm create vite@latest dziennik -- --template react
cd dziennik
npm install
npm install bootstrap
src/App.jsx, kasujesz domyślną zawartość i importujesz Bootstrap:import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
function App() {
return (
<div className="container mt-3">
<h1>Dziennik ocen</h1>
<p>Zaraz tu będzie apka!</p>
</div>
)
}
export default App
npm run dev?const poczatkowe = [
{ id: 1, uczen: 'Anna Kowalska', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-15' },
{ id: 2, uczen: 'Jan Nowak', przedmiot: 'Fizyka', ocena: 2, data: '2025-01-16' },
{ id: 3, uczen: 'Anna Kowalska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-17' },
{ id: 4, uczen: 'Marta Wiśniewska', przedmiot: 'Matematyka', ocena: 3, data: '2025-01-18' },
{ id: 5, uczen: 'Jan Nowak', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-20' },
{ id: 6, uczen: 'Marta Wiśniewska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-22' },
]
App(). Zauważ — tym razem mamy DWA rodzaje state: dane (oceny, formularz) i UI (sortowanie, filtr, edycja).function App() {
// Dane
const [oceny, setOceny] = useState(poczatkowe)
const [nowyUczen, setNowyUczen] = useState('')
const [nowyPrzedmiot, setNowyPrzedmiot] = useState('')
const [nowaOcena, setNowaOcena] = useState(3)
// Na razie return ze szkieletem
return (
<div className="container mt-3">
<h1>Dziennik ocen</h1>
<p>Ocen: {oceny.length}</p>
</div>
)
}
nowaOcena inicjalizujemy na 3, a nie na ''?d-flex gap-2.<div className="d-flex gap-2 mb-3">
<input className="form-control" type="text" placeholder="Uczeń..."
value={nowyUczen} onChange={(e) => setNowyUczen(e.target.value)} />
<input className="form-control" type="text" placeholder="Przedmiot..."
value={nowyPrzedmiot} onChange={(e) => setNowyPrzedmiot(e.target.value)} />
<select className="form-select" style={{ width: '120px' }}
value={nowaOcena} onChange={(e) => setNowaOcena(Number(e.target.value))}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
<option value={5}>5</option>
<option value={6}>6</option>
</select>
<button className="btn btn-primary" onClick={dodaj}>Dodaj</button>
</div>
Number(e.target.value) w onChange selecta! Bez Number() dostalibyśmy string „3" zamiast number 3.dodaj() na razie tylko dodaje — edycję dorobimy w kroku 5. Ale przygotuj się: ta sama funkcja będzie potem obsługiwać OBA tryby!function dodaj() {
if (nowyUczen.trim() === '' || nowyPrzedmiot.trim() === '') return
const nowy = {
id: Date.now(),
uczen: nowyUczen,
przedmiot: nowyPrzedmiot,
ocena: nowaOcena,
data: new Date().toISOString().slice(0, 10),
}
setOceny([...oceny, nowy])
setNowyUczen('')
setNowyPrzedmiot('')
setNowaOcena(3)
}
new Date().toISOString().slice(0, 10)?new Date() → obiekt daty. .toISOString() → "2025-01-22T14:30:00.000Z". .slice(0, 10) → "2025-01-22". Elegancki sposób na dzisiejszą datę!.trim() === '' sprawdza, czy pole nie jest puste (ani same spacje). Nie walidujemy oceny — select wymusza wartość 1-6.oceny.length powinno zwiększyć się o 1.Najpierw funkcja kolorująca badge — dodaj ją wewnątrz App():
function kolorOceny(ocena) {
if (ocena <= 2) return 'bg-danger'
if (ocena === 3) return 'bg-warning text-dark'
if (ocena <= 5) return 'bg-success'
return 'bg-primary'
}
function usun(id) {
setOceny(oceny.filter(o => o.id !== id))
}
I sama tabela w JSX:
<table className="table table-striped">
<thead>
<tr>
<th>Uczeń</th>
<th>Przedmiot</th>
<th>Ocena</th>
<th>Data</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{oceny.map(o => (
<tr key={o.id}>
<td>{o.uczen}</td>
<td>{o.przedmiot}</td>
<td><span className={`badge ${kolorOceny(o.ocena)}`}>{o.ocena}</span></td>
<td>{o.data}</td>
<td>
<button className="btn btn-danger btn-sm" onClick={() => usun(o.id)}>✕</button>
</td>
</tr>
))}
</tbody>
</table>
className={`badge ${kolorOceny(o.ocena)}`} to template literal — łączysz stałą klasę „badge" ze zmienną klasą koloru.5a. Nowy state
const [edytowanyId, setEdytowanyId] = useState(null)
null = tryb dodawania. Jak klikniemy edit → ustawiamy id edytowanego elementu. To prosty wzorzec „toggle" między dwoma trybami.5b. Funkcja edytuj(id)
function edytuj(id) {
const o = oceny.find(o => o.id === id)
setNowyUczen(o.uczen)
setNowyPrzedmiot(o.przedmiot)
setNowaOcena(o.ocena)
setEdytowanyId(id)
}
.find() tutaj?oceny obiektu, którego id pasuje. Zwraca cały obiekt — wtedy wypełniamy formularz jego danymi. Użytkownik widzi stare wartości i może je zmienić!5c. Zmień dodaj() — obsługa edycji
function dodaj() {
if (nowyUczen.trim() === '' || nowyPrzedmiot.trim() === '') return
if (edytowanyId !== null) {
// TRYB EDYCJI — aktualizuj istniejący obiekt
setOceny(oceny.map(o =>
o.id === edytowanyId
? { ...o, uczen: nowyUczen, przedmiot: nowyPrzedmiot, ocena: nowaOcena }
: o
))
setEdytowanyId(null)
} else {
// TRYB DODAWANIA — nowy obiekt
const nowy = {
id: Date.now(),
uczen: nowyUczen,
przedmiot: nowyPrzedmiot,
ocena: nowaOcena,
data: new Date().toISOString().slice(0, 10),
}
setOceny([...oceny, nowy])
}
setNowyUczen('')
setNowyPrzedmiot('')
setNowaOcena(3)
}
oceny.map(o => o.id === edytowanyId ? { ...o, ... } : o). Przelatuje całą tablicę — jak trafia na edytowany element, podmienia dane. Resztę zostawia.5d. Zmień usun() — wyczyść edycję jeśli kasujesz edytowany
function usun(id) {
setOceny(oceny.filter(o => o.id !== id))
if (edytowanyId === id) setEdytowanyId(null)
}
5e. Zmień formularz — warunkowy przycisk
<button className={`btn ${edytowanyId ? 'btn-warning' : 'btn-primary'}`}
onClick={dodaj}>
{edytowanyId ? 'Zapisz' : 'Dodaj'}
</button>
{edytowanyId && (
<button className="btn btn-secondary" onClick={() => {
setEdytowanyId(null); setNowyUczen(''); setNowyPrzedmiot(''); setNowaOcena(3);
}}>Anuluj</button>
)}
5f. Dodaj przycisk edycji w tabeli
<td>
<button className="btn btn-warning btn-sm me-1" onClick={() => edytuj(o.id)}>✎</button>
<button className="btn btn-danger btn-sm" onClick={() => usun(o.id)}>✕</button>
</td>
6a. Nowy state
const [sortKolumna, setSortKolumna] = useState('data')
const [sortRosnaco, setSortRosnaco] = useState(true)
6b. Funkcja zmienSort()
function zmienSort(kolumna) {
if (sortKolumna === kolumna) {
setSortRosnaco(!sortRosnaco)
} else {
setSortKolumna(kolumna)
setSortRosnaco(true)
}
}
6c. Obliczona posortowana lista
const posortowane = [...oceny].sort((a, b) => {
if (a[sortKolumna] < b[sortKolumna]) return sortRosnaco ? -1 : 1
if (a[sortKolumna] > b[sortKolumna]) return sortRosnaco ? 1 : -1
return 0
})
a[sortKolumna]?sortKolumna = 'uczen', to a['uczen'] = a.uczen. Dzięki temu jeden sort obsługuje WSZYSTKIE kolumny![...oceny].sort() — robimy kopię! .sort() mutuje oryginalną tablicę, a React tego nie lubi.6d. Zamień .map w tbody na posortowane
{posortowane.map(o => (...))}
6e. Klikalne nagłówki z ikonkami
<thead>
<tr>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('uczen')}>
Uczeń {sortKolumna === 'uczen' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('przedmiot')}>
Przedmiot {sortKolumna === 'przedmiot' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('ocena')}>
Ocena {sortKolumna === 'ocena' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('data')}>
Data {sortKolumna === 'data' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th>Akcje</th>
</tr>
</thead>
sortKolumna === 'uczen' ? (sortRosnaco ? '▲' : '▼') : '' → jeśli ta kolumna jest aktywna → pokaż strzałkę (góra/dół), jeśli nie → nic.7a. State filtra
const [filtrPrzedmiot, setFiltrPrzedmiot] = useState('')
7b. Computed values — unikalne przedmioty + filtrowanie + sortowanie + średnia
const przedmioty = [...new Set(oceny.map(o => o.przedmiot))]
const przefiltrowane = oceny.filter(o =>
filtrPrzedmiot === '' || o.przedmiot === filtrPrzedmiot
)
const posortowane = [...przefiltrowane].sort((a, b) => {
if (a[sortKolumna] < b[sortKolumna]) return sortRosnaco ? -1 : 1
if (a[sortKolumna] > b[sortKolumna]) return sortRosnaco ? 1 : -1
return 0
})
const srednia = oceny.length > 0
? oceny.reduce((s, o) => s + o.ocena, 0) / oceny.length
: 0
oceny → przefiltrowane → posortowane. Każdy krok to nowa kopia. Oryginalna tablica oceny nigdy się nie zmienia![...new Set(oceny.map(o => o.przedmiot))] → wyciąga tablicę przedmiotów → Set usuwa duplikaty → spread zamienia na tablicę. Wynik: ['Matematyka', 'Fizyka', 'Informatyka'].7c. Karta podsumowania — przed formularzem
<div className="card mb-4">
<div className="card-body d-flex justify-content-around text-center">
<div>
<h5 className="card-title">Ocen</h5>
<p className="card-text fs-4">{oceny.length}</p>
</div>
<div>
<h5 className="card-title">Średnia</h5>
<p className="card-text fs-4">{srednia.toFixed(2)}</p>
</div>
<div>
<h5 className="card-title">Przedmiotów</h5>
<p className="card-text fs-4">{przedmioty.length}</p>
</div>
</div>
</div>
7d. Select filtra — po formularzu, przed tabelą
<select className="form-select mb-3" style={{ width: '220px' }}
value={filtrPrzedmiot} onChange={(e) => setFiltrPrzedmiot(e.target.value)}>
<option value="">Wszystkie przedmioty</option>
{przedmioty.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
przedmioty — dodasz nowy przedmiot → pojawi się w filtrze. Nie musisz nic hardkodować.7e. Alert ostrzegawczy — na koniec return
{oceny.length > 0 && srednia < 3 && (
<div className="alert alert-danger">
⚠️ Uwaga! Średnia ocen poniżej 3.0 — potrzebna poprawa!
</div>
)}
oceny.length > 0 && srednia < 3 — sprawdzamy oba warunki. Bez pierwszego, pusta tablica dałaby średnia = 0, i alert by się pokazał mimo braku danych.1. Karta pokazuje poprawną liczbę ocen i średnią?
2. Filtr po przedmiocie działa?
3. Sortowanie nadal działa po filtrowaniu?
4. Edycja nadal działa (pełny cykl)?
5. Dodaj kilka jedynek — alert się pojawi gdy średnia < 3?
#04 — Gotowy kod
App.jsx
import { useState } from 'react'
import 'bootstrap/dist/css/bootstrap.css'
const poczatkowe = [
{ id: 1, uczen: 'Anna Kowalska', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-15' },
{ id: 2, uczen: 'Jan Nowak', przedmiot: 'Fizyka', ocena: 2, data: '2025-01-16' },
{ id: 3, uczen: 'Anna Kowalska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-17' },
{ id: 4, uczen: 'Marta Wiśniewska', przedmiot: 'Matematyka', ocena: 3, data: '2025-01-18' },
{ id: 5, uczen: 'Jan Nowak', przedmiot: 'Matematyka', ocena: 4, data: '2025-01-20' },
{ id: 6, uczen: 'Marta Wiśniewska', przedmiot: 'Informatyka', ocena: 5, data: '2025-01-22' },
]
function App() {
const [oceny, setOceny] = useState(poczatkowe)
const [nowyUczen, setNowyUczen] = useState('')
const [nowyPrzedmiot, setNowyPrzedmiot] = useState('')
const [nowaOcena, setNowaOcena] = useState(3)
const [edytowanyId, setEdytowanyId] = useState(null)
const [sortKolumna, setSortKolumna] = useState('data')
const [sortRosnaco, setSortRosnaco] = useState(true)
const [filtrPrzedmiot, setFiltrPrzedmiot] = useState('')
const przedmioty = [...new Set(oceny.map(o => o.przedmiot))]
const przefiltrowane = oceny.filter(o =>
filtrPrzedmiot === '' || o.przedmiot === filtrPrzedmiot
)
const posortowane = [...przefiltrowane].sort((a, b) => {
if (a[sortKolumna] < b[sortKolumna]) return sortRosnaco ? -1 : 1
if (a[sortKolumna] > b[sortKolumna]) return sortRosnaco ? 1 : -1
return 0
})
const srednia = oceny.length > 0
? oceny.reduce((s, o) => s + o.ocena, 0) / oceny.length
: 0
function dodaj() {
if (nowyUczen.trim() === '' || nowyPrzedmiot.trim() === '') return
if (edytowanyId !== null) {
setOceny(oceny.map(o =>
o.id === edytowanyId
? { ...o, uczen: nowyUczen, przedmiot: nowyPrzedmiot, ocena: nowaOcena }
: o
))
setEdytowanyId(null)
} else {
const nowy = {
id: Date.now(),
uczen: nowyUczen,
przedmiot: nowyPrzedmiot,
ocena: nowaOcena,
data: new Date().toISOString().slice(0, 10),
}
setOceny([...oceny, nowy])
}
setNowyUczen('')
setNowyPrzedmiot('')
setNowaOcena(3)
}
function edytuj(id) {
const o = oceny.find(o => o.id === id)
setNowyUczen(o.uczen)
setNowyPrzedmiot(o.przedmiot)
setNowaOcena(o.ocena)
setEdytowanyId(id)
}
function usun(id) {
setOceny(oceny.filter(o => o.id !== id))
if (edytowanyId === id) setEdytowanyId(null)
}
function zmienSort(kolumna) {
if (sortKolumna === kolumna) {
setSortRosnaco(!sortRosnaco)
} else {
setSortKolumna(kolumna)
setSortRosnaco(true)
}
}
function kolorOceny(ocena) {
if (ocena <= 2) return 'bg-danger'
if (ocena === 3) return 'bg-warning text-dark'
if (ocena <= 5) return 'bg-success'
return 'bg-primary'
}
return (
<div className="container mt-3">
<h1>Dziennik ocen</h1>
<div className="card mb-4">
<div className="card-body d-flex justify-content-around text-center">
<div>
<h5 className="card-title">Ocen</h5>
<p className="card-text fs-4">{oceny.length}</p>
</div>
<div>
<h5 className="card-title">Średnia</h5>
<p className="card-text fs-4">{srednia.toFixed(2)}</p>
</div>
<div>
<h5 className="card-title">Przedmiotów</h5>
<p className="card-text fs-4">{przedmioty.length}</p>
</div>
</div>
</div>
<div className="d-flex gap-2 mb-3">
<input className="form-control" type="text" placeholder="Uczeń..."
value={nowyUczen} onChange={(e) => setNowyUczen(e.target.value)} />
<input className="form-control" type="text" placeholder="Przedmiot..."
value={nowyPrzedmiot} onChange={(e) => setNowyPrzedmiot(e.target.value)} />
<select className="form-select" style={{ width: '120px' }}
value={nowaOcena} onChange={(e) => setNowaOcena(Number(e.target.value))}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
<option value={5}>5</option>
<option value={6}>6</option>
</select>
<button className={`btn ${edytowanyId ? 'btn-warning' : 'btn-primary'}`}
onClick={dodaj}>
{edytowanyId ? 'Zapisz' : 'Dodaj'}
</button>
{edytowanyId && (
<button className="btn btn-secondary" onClick={() => {
setEdytowanyId(null); setNowyUczen(''); setNowyPrzedmiot(''); setNowaOcena(3);
}}>Anuluj</button>
)}
</div>
<select className="form-select mb-3" style={{ width: '220px' }}
value={filtrPrzedmiot} onChange={(e) => setFiltrPrzedmiot(e.target.value)}>
<option value="">Wszystkie przedmioty</option>
{przedmioty.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
<table className="table table-striped">
<thead>
<tr>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('uczen')}>
Uczeń {sortKolumna === 'uczen' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('przedmiot')}>
Przedmiot {sortKolumna === 'przedmiot' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('ocena')}>
Ocena {sortKolumna === 'ocena' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th style={{ cursor: 'pointer' }} onClick={() => zmienSort('data')}>
Data {sortKolumna === 'data' ? (sortRosnaco ? '▲' : '▼') : ''}
</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{posortowane.map(o => (
<tr key={o.id}>
<td>{o.uczen}</td>
<td>{o.przedmiot}</td>
<td><span className={`badge ${kolorOceny(o.ocena)}`}>{o.ocena}</span></td>
<td>{o.data}</td>
<td>
<button className="btn btn-warning btn-sm me-1" onClick={() => edytuj(o.id)}>✎</button>
<button className="btn btn-danger btn-sm" onClick={() => usun(o.id)}>✕</button>
</td>
</tr>
))}
</tbody>
</table>
{oceny.length > 0 && srednia < 3 && (
<div className="alert alert-danger">
⚠️ Uwaga! Średnia ocen poniżej 3.0 — potrzebna poprawa!
</div>
)}
</div>
)
}
export default App
.find(), .sort(), [...new Set()] i wzorzec edycji z edytowanyId. Brawo! 🎉📝 Sprawdzian — Dziennik ocen
5 pytań · 5 minut · minimum 60% żeby zdać
.find() od .filter()?.sort() stosujemy operator spread [...tablica]?.map()?📝 Ściąga React
Nowy projekt
npm create vite@latest app -- --template react
cd app; npm installImport useState
import { useState } from 'react'Stan (prymityw)
const [x, setX] = useState(0)
setX(x + 1)Stan (tablica)
const [l, setL] = useState([])
setL([...l, nowy])Zmiana obiektu w tablicy
setL(l.map(el =>
el.id === id
? { ...el, pole: nowa }
: el
))Usuwanie z tablicy
setL(l.filter(el =>
el.id !== id
)).filter()
const w = arr.filter(el =>
el.category === 1
).map() w JSX
{lista.map(el => (
<div key={el.id}>{el.x}</div>
))}Controlled input
<input value={x}
onChange={e => setX(e.target.value)} />Controlled checkbox
<input type="checkbox"
checked={on}
onChange={e => setOn(e.target.checked)} />Controlled select
<select value={x}
onChange={e => setX(Number(e.target.value))}>
<option value={1}>A</option>
</select>Ternary w JSX
{warunek ? <A /> : <B />}Short-circuit
{warunek && <A />}Spread operator
const n = { ...stary, pole: 'x' }
const a2 = [...a1, nowy]Warunkowe klasy
className={ok ? 'done' : ''}
className={`base ${ok ? 'done' : ''}`}Bootstrap switch (JSX)
<div className="form-check form-switch">
<input className="form-check-input"
type="checkbox" checked={x}
onChange={e => setX(e.target.checked)} />
<label className="form-check-label">
Label</label>
</div>🐛 Typowe błędy
// ❌ <div class="box"> ✅ <div className="box">// ❌ <label for="x"> ✅ <label htmlFor="x">// ❌ onClick={pobierz(id)} → od razu
// ✅ onClick={() => pobierz(id)} → po kliknięciu// ❌ z.downloads++ ✅ { ...z, downloads: z.downloads + 1 }
// ❌ arr.push(x) ✅ [...arr, x]
// ❌ arr.splice(i, 1) ✅ arr.filter(el => el.id !== id)// ❌ {l.map(el => <div>...</div>)}
// ✅ {l.map(el => <div key={el.id}>...</div>)}Muszą być w public/assets/. Ścieżka: /assets/obraz1.jpg.
import 'bootstrap/dist/css/bootstrap.css'// ❌ <img src="x"> ✅ <img src="x" />// ❌ <input checked={x} />
// ✅ <input checked={x} onChange={e => setX(e.target.checked)} />// select/number input zwraca STRING
// ❌ setX(e.target.value) → "2" (string)
// ✅ setX(Number(e.target.value)) → 2 (number)1. Co to Angular?
Angular to framework od Google do budowy aplikacji SPA (Single Page Application). Używa TypeScript zamiast czystego JavaScript.
Angular vs React
| Cecha | Angular | React |
|---|---|---|
| Typ | Pełny framework | Biblioteka UI |
| Język | TypeScript (wymagany) | JS/TS (opcjonalny) |
| Szablony | HTML + dyrektywy | JSX |
| Two-way binding | Wbudowany [(ngModel)] | Ręczny (value + onChange) |
| Routing | Wbudowany | Zewnętrzny (react-router) |
| DI (Dependency Injection) | Wbudowany | Brak (Context API) |
Kluczowe koncepcje
- Komponenty — bloki UI z własnym HTML, CSS, TS
- Moduły — grupują komponenty, serwisy, routing
- Serwisy — logika biznesowa, pobieranie danych
- Dyrektywy — *ngIf, *ngFor, [ngClass]
- Dependency Injection — automatyczne wstrzykiwanie zależności
📌 Zadanie: Porównanie frameworków
Odpowiedz na pytania:
1. Który framework wymaga TypeScript?
2. Gdzie two-way binding jest wbudowany?
3. Co to SPA?
1. Angular wymaga TypeScript
2. Angular ma wbudowany two-way binding [(ngModel)]
3. SPA = Single Page Application — aplikacja ładowana raz, bez przeładowań strony
2. Środowisko (Angular CLI)
Angular CLI to narzędzie do tworzenia i zarządzania projektem Angular.
Wymagania
- Node.js — wersja 18+ (LTS)
- npm — menedżer pakietów
- Angular CLI —
npm install -g @angular/cli
Tworzenie projektu
# Instalacja Angular CLI globalnie
npm install -g @angular/cli
# Tworzenie nowego projektu
ng new moj-projekt
# → Wybierz: CSS/SCSS, routing Yes/No
# Uruchomienie serwera dev
cd moj-projekt
ng serve
# → http://localhost:4200
Komendy CLI
| Komenda | Opis |
|---|---|
ng new nazwa | Nowy projekt |
ng serve | Serwer deweloperski |
ng generate component nazwa | Generuj komponent |
ng generate service nazwa | Generuj serwis |
ng build | Build produkcyjny |
ng test | Uruchom testy |
Skróty CLI
ng g c header # generate component
ng g s data # generate service
ng g m admin # generate module
📌 Zadanie: Stwórz projekt
Wykonaj kroki:
1. Zainstaluj Angular CLI
2. Stwórz projekt "galeria-angular"
3. Uruchom serwer i otwórz w przeglądarce
npm install -g @angular/cli
ng new galeria-angular
cd galeria-angular
ng serve
# Otwórz http://localhost:4200
3. Komponenty
Komponent w Angular to klasa TypeScript z dekoratorem @Component.
Struktura komponentu
// header.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-header', // tag HTML
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent {
tytul = 'Moja Aplikacja'; // właściwość
kliknij() { // metoda
console.log('Kliknięto!');
}
}
Szablon HTML
<!-- header.component.html -->
<header>
<h1>{{ tytul }}</h1>
<button (click)="kliknij()">Klik</button>
</header>
Generowanie komponentu
ng generate component header
# lub skrót:
ng g c header
# Tworzy:
# src/app/header/
# ├── header.component.ts
# ├── header.component.html
# ├── header.component.css
# └── header.component.spec.ts
Użycie komponentu
<!-- app.component.html -->
<app-header></app-header>
<main>Treść aplikacji</main>
📌 Zadanie: Komponent Karta
Wygeneruj komponent karta i dodaj:
— właściwość nazwa = 'Produkt'
— właściwość cena = 99
— metodę dodajDoKoszyka() wyświetlającą alert
— szablon HTML z tymi danymi i przyciskiem
// karta.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-karta',
templateUrl: './karta.component.html'
})
export class KartaComponent {
nazwa = 'Produkt';
cena = 99;
dodajDoKoszyka() {
alert(`Dodano ${this.nazwa} do koszyka!`);
}
}
<!-- karta.component.html -->
<div class="karta">
<h3>{{ nazwa }}</h3>
<p>Cena: {{ cena }} zł</p>
<button (click)="dodajDoKoszyka()">Dodaj do koszyka</button>
</div>
4. Szablony i interpolacja
Angular używa {{ }} do wyświetlania danych w HTML (interpolacja).
Interpolacja {{ }}
// komponent
export class AppComponent {
imie = 'Maks';
wiek = 18;
getPowitanie() {
return `Cześć ${this.imie}!`;
}
}
<!-- szablon -->
<p>Imię: {{ imie }}</p>
<p>Wiek: {{ wiek }}</p>
<p>Wynik: {{ 2 + 2 }}</p>
<p>{{ getPowitanie() }}</p>
Property binding [atrybut]
<!-- Dynamiczny atrybut -->
<img [src]="urlObrazu" [alt]="opis">
<button [disabled]="czyWylaczony">Wyślij</button>
<div [class.active]="jestAktywny"></div>
<p [style.color]="kolor">Tekst</p>
Event binding (event)
<button (click)="zapisz()">Zapisz</button>
<input (input)="onInput($event)">
<input (keyup.enter)="wyslij()">
<div (mouseenter)="pokaz()" (mouseleave)="ukryj()"></div>
Porównanie React vs Angular
| React (JSX) | Angular (Template) |
|---|---|
{wartosc} | {{ wartosc }} |
src={url} | [src]="url" |
onClick={fn} | (click)="fn()" |
className="x" | class="x" |
📌 Zadanie: Binding danych
Stwórz komponent z:
— właściwością urlLogo = 'logo.png'
— właściwością altText = 'Logo firmy'
— metodą kliknijLogo()
W szablonie użyj property binding dla src i alt, oraz event binding dla click.
<img
[src]="urlLogo"
[alt]="altText"
(click)="kliknijLogo()">
5. Dyrektywy (*ngIf, *ngFor)
Dyrektywy strukturalne zmieniają strukturę DOM — dodają/usuwają elementy.
*ngIf — warunkowe renderowanie
<!-- Pokaż jeśli warunek true -->
<p *ngIf="zalogowany">Witaj z powrotem!</p>
<!-- if-else -->
<p *ngIf="zalogowany; else niezalogowany">
Witaj {{ imie }}!
</p>
<ng-template #niezalogowany>
<p>Proszę się zalogować</p>
</ng-template>
*ngFor — pętla po tablicy
// komponent
produkty = [
{ id: 1, nazwa: 'Laptop', cena: 3500 },
{ id: 2, nazwa: 'Telefon', cena: 1200 },
{ id: 3, nazwa: 'Słuchawki', cena: 150 }
];
<!-- szablon -->
<ul>
<li *ngFor="let p of produkty">
{{ p.nazwa }} — {{ p.cena }} zł
</li>
</ul>
<!-- z indeksem -->
<div *ngFor="let p of produkty; let i = index">
{{ i + 1 }}. {{ p.nazwa }}
</div>
<!-- trackBy dla wydajności -->
<div *ngFor="let p of produkty; trackBy: trackById">
trackById(index: number, item: any) {
return item.id;
}
[ngClass] — klasy warunkowe
<div [ngClass]="{'active': jestAktywny, 'disabled': jestWylaczony}">
</div>
[ngStyle] — style warunkowe
<p [ngStyle]="{'color': blad ? 'red' : 'green'}">
{{ wiadomosc }}
</p>
📌 Zadanie: Lista z warunkiem
Mając tablicę użytkowników, wyświetl:
— listę imion używając *ngFor
— komunikat "Brak użytkowników" gdy lista pusta (*ngIf)
<div *ngIf="uzytkownicy.length === 0">
Brak użytkowników
</div>
<ul *ngIf="uzytkownicy.length > 0">
<li *ngFor="let u of uzytkownicy">
{{ u.imie }}
</li>
</ul>
6. Data Binding
Angular ma 4 typy data binding:
1. Interpolacja (komponent → DOM)
{{ wartosc }}
2. Property binding (komponent → DOM)
[src]="urlObrazu"
[disabled]="jestWylaczony"
3. Event binding (DOM → komponent)
(click)="zapisz()"
(input)="onInput($event)"
4. Two-way binding (obustronny)
<!-- Wymaga FormsModule! -->
<input [(ngModel)]="imie">
<p>Wpisano: {{ imie }}</p>
// app.module.ts
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [FormsModule]
})
[(ngModel)] = syntax banana-in-box
<!-- To samo co: -->
<input [ngModel]="imie" (ngModelChange)="imie = $event">
Porównanie z React
| React | Angular |
|---|---|
value={x} onChange={e => setX(e.target.value)} | [(ngModel)]="x" |
📌 Zadanie: Formularz z two-way binding
Stwórz formularz z polami imię i email używając [(ngModel)]. Wyświetl podgląd danych pod formularzem.
// komponent
imie = '';
email = '';
<input [(ngModel)]="imie" placeholder="Imię">
<input [(ngModel)]="email" placeholder="Email">
<p>Imię: {{ imie }}</p>
<p>Email: {{ email }}</p>
7. @Input i @Output
Komunikacja między komponentami rodzic ↔ dziecko.
@Input — dane od rodzica
// dziecko: karta.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-karta',
template: `<div>{{ nazwa }} — {{ cena }} zł</div>`
})
export class KartaComponent {
@Input() nazwa = '';
@Input() cena = 0;
}
<!-- rodzic -->
<app-karta [nazwa]="'Laptop'" [cena]="3500"></app-karta>
<app-karta [nazwa]="produkt.nazwa" [cena]="produkt.cena"></app-karta>
@Output — eventy do rodzica
// dziecko: karta.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-karta',
template: `
<div>{{ nazwa }}</div>
<button (click)="onDodaj()">Dodaj</button>
`
})
export class KartaComponent {
@Input() nazwa = '';
@Output() dodano = new EventEmitter<string>();
onDodaj() {
this.dodano.emit(this.nazwa);
}
}
<!-- rodzic -->
<app-karta
[nazwa]="'Laptop'"
(dodano)="handleDodano($event)">
</app-karta>
// rodzic
handleDodano(nazwa: string) {
console.log('Dodano:', nazwa);
}
Porównanie z React
| React | Angular |
|---|---|
<Karta nazwa="X" /> | <app-karta [nazwa]="'X'"> |
<Karta onDodaj={fn} /> | <app-karta (dodano)="fn($event)"> |
📌 Zadanie: Komponent z Input/Output
Stwórz komponent Przycisk:
— @Input() tekst: string
— @Output() kliknieto: EventEmitter
Użyj go w rodzicu i obsłuż kliknięcie.
// przycisk.component.ts
@Component({
selector: 'app-przycisk',
template: `<button (click)="onClick()">{{ tekst }}</button>`
})
export class PrzyciskComponent {
@Input() tekst = 'Kliknij';
@Output() kliknieto = new EventEmitter<void>();
onClick() {
this.kliknieto.emit();
}
}
// rodzic
<app-przycisk
[tekst]="'Zapisz'"
(kliknieto)="zapisz()">
</app-przycisk>
8. Serwisy i Dependency Injection
Serwis = klasa z logiką biznesową (API, obliczenia). DI = Angular automatycznie tworzy i wstrzykuje instancje.
Tworzenie serwisu
ng generate service data
# lub: ng g s data
// data.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // dostępny w całej aplikacji
})
export class DataService {
private produkty = [
{ id: 1, nazwa: 'Laptop', cena: 3500 },
{ id: 2, nazwa: 'Telefon', cena: 1200 }
];
getProdukty() {
return this.produkty;
}
dodajProdukt(nazwa: string, cena: number) {
const id = this.produkty.length + 1;
this.produkty.push({ id, nazwa, cena });
}
}
Wstrzykiwanie do komponentu
// lista.component.ts
import { Component } from '@angular/core';
import { DataService } from '../data.service';
@Component({
selector: 'app-lista',
template: `
<ul>
<li *ngFor="let p of produkty">{{ p.nazwa }}</li>
</ul>
`
})
export class ListaComponent {
produkty: any[] = [];
constructor(private dataService: DataService) {
this.produkty = this.dataService.getProdukty();
}
}
Dlaczego serwisy?
- Współdzielenie danych między komponentami
- Separacja logiki od UI
- Łatwiejsze testowanie
- Singleton — jedna instancja w aplikacji
📌 Zadanie: Serwis koszyka
Stwórz KoszykService z metodami:
— dodaj(produkt)
— usun(id)
— getKoszyk()
— getSuma()
@Injectable({ providedIn: 'root' })
export class KoszykService {
private koszyk: any[] = [];
dodaj(produkt: any) {
this.koszyk.push(produkt);
}
usun(id: number) {
this.koszyk = this.koszyk.filter(p => p.id !== id);
}
getKoszyk() {
return this.koszyk;
}
getSuma() {
return this.koszyk.reduce((sum, p) => sum + p.cena, 0);
}
}
9. Routing
Angular ma wbudowany router do nawigacji między widokami.
Konfiguracja routingu
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProduktyComponent } from './produkty/produkty.component';
import { KontaktComponent } from './kontakt/kontakt.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'produkty', component: ProduktyComponent },
{ path: 'kontakt', component: KontaktComponent },
{ path: '**', redirectTo: '' } // 404 → home
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
router-outlet
<!-- app.component.html -->
<nav>
<a routerLink="/">Home</a>
<a routerLink="/produkty">Produkty</a>
<a routerLink="/kontakt">Kontakt</a>
</nav>
<router-outlet></router-outlet>
routerLinkActive — aktywny link
<a routerLink="/produkty" routerLinkActive="active">
Produkty
</a>
Parametry trasy
// routing
{ path: 'produkt/:id', component: ProduktDetailComponent }
// komponent
import { ActivatedRoute } from '@angular/router';
constructor(private route: ActivatedRoute) {
this.route.params.subscribe(params => {
this.produktId = params['id'];
});
}
Nawigacja programowa
import { Router } from '@angular/router';
constructor(private router: Router) {}
goToProdukt(id: number) {
this.router.navigate(['/produkt', id]);
}
📌 Zadanie: Routing podstawowy
Skonfiguruj routing z 3 stronami: Home, O nas, Kontakt. Dodaj nawigację i router-outlet.
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'o-nas', component: ONasComponent },
{ path: 'kontakt', component: KontaktComponent }
];
<nav>
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
<a routerLink="/o-nas" routerLinkActive="active">O nas</a>
<a routerLink="/kontakt" routerLinkActive="active">Kontakt</a>
</nav>
<router-outlet></router-outlet>
10. Formularze
Angular ma 2 podejścia: Template-driven (proste) i Reactive (zaawansowane).
Template-driven (FormsModule)
// app.module.ts
import { FormsModule } from '@angular/forms';
@NgModule({ imports: [FormsModule] })
<form #f="ngForm" (ngSubmit)="onSubmit(f)">
<input name="imie" [(ngModel)]="imie" required>
<input name="email" [(ngModel)]="email" required email>
<button [disabled]="!f.valid">Wyślij</button>
</form>
Reactive Forms (ReactiveFormsModule)
// app.module.ts
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({ imports: [ReactiveFormsModule] })
// komponent
import { FormGroup, FormControl, Validators } from '@angular/forms';
export class FormComponent {
form = new FormGroup({
imie: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email]),
wiek: new FormControl(18, [Validators.min(1), Validators.max(120)])
});
onSubmit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="imie">
<div *ngIf="form.get('imie')?.invalid && form.get('imie')?.touched">
Imię jest wymagane
</div>
<input formControlName="email">
<input type="number" formControlName="wiek">
<button [disabled]="form.invalid">Wyślij</button>
</form>
Walidatory
| Walidator | Użycie |
|---|---|
required | Pole wymagane |
email | Format email |
minLength(n) | Min. długość |
maxLength(n) | Max. długość |
min(n) | Min. wartość |
max(n) | Max. wartość |
pattern(regex) | Wyrażenie regularne |
📌 Zadanie: Formularz rejestracji
Stwórz reactive form z polami: imie (required), email (required, email), haslo (required, minLength 6). Pokaż błędy walidacji.
form = new FormGroup({
imie: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email]),
haslo: new FormControl('', [Validators.required, Validators.minLength(6)])
});
11. HttpClient
Angular ma wbudowany HttpClient do komunikacji z API.
Konfiguracja
// app.module.ts
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [HttpClientModule]
})
Serwis z HTTP
// api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ApiService {
private apiUrl = 'https://jsonplaceholder.typicode.com';
constructor(private http: HttpClient) {}
// GET
getPosts(): Observable<any[]> {
return this.http.get<any[]>(`${this.apiUrl}/posts`);
}
// POST
addPost(post: any): Observable<any> {
return this.http.post(`${this.apiUrl}/posts`, post);
}
// DELETE
deletePost(id: number): Observable<any> {
return this.http.delete(`${this.apiUrl}/posts/${id}`);
}
}
Użycie w komponencie
export class PostyComponent implements OnInit {
posty: any[] = [];
loading = true;
constructor(private api: ApiService) {}
ngOnInit() {
this.api.getPosts().subscribe({
next: (data) => {
this.posty = data;
this.loading = false;
},
error: (err) => console.error(err)
});
}
}
Porównanie z React
| React | Angular |
|---|---|
useEffect + fetch | ngOnInit + HttpClient.subscribe |
| Promise | Observable (RxJS) |
📌 Zadanie: Pobierz użytkowników
Stwórz serwis pobierający użytkowników z https://jsonplaceholder.typicode.com/users. Wyświetl ich imiona w komponencie.
// user.service.ts
getUsers(): Observable<any[]> {
return this.http.get<any[]>('https://jsonplaceholder.typicode.com/users');
}
// komponent
ngOnInit() {
this.userService.getUsers().subscribe(data => {
this.users = data;
});
}
12. Struktura projektu Angular
Domyślna struktura
moj-projekt/
├── src/
│ ├── app/
│ │ ├── app.component.ts # Główny komponent
│ │ ├── app.component.html
│ │ ├── app.component.css
│ │ ├── app.module.ts # Główny moduł
│ │ └── app-routing.module.ts # Routing
│ ├── assets/ # Statyczne pliki
│ ├── index.html # Główny HTML
│ ├── main.ts # Entry point
│ └── styles.css # Globalne style
├── angular.json # Konfiguracja CLI
├── package.json # Zależności
└── tsconfig.json # Konfiguracja TS
Zalecana struktura dla większych projektów
src/app/
├── core/ # Singletons, guardy, interceptory
│ ├── services/
│ │ └── auth.service.ts
│ └── guards/
│ └── auth.guard.ts
├── shared/ # Współdzielone komponenty, pipes
│ ├── components/
│ │ └── button/
│ └── pipes/
├── features/ # Moduły funkcjonalne
│ ├── home/
│ │ ├── home.component.ts
│ │ └── home.module.ts
│ ├── produkty/
│ │ ├── produkty.component.ts
│ │ ├── produkt-detail/
│ │ └── produkty.module.ts
│ └── koszyk/
├── app.component.ts
├── app.module.ts
└── app-routing.module.ts
Moduły funkcjonalne (Feature Modules)
// produkty/produkty.module.ts
@NgModule({
declarations: [
ProduktyComponent,
ProduktDetailComponent
],
imports: [
CommonModule,
ProduktyRoutingModule
]
})
export class ProduktyModule {}
Lazy Loading
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'produkty',
loadChildren: () => import('./features/produkty/produkty.module')
.then(m => m.ProduktyModule)
}
];
📌 Zadanie: Organizacja projektu
Narysuj strukturę folderów dla aplikacji e-commerce z modułami: produkty, koszyk, użytkownik.
src/app/
├── core/
│ └── services/
│ ├── auth.service.ts
│ └── api.service.ts
├── shared/
│ └── components/
│ └── navbar/
├── features/
│ ├── produkty/
│ │ ├── lista-produktow/
│ │ ├── produkt-detail/
│ │ └── produkty.module.ts
│ ├── koszyk/
│ │ ├── koszyk.component.ts
│ │ └── koszyk.module.ts
│ └── user/
│ ├── login/
│ ├── profil/
│ └── user.module.ts
├── app.module.ts
└── app-routing.module.ts
🔥 Rozgrzewka — Galeria zdjęć (Angular)
Odpowiedz na pytania przed rozpoczęciem ćwiczenia:
1. Jak wyświetlić tablicę obrazów w Angular?
2. Jak dynamicznie ustawić atrybut src obrazka?
3. Jak obsłużyć kliknięcie w Angular?
4. Jak warunkowo pokazać element?
5. Jaka komenda CLI generuje komponent?
📋 Instrukcja — Galeria zdjęć (Angular)
Zbuduj galerię zdjęć w Angular z filtrowaniem kategorii i modalne preview.
Funkcjonalności
- Wyświetlanie siatki obrazów z *ngFor
- Filtrowanie po kategorii
- Powiększanie obrazu w modalu
- Licznik pobrań dla każdego obrazu
Technologie
- Angular 17+
- *ngFor, *ngIf, [ngClass]
- @Input, @Output
- Bootstrap 5 (opcjonalnie)
🔨 Budowa — Galeria zdjęć (Angular)
Krok 1: Nowy projekt
ng new galeria-angular
cd galeria-angular
ng serve
Krok 2: Model danych
// app.component.ts
zdjecia = [
{ id: 1, url: 'assets/img1.jpg', tytul: 'Góry', kategoria: 'natura', downloads: 0 },
{ id: 2, url: 'assets/img2.jpg', tytul: 'Miasto', kategoria: 'urban', downloads: 0 },
{ id: 3, url: 'assets/img3.jpg', tytul: 'Las', kategoria: 'natura', downloads: 0 },
{ id: 4, url: 'assets/img4.jpg', tytul: 'Plaża', kategoria: 'natura', downloads: 0 }
];
aktywnaKategoria = 'wszystkie';
wybraneZdjecie: any = null;
Krok 3: Filtrowanie
get przefiltrowane() {
if (this.aktywnaKategoria === 'wszystkie') {
return this.zdjecia;
}
return this.zdjecia.filter(z => z.kategoria === this.aktywnaKategoria);
}
get kategorie() {
return ['wszystkie', ...new Set(this.zdjecia.map(z => z.kategoria))];
}
setKategoria(kat: string) {
this.aktywnaKategoria = kat;
}
Krok 4: Szablon HTML
<div class="container">
<h1>Galeria</h1>
<!-- Filtry -->
<div class="filtry">
<button *ngFor="let kat of kategorie"
[ngClass]="{'active': kat === aktywnaKategoria}"
(click)="setKategoria(kat)">
{{ kat }}
</button>
</div>
<!-- Siatka -->
<div class="galeria">
<div *ngFor="let z of przefiltrowane" class="karta">
<img [src]="z.url" [alt]="z.tytul" (click)="wybraneZdjecie = z">
<h3>{{ z.tytul }}</h3>
<p>Pobrano: {{ z.downloads }}</p>
<button (click)="download(z)">Pobierz</button>
</div>
</div>
</div>
Krok 5: Modal
<!-- Modal -->
<div *ngIf="wybraneZdjecie" class="modal-overlay" (click)="wybraneZdjecie = null">
<div class="modal-content" (click)="$event.stopPropagation()">
<button class="close-btn" (click)="wybraneZdjecie = null">×</button>
<img [src]="wybraneZdjecie.url" [alt]="wybraneZdjecie.tytul">
<h2>{{ wybraneZdjecie.tytul }}</h2>
</div>
</div>
Krok 6: Metoda download
download(zdjecie: any) {
zdjecie.downloads++;
}
Krok 7: Style CSS
.galeria {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.karta {
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
}
.karta img {
width: 100%;
cursor: pointer;
}
.filtry button.active {
background: #007bff;
color: white;
}
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.8);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
max-width: 90%;
}
Krok 8: Obrazy w assets
Dodaj pliki graficzne do src/assets/: img1.jpg, img2.jpg, img3.jpg, img4.jpg
💻 Gotowy kod — Galeria (Angular)
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
zdjecia = [
{ id: 1, url: 'assets/img1.jpg', tytul: 'Góry', kategoria: 'natura', downloads: 0 },
{ id: 2, url: 'assets/img2.jpg', tytul: 'Miasto', kategoria: 'urban', downloads: 0 },
{ id: 3, url: 'assets/img3.jpg', tytul: 'Las', kategoria: 'natura', downloads: 0 },
{ id: 4, url: 'assets/img4.jpg', tytul: 'Plaża', kategoria: 'natura', downloads: 0 }
];
aktywnaKategoria = 'wszystkie';
wybraneZdjecie: any = null;
get przefiltrowane() {
if (this.aktywnaKategoria === 'wszystkie') return this.zdjecia;
return this.zdjecia.filter(z => z.kategoria === this.aktywnaKategoria);
}
get kategorie() {
return ['wszystkie', ...new Set(this.zdjecia.map(z => z.kategoria))];
}
setKategoria(kat: string) {
this.aktywnaKategoria = kat;
}
download(zdjecie: any) {
zdjecie.downloads++;
}
}
📝 Sprawdzian — Galeria (Angular)
5 pytań · 10 minut · minimum 60% żeby zdać
🔥 Rozgrzewka — Lista zadań (Angular)
1. Two-way binding dla inputa w Angular:
2. Jaki moduł potrzebny do ngModel?
3. Jak dodać element do tablicy (niemutująco)?
4. Metoda usunięcia elementu z tablicy po id:
5. Jak obsłużyć submit formularza?
📋 Instrukcja — Lista zadań (Angular)
Zbuduj aplikację Todo w Angular z dodawaniem, usuwaniem i oznaczaniem zadań.
Funkcjonalności
- Dodawanie nowych zadań
- Oznaczanie jako wykonane (checkbox)
- Usuwanie zadań
- Filtrowanie: wszystkie/aktywne/wykonane
🔨 Budowa — Lista zadań (Angular)
Krok 1: Projekt i FormsModule
// app.module.ts
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, FormsModule]
})
Krok 2: Model danych
interface Zadanie {
id: number;
tytul: string;
wykonane: boolean;
}
zadania: Zadanie[] = [];
noweZadanie = '';
filtr = 'wszystkie';
Krok 3: Metody CRUD
dodaj() {
if (!this.noweZadanie.trim()) return;
this.zadania = [...this.zadania, {
id: Date.now(),
tytul: this.noweZadanie,
wykonane: false
}];
this.noweZadanie = '';
}
usun(id: number) {
this.zadania = this.zadania.filter(z => z.id !== id);
}
toggle(id: number) {
this.zadania = this.zadania.map(z =>
z.id === id ? { ...z, wykonane: !z.wykonane } : z
);
}
Krok 4: Filtrowanie
get przefiltrowane() {
switch (this.filtr) {
case 'aktywne': return this.zadania.filter(z => !z.wykonane);
case 'wykonane': return this.zadania.filter(z => z.wykonane);
default: return this.zadania;
}
}
Krok 5: Szablon HTML
<div class="todo-app">
<h1>Lista zadań</h1>
<form (ngSubmit)="dodaj()">
<input [(ngModel)]="noweZadanie" name="nowe" placeholder="Nowe zadanie...">
<button type="submit">Dodaj</button>
</form>
<div class="filtry">
<button [ngClass]="{'active': filtr === 'wszystkie'}" (click)="filtr = 'wszystkie'">Wszystkie</button>
<button [ngClass]="{'active': filtr === 'aktywne'}" (click)="filtr = 'aktywne'">Aktywne</button>
<button [ngClass]="{'active': filtr === 'wykonane'}" (click)="filtr = 'wykonane'">Wykonane</button>
</div>
<ul>
<li *ngFor="let z of przefiltrowane" [ngClass]="{'done': z.wykonane}">
<input type="checkbox" [checked]="z.wykonane" (change)="toggle(z.id)">
{{ z.tytul }}
<button (click)="usun(z.id)">×</button>
</li>
</ul>
</div>
Krok 6: Style
.done { text-decoration: line-through; opacity: 0.6; }
.filtry button.active { background: #28a745; color: white; }
Krok 7: Testowanie
Przetestuj wszystkie funkcje: dodawanie, usuwanie, toggle, filtrowanie.
💻 Gotowy kod — Lista zadań (Angular)
import { Component } from '@angular/core';
interface Zadanie {
id: number;
tytul: string;
wykonane: boolean;
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
zadania: Zadanie[] = [];
noweZadanie = '';
filtr = 'wszystkie';
get przefiltrowane() {
switch (this.filtr) {
case 'aktywne': return this.zadania.filter(z => !z.wykonane);
case 'wykonane': return this.zadania.filter(z => z.wykonane);
default: return this.zadania;
}
}
dodaj() {
if (!this.noweZadanie.trim()) return;
this.zadania = [...this.zadania, {
id: Date.now(),
tytul: this.noweZadanie,
wykonane: false
}];
this.noweZadanie = '';
}
usun(id: number) {
this.zadania = this.zadania.filter(z => z.id !== id);
}
toggle(id: number) {
this.zadania = this.zadania.map(z =>
z.id === id ? { ...z, wykonane: !z.wykonane } : z
);
}
}
📝 Sprawdzian — Lista zadań (Angular)
5 pytań · 10 minut · minimum 60% żeby zdać
🔥 Rozgrzewka — Tracker wydatków (Angular)
1. Metoda sumowania tablicy w JS:
2. Jak przekazać dane do komponentu dziecka?
3. Jak wysłać event do rodzica?
4. Pipe do formatowania waluty:
5. Typ dla number input (e.target.value):
📋 Instrukcja — Tracker wydatków (Angular)
Zbuduj aplikację do śledzenia wydatków z kategoriami i podsumowaniem.
Funkcjonalności
- Dodawanie wydatków (opis, kwota, kategoria)
- Lista wydatków z możliwością usuwania
- Podsumowanie: suma, średnia, max
- Filtrowanie po kategorii
🔨 Budowa — Tracker wydatków (Angular)
Krok 1: Model danych
interface Wydatek {
id: number;
opis: string;
kwota: number;
kategoria: string;
data: Date;
}
wydatki: Wydatek[] = [];
nowyOpis = '';
nowaKwota = 0;
nowaKategoria = 'jedzenie';
filtrKategoria = 'wszystkie';
kategorie = ['jedzenie', 'transport', 'rozrywka', 'rachunki', 'inne'];
Krok 2: Metody obliczeń
get przefiltrowane() {
if (this.filtrKategoria === 'wszystkie') return this.wydatki;
return this.wydatki.filter(w => w.kategoria === this.filtrKategoria);
}
get suma() {
return this.przefiltrowane.reduce((s, w) => s + w.kwota, 0);
}
get srednia() {
return this.przefiltrowane.length ? this.suma / this.przefiltrowane.length : 0;
}
get max() {
return this.przefiltrowane.length
? Math.max(...this.przefiltrowane.map(w => w.kwota))
: 0;
}
Krok 3: Dodawanie/Usuwanie
dodaj() {
if (!this.nowyOpis.trim() || this.nowaKwota <= 0) return;
this.wydatki = [...this.wydatki, {
id: Date.now(),
opis: this.nowyOpis,
kwota: this.nowaKwota,
kategoria: this.nowaKategoria,
data: new Date()
}];
this.nowyOpis = '';
this.nowaKwota = 0;
}
usun(id: number) {
this.wydatki = this.wydatki.filter(w => w.id !== id);
}
Krok 4: Formularz HTML
<form (ngSubmit)="dodaj()">
<input [(ngModel)]="nowyOpis" name="opis" placeholder="Opis wydatku">
<input type="number" [(ngModel)]="nowaKwota" name="kwota" placeholder="Kwota">
<select [(ngModel)]="nowaKategoria" name="kategoria">
<option *ngFor="let k of kategorie" [value]="k">{{ k }}</option>
</select>
<button type="submit">Dodaj</button>
</form>
Krok 5: Lista i podsumowanie
<div class="podsumowanie">
<p>Suma: {{ suma | currency:'PLN' }}</p>
<p>Średnia: {{ srednia | currency:'PLN' }}</p>
<p>Max: {{ max | currency:'PLN' }}</p>
</div>
<select [(ngModel)]="filtrKategoria">
<option value="wszystkie">Wszystkie</option>
<option *ngFor="let k of kategorie" [value]="k">{{ k }}</option>
</select>
<ul>
<li *ngFor="let w of przefiltrowane">
{{ w.opis }} - {{ w.kwota | currency:'PLN' }} ({{ w.kategoria }})
<button (click)="usun(w.id)">×</button>
</li>
</ul>
Krok 6: Locale dla PLN
// app.module.ts
import { registerLocaleData } from '@angular/common';
import localePl from '@angular/common/locales/pl';
registerLocaleData(localePl);
@NgModule({
providers: [{ provide: LOCALE_ID, useValue: 'pl-PL' }]
})
Krok 7: Style
.podsumowanie { background: #f8f9fa; padding: 15px; border-radius: 8px; }
.podsumowanie p { margin: 5px 0; font-weight: bold; }
Krok 8: Testowanie
Dodaj kilka wydatków, przetestuj filtrowanie i obliczenia.
💻 Gotowy kod — Tracker wydatków (Angular)
import { Component } from '@angular/core';
interface Wydatek {
id: number;
opis: string;
kwota: number;
kategoria: string;
data: Date;
}
@Component({ selector: 'app-root', templateUrl: './app.component.html' })
export class AppComponent {
wydatki: Wydatek[] = [];
nowyOpis = '';
nowaKwota = 0;
nowaKategoria = 'jedzenie';
filtrKategoria = 'wszystkie';
kategorie = ['jedzenie', 'transport', 'rozrywka', 'rachunki', 'inne'];
get przefiltrowane() {
if (this.filtrKategoria === 'wszystkie') return this.wydatki;
return this.wydatki.filter(w => w.kategoria === this.filtrKategoria);
}
get suma() { return this.przefiltrowane.reduce((s, w) => s + w.kwota, 0); }
get srednia() { return this.przefiltrowane.length ? this.suma / this.przefiltrowane.length : 0; }
get max() { return this.przefiltrowane.length ? Math.max(...this.przefiltrowane.map(w => w.kwota)) : 0; }
dodaj() {
if (!this.nowyOpis.trim() || this.nowaKwota <= 0) return;
this.wydatki = [...this.wydatki, {
id: Date.now(), opis: this.nowyOpis, kwota: this.nowaKwota,
kategoria: this.nowaKategoria, data: new Date()
}];
this.nowyOpis = ''; this.nowaKwota = 0;
}
usun(id: number) { this.wydatki = this.wydatki.filter(w => w.id !== id); }
}
📝 Sprawdzian — Tracker wydatków (Angular)
5 pytań · 10 minut · minimum 60% żeby zdać
🔥 Rozgrzewka — Dziennik ocen (Angular)
1. Jak wygenerować serwis Angular CLI?
2. Dekorator dla serwisu singleton:
3. Sortowanie tablicy w JS:
4. Jak wstrzyknąć serwis do komponentu?
5. Znajdowanie elementu po warunku:
📋 Instrukcja — Dziennik ocen (Angular)
Zbuduj dziennik ocen z serwisem do zarządzania danymi.
Funkcjonalności
- Lista uczniów z przedmiotami i ocenami
- Dodawanie ocen do ucznia
- Średnia ocen ucznia
- Sortowanie po nazwisku/średniej
- Serwis do przechowywania danych
🔨 Budowa — Dziennik ocen (Angular)
Krok 1: Serwis
ng generate service dziennik
// dziennik.service.ts
@Injectable({ providedIn: 'root' })
export class DziennikService {
private uczniowie = [
{ id: 1, imie: 'Jan', nazwisko: 'Kowalski', oceny: [4, 5, 3, 4] },
{ id: 2, imie: 'Anna', nazwisko: 'Nowak', oceny: [5, 5, 4, 5] },
{ id: 3, imie: 'Piotr', nazwisko: 'Wiśniewski', oceny: [3, 3, 4, 3] }
];
getUczniowie() { return this.uczniowie; }
dodajOcene(id: number, ocena: number) {
const uczen = this.uczniowie.find(u => u.id === id);
if (uczen) uczen.oceny.push(ocena);
}
getSrednia(oceny: number[]) {
return oceny.length ? oceny.reduce((s, o) => s + o, 0) / oceny.length : 0;
}
}
Krok 2: Komponent
export class AppComponent {
uczniowie: any[] = [];
sortowanie = 'nazwisko';
nowaOcena = 5;
wybranyUczen: number | null = null;
constructor(private dziennik: DziennikService) {
this.uczniowie = this.dziennik.getUczniowie();
}
get posortowani() {
return [...this.uczniowie].sort((a, b) => {
if (this.sortowanie === 'nazwisko') {
return a.nazwisko.localeCompare(b.nazwisko);
}
return this.getSrednia(b.oceny) - this.getSrednia(a.oceny);
});
}
getSrednia(oceny: number[]) {
return this.dziennik.getSrednia(oceny);
}
dodajOcene(id: number) {
this.dziennik.dodajOcene(id, this.nowaOcena);
}
}
Krok 3: Szablon
<h1>Dziennik ocen</h1>
<select [(ngModel)]="sortowanie">
<option value="nazwisko">Sortuj po nazwisku</option>
<option value="srednia">Sortuj po średniej</option>
</select>
<table>
<tr>
<th>Imię</th><th>Nazwisko</th><th>Oceny</th><th>Średnia</th><th>Akcje</th>
</tr>
<tr *ngFor="let u of posortowani">
<td>{{ u.imie }}</td>
<td>{{ u.nazwisko }}</td>
<td>{{ u.oceny.join(', ') }}</td>
<td>{{ getSrednia(u.oceny) | number:'1.2-2' }}</td>
<td>
<select [(ngModel)]="nowaOcena">
<option *ngFor="let o of [1,2,3,4,5,6]" [value]="o">{{ o }}</option>
</select>
<button (click)="dodajOcene(u.id)">Dodaj</button>
</td>
</tr>
</table>
Krok 4: Style tabeli
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; border: 1px solid #ddd; text-align: left; }
th { background: #f8f9fa; }
Krok 5: Kolorowanie średniej
<td [ngStyle]="{'color': getSrednia(u.oceny) >= 4 ? 'green' : getSrednia(u.oceny) < 3 ? 'red' : 'orange'}">
{{ getSrednia(u.oceny) | number:'1.2-2' }}
</td>
Krok 6: Dodawanie ucznia
// Rozszerz serwis
dodajUcznia(imie: string, nazwisko: string) {
this.uczniowie.push({
id: Date.now(),
imie, nazwisko,
oceny: []
});
}
Krok 7: Walidacja
Dodaj walidację: ocena 1-6, niepuste imię/nazwisko.
Krok 8: Testowanie
Przetestuj dodawanie ocen, sortowanie, kolorowanie.
💻 Gotowy kod — Dziennik ocen (Angular)
// dziennik.service.ts
@Injectable({ providedIn: 'root' })
export class DziennikService {
private uczniowie = [
{ id: 1, imie: 'Jan', nazwisko: 'Kowalski', oceny: [4, 5, 3, 4] },
{ id: 2, imie: 'Anna', nazwisko: 'Nowak', oceny: [5, 5, 4, 5] },
{ id: 3, imie: 'Piotr', nazwisko: 'Wiśniewski', oceny: [3, 3, 4, 3] }
];
getUczniowie() { return this.uczniowie; }
dodajOcene(id: number, ocena: number) {
const u = this.uczniowie.find(x => x.id === id);
if (u && ocena >= 1 && ocena <= 6) u.oceny.push(ocena);
}
getSrednia(oceny: number[]) {
return oceny.length ? oceny.reduce((s, o) => s + o, 0) / oceny.length : 0;
}
}
// app.component.ts
export class AppComponent {
uczniowie: any[] = [];
sortowanie = 'nazwisko';
nowaOcena = 5;
constructor(private dziennik: DziennikService) {
this.uczniowie = this.dziennik.getUczniowie();
}
get posortowani() {
return [...this.uczniowie].sort((a, b) =>
this.sortowanie === 'nazwisko'
? a.nazwisko.localeCompare(b.nazwisko)
: this.getSrednia(b.oceny) - this.getSrednia(a.oceny)
);
}
getSrednia(oceny: number[]) { return this.dziennik.getSrednia(oceny); }
dodajOcene(id: number) { this.dziennik.dodajOcene(id, this.nowaOcena); }
}
📝 Sprawdzian — Dziennik ocen (Angular)
5 pytań · 10 minut · minimum 60% żeby zdać
📋 Ściąga Angular
Nowy projekt
ng new nazwa
ng serveGenerowanie
ng g c nazwa # component
ng g s nazwa # service
ng g m nazwa # moduleInterpolacja
{{ wartosc }}
{{ metoda() }}
{{ a + b }}Property binding
[src]="url"
[disabled]="x"
[class.active]="y"Event binding
(click)="fn()"
(input)="onInput($event)"
(keyup.enter)="submit()"Two-way binding
[(ngModel)]="wartosc"
// Wymaga FormsModule!*ngIf
<p *ngIf="warunek">...</p>
*ngIf="x; else other"
<ng-template #other>*ngFor
*ngFor="let x of arr"
*ngFor="let x of arr; let i = index"
trackBy: trackFn[ngClass]
[ngClass]="{'active': x, 'disabled': y}"@Input
@Input() nazwa = '';
// rodzic: [nazwa]="'X'"@Output
@Output() ev = new EventEmitter();
this.ev.emit(data);
// rodzic: (ev)="fn($event)"Serwis
@Injectable({providedIn:'root'})
constructor(private srv: XService)🐛 Typowe błędy Angular
Can't bind to 'ngModel' since it isn't a known property
→ Dodaj FormsModule do imports w module// ❌ *ngFor="x of arr"
// ✅ *ngFor="let x of arr"Component X is not part of any NgModule
→ Dodaj do declarations w @NgModule// ❌ this.arr.push(x)
// ✅ this.arr = [...this.arr, x]No provider for XService
→ Dodaj @Injectable({providedIn:'root'})// ❌ <app-Header> (wielkość liter!)
// ✅ <app-header>// ❌ *ngFor="let x of data$"
// ✅ *ngFor="let x of data$ | async"// ✅ trackById(index: number, item: any) {
// return item.id;
// }🖥️ Co to JavaFX?
„JavaFX to framework do tworzenia aplikacji desktopowych w Javie. Na egzaminie INF.04 część I to właśnie aplikacja okienkowa — najczęściej w JavaFX lub Swing."
Czym jest JavaFX?
JavaFX to nowoczesny framework GUI dla Javy, który zastąpił starszy Swing. Pozwala tworzyć aplikacje okienkowe z przyciskami, polami tekstowymi, tabelami i innymi kontrolkami.
Porównanie z innymi technologiami
| Technologia | Typ | Język | Zastosowanie |
|---|---|---|---|
| JavaFX | Desktop | Java | Aplikacje okienkowe Windows/Mac/Linux |
| Swing | Desktop | Java | Starsze aplikacje desktop (nadal na egzaminie) |
| React | Web | JavaScript | Strony i aplikacje webowe |
| Android | Mobile | Java/Kotlin | Aplikacje na telefony |
Dlaczego JavaFX na egzaminie?
Część I egzaminu to aplikacja desktopowa. Możesz wybrać JavaFX, Swing lub C# WinForms. JavaFX jest najpopularniejszy i najłatwiejszy do opanowania.
Zalety JavaFX
- Scene Builder — wizualne projektowanie interfejsu (drag & drop)
- FXML — oddzielenie wyglądu (XML) od logiki (Java)
- CSS — możesz stylować kontrolki jak w HTML
- Nowoczesny — animacje, efekty, multimedia
Podstawowe pojęcia
| Pojęcie | Opis |
|---|---|
Stage | Okno aplikacji (główne lub dialog) |
Scene | Zawartość okna (kontrolki + layout) |
Node | Dowolny element UI (Button, Label, VBox...) |
Controller | Klasa z logiką obsługi zdarzeń |
FXML | Plik XML definiujący układ kontrolek |
Pierwszy program
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.stage.Stage;
public class HelloFX extends Application {
@Override
public void start(Stage stage) {
Label label = new Label("Witaj w JavaFX!");
Scene scene = new Scene(label, 300, 200);
stage.setTitle("Moja aplikacja");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
„Jakie widzisz podobieństwa między JavaFX a React? Podpowiedź: pomyśl o komponentach i oddzieleniu widoku od logiki."
📝 Zadanie do teorii
3 elementy: Stage (okno), Scene (scena/zawartość), Node (kontrolka/element UI)
Odpowiednik komponentu: W JavaFX to Controller + FXML — razem tworzą zamkniętą całość z widokiem i logiką, podobnie jak komponent React.
🛠️ NetBeans + Scene Builder
„Na egzaminie masz NetBeans z zainstalowanym Scene Builderem. To pozwala projektować interfejs wizualnie — przeciągasz przyciski, pola tekstowe, a NetBeans generuje kod FXML."
NetBeans IDE
NetBeans to darmowe IDE do Javy, które jest standardem na egzaminie INF.04. Ma wbudowane wsparcie dla JavaFX.
Tworzenie projektu JavaFX
- File → New Project
- Wybierz JavaFX → JavaFX FXML Application
- Podaj nazwę projektu (np.
Kalkulator) - NetBeans utworzy 3 pliki:
Main.java,FXMLDocument.fxml,FXMLDocumentController.java
Scene Builder
Scene Builder to wizualny edytor FXML — projektujesz interfejs metodą drag & drop.
Otwieranie Scene Builder
- Kliknij prawym na plik
.fxml - Wybierz Open in Scene Builder
- Przeciągaj kontrolki z lewego panelu na scenę
Główne sekcje Scene Buildera
| Sekcja | Opis |
|---|---|
| Library (lewy panel) | Paleta kontrolek do przeciągania |
| Hierarchy (lewy dolny) | Drzewo elementów (co zawiera co) |
| Content (środek) | Podgląd interfejsu |
| Inspector (prawy panel) | Właściwości zaznaczonego elementu |
💡 W Inspector → Code ustaw fx:id dla kontrolek, które chcesz obsługiwać w kodzie Java. Np. fx:id="btnDodaj"
Łączenie FXML z Controllerem
// W FXMLDocumentController.java:
public class FXMLDocumentController implements Initializable {
@FXML
private Label lblWynik; // Połączenie z fx:id="lblWynik"
@FXML
private TextField txtLiczba;
@FXML
private void handleButtonClick(ActionEvent event) {
// Obsługa kliknięcia przycisku
lblWynik.setText("Kliknięto!");
}
@Override
public void initialize(URL url, ResourceBundle rb) {
// Kod uruchamiany przy starcie
}
}
📝 Zadanie do teorii
- Library — paleta kontrolek (Button, Label, TextField...)
- Hierarchy — drzewo elementów, pokazuje strukturę zagnieżdżenia
- Content — podgląd projektowanego interfejsu
- Inspector — właściwości, layout i kod (fx:id, onAction)
📁 Struktura projektu JavaFX
„Projekt JavaFX w NetBeans ma standardową strukturę. Najważniejsze to wiedzieć gdzie jest FXML, gdzie Controller, i jak są połączone."
Typowa struktura
MojProjekt/
├── src/
│ └── mojprojekt/
│ ├── MojProjekt.java # Klasa główna (main)
│ ├── FXMLDocument.fxml # Układ interfejsu
│ └── FXMLDocumentController.java # Logika
├── build/
└── nbproject/ # Pliki NetBeans
Opis plików
| Plik | Rola |
|---|---|
MojProjekt.java | Punkt wejścia, ładuje FXML i pokazuje okno |
FXMLDocument.fxml | Definicja UI w XML (kontrolki, layout) |
FXMLDocumentController.java | Obsługa zdarzeń, logika biznesowa |
Plik główny (Main)
package mojprojekt;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class MojProjekt extends Application {
@Override
public void start(Stage stage) throws Exception {
// Ładowanie pliku FXML
Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
Scene scene = new Scene(root);
stage.setTitle("Moja Aplikacja");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Plik FXML
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns:fx="http://javafx.com/fxml"
fx:controller="mojprojekt.FXMLDocumentController"
spacing="10" alignment="CENTER">
<Label text="Witaj!" fx:id="lblPowitanie"/>
<Button text="Kliknij" onAction="#handleKlik"/>
</VBox>
fx:controller musi wskazywać pełną nazwę klasy controllera (z pakietem).
📝 Zadanie do teorii
fx:controller w pliku FXML? Co się stanie, jeśli go pominiesz?
fx:controller wskazuje klasę Java, która obsługuje zdarzenia i ma dostęp do kontrolek (przez @FXML).
Bez tego atrybutu: kontrolki będą się wyświetlać, ale kliknięcia przycisków nie będą działać (brak połączenia z metodami handlera).
📐 Layouty (VBox, HBox, GridPane)
„Layouty to kontenery, które automatycznie rozmieszczają kontrolki. Podobnie jak flexbox w CSS — nie musisz podawać dokładnych pozycji."
Najważniejsze layouty
| Layout | Rozmieszczenie | Zastosowanie |
|---|---|---|
VBox | Pionowo (w kolumnie) | Formularz, lista opcji |
HBox | Poziomo (w wierszu) | Pasek narzędzi, przyciski obok siebie |
GridPane | Siatka (wiersze × kolumny) | Kalkulator, formularze |
BorderPane | 5 stref (top, left, center, right, bottom) | Główny layout aplikacji |
AnchorPane | Pozycjonowanie względem krawędzi | Dokładne pozycjonowanie |
StackPane | Warstwy (jeden na drugim) | Overlay, nakładki |
VBox — układ pionowy
<VBox spacing="10" alignment="CENTER" padding="20">
<Label text="Formularz"/>
<TextField promptText="Imię"/>
<TextField promptText="Nazwisko"/>
<Button text="Zapisz"/>
</VBox>
HBox — układ poziomy
<HBox spacing="10" alignment="CENTER">
<Button text="OK"/>
<Button text="Anuluj"/>
<Button text="Pomoc"/>
</HBox>
GridPane — siatka
<GridPane hgap="10" vgap="10">
<Label text="Login:" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<TextField GridPane.rowIndex="0" GridPane.columnIndex="1"/>
<Label text="Hasło:" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
<PasswordField GridPane.rowIndex="1" GridPane.columnIndex="1"/>
<Button text="Zaloguj" GridPane.rowIndex="2" GridPane.columnIndex="1"/>
</GridPane>
BorderPane
<BorderPane>
<top><MenuBar>...</MenuBar></top>
<left><VBox>Menu boczne</VBox></left>
<center><TextArea/></center>
<bottom><Label text="Status"/></bottom>
</BorderPane>
💡 Na egzaminie najczęściej używasz VBox lub GridPane dla prostych formularzy. BorderPane dla aplikacji z menu.
📝 Zadanie do teorii
GridPane — naturalnie mapuje przyciski kalkulatora na siatkę 4×4. Każdy przycisk ma swój wiersz i kolumnę.
Alternatywnie: VBox z zagnieżdżonymi HBox (każdy wiersz to HBox z 4 przyciskami).
🔘 Button i Label
„Button i Label to podstawowe kontrolki. Label wyświetla tekst, Button reaguje na kliknięcia. To są Twoje chleb i masło w JavaFX."
Label — etykieta tekstowa
<!-- W FXML -->
<Label text="Witaj!" fx:id="lblWynik"/>
<Label text="Pogrubiony" style="-fx-font-weight: bold; -fx-font-size: 18px;"/>
// W Controller
@FXML private Label lblWynik;
// Zmiana tekstu
lblWynik.setText("Nowy tekst");
// Pobranie tekstu
String tekst = lblWynik.getText();
Button — przycisk
<!-- W FXML -->
<Button text="Kliknij mnie" fx:id="btnAkcja" onAction="#handleKlik"/>
// W Controller
@FXML private Button btnAkcja;
@FXML
private void handleKlik(ActionEvent event) {
System.out.println("Kliknięto przycisk!");
lblWynik.setText("Kliknięto!");
// Wyłączenie przycisku
btnAkcja.setDisable(true);
}
Właściwości Button
| Właściwość | Metoda | Opis |
|---|---|---|
| Tekst | setText() / getText() | Napis na przycisku |
| Wyłączony | setDisable(true/false) | Szary, nieklikany |
| Widoczny | setVisible(true/false) | Ukryj/pokaż |
| Styl | setStyle("-fx-...") | CSS inline |
Stylowanie CSS
<Button text="Zielony"
style="-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-size: 14px;"/>
<Label style="-fx-text-fill: red; -fx-font-weight: bold;"/>
W React: <button onClick={handleClick}>
W JavaFX: <Button onAction="#handleClick">
Różnica: React używa camelCase, JavaFX używa # przed nazwą metody.
📝 Zadanie do teorii
@FXML private TextField txtImie;
@FXML private Label lblWynik;
@FXML
private void handlePowitaj(ActionEvent event) {
String imie = txtImie.getText();
lblWynik.setText("Witaj " + imie + "!");
}
✏️ TextField i TextArea
„TextField to pole jednoliniowe — login, email, liczba. TextArea to pole wieloliniowe — opis, komentarz, notatka."
TextField — pole jednoliniowe
<TextField fx:id="txtImie" promptText="Wpisz imię..."/>
<TextField fx:id="txtLiczba" prefWidth="100"/>
<PasswordField fx:id="txtHaslo" promptText="Hasło"/>
// Pobranie tekstu
String imie = txtImie.getText();
// Ustawienie tekstu
txtImie.setText("Jan");
// Wyczyszczenie
txtImie.clear();
// Konwersja na liczbę (UWAGA: może rzucić wyjątek!)
int liczba = Integer.parseInt(txtLiczba.getText());
double wartosc = Double.parseDouble(txtLiczba.getText());
TextArea — pole wieloliniowe
<TextArea fx:id="txtOpis"
promptText="Wpisz opis..."
prefRowCount="5"
prefColumnCount="30"
wrapText="true"/>
// Pobranie całego tekstu
String opis = txtOpis.getText();
// Dodanie tekstu na końcu
txtOpis.appendText("\nNowa linia");
// Zaznaczenie całego tekstu
txtOpis.selectAll();
Walidacja liczb
@FXML
private void handleOblicz(ActionEvent event) {
try {
double liczba = Double.parseDouble(txtLiczba.getText());
lblWynik.setText("Wynik: " + (liczba * 2));
} catch (NumberFormatException e) {
lblWynik.setText("Błąd: podaj liczbę!");
}
}
💡 Na egzaminie ZAWSZE obsługuj wyjątki przy konwersji! try-catch dla NumberFormatException.
📝 Zadanie do teorii
getText() a getSelectedText()? Kiedy użyjesz każdej?
getText()— zwraca CAŁY tekst z polagetSelectedText()— zwraca tylko zaznaczony fragment
Użyjesz: getText() przy odczycie wartości formularza, getSelectedText() przy operacjach kopiuj/wytnij.
☑️ CheckBox i RadioButton
„CheckBox to 'tak/nie' — można zaznaczyć wiele. RadioButton to 'wybierz jedno' — grupowane razem."
CheckBox — wielokrotny wybór
<CheckBox fx:id="chkSer" text="Ser"/>
<CheckBox fx:id="chkSzynka" text="Szynka"/>
<CheckBox fx:id="chkPieczarki" text="Pieczarki" selected="true"/>
// Sprawdzenie czy zaznaczony
if (chkSer.isSelected()) {
System.out.println("Wybrano ser");
}
// Zaznaczenie programowe
chkSzynka.setSelected(true);
// Lista wybranych
String dodatki = "";
if (chkSer.isSelected()) dodatki += "ser ";
if (chkSzynka.isSelected()) dodatki += "szynka ";
if (chkPieczarki.isSelected()) dodatki += "pieczarki";
RadioButton — pojedynczy wybór
<!-- Grupowanie przez ToggleGroup -->
<ToggleGroup fx:id="grupaRozmiar"/>
<RadioButton fx:id="rbMala" text="Mała" toggleGroup="$grupaRozmiar"/>
<RadioButton fx:id="rbSrednia" text="Średnia" toggleGroup="$grupaRozmiar" selected="true"/>
<RadioButton fx:id="rbDuza" text="Duża" toggleGroup="$grupaRozmiar"/>
@FXML private ToggleGroup grupaRozmiar;
@FXML private RadioButton rbMala, rbSrednia, rbDuza;
// Sprawdzenie który wybrany
if (rbMala.isSelected()) {
System.out.println("Mała pizza");
}
// Alternatywnie - przez grupę
RadioButton wybrany = (RadioButton) grupaRozmiar.getSelectedToggle();
String rozmiar = wybrany.getText(); // "Mała", "Średnia" lub "Duża"
ToggleGroup, żeby działało "wybierz jeden".
📝 Zadanie do teorii
- Rozmiar: RadioButton z ToggleGroup — można wybrać tylko jeden rozmiar
- Dodatki: CheckBox — można wybrać wiele dodatków naraz
📋 ComboBox i ChoiceBox
„ComboBox i ChoiceBox to listy rozwijane — wybierasz jedną opcję z wielu. ComboBox pozwala też wpisać własną wartość."
ComboBox — lista rozwijana
<ComboBox fx:id="cmbMiasto" promptText="Wybierz miasto">
<items>
<FXCollections fx:factory="observableArrayList">
<String fx:value="Warszawa"/>
<String fx:value="Kraków"/>
<String fx:value="Gdańsk"/>
</FXCollections>
</items>
</ComboBox>
@FXML private ComboBox<String> cmbMiasto;
// Inicjalizacja w kodzie (łatwiejsze!)
@Override
public void initialize(URL url, ResourceBundle rb) {
cmbMiasto.getItems().addAll("Warszawa", "Kraków", "Gdańsk", "Wrocław");
cmbMiasto.setValue("Warszawa"); // Domyślna wartość
}
// Pobranie wybranej wartości
String miasto = cmbMiasto.getValue();
// Obsługa zmiany wyboru
cmbMiasto.setOnAction(e -> {
System.out.println("Wybrano: " + cmbMiasto.getValue());
});
ChoiceBox — prostsza alternatywa
@FXML private ChoiceBox<String> chbKategoria;
@Override
public void initialize(URL url, ResourceBundle rb) {
chbKategoria.getItems().addAll("Elektronika", "AGD", "Meble");
chbKategoria.setValue("Elektronika");
}
Różnice ComboBox vs ChoiceBox
| Cecha | ComboBox | ChoiceBox |
|---|---|---|
| Edytowalny | Tak (można wpisać) | Nie |
| Placeholder | promptText | Brak |
| Wydajność | Lepsza dla dużych list | Dla małych list |
📝 Zadanie do teorii
cmbMiasto.getItems().add("Poznań"); // Na końcu
cmbMiasto.getItems().add(0, "Łódź"); // Na początku
cmbMiasto.getItems().addAll("A", "B", "C"); // Wiele naraz
⚡ Obsługa zdarzeń
„Zdarzenia to reakcje na akcje użytkownika — kliknięcie, wpisanie tekstu, wybranie opcji. To serce interaktywności."
Sposób 1: onAction w FXML
<Button text="Kliknij" onAction="#handleKlik"/>
@FXML
private void handleKlik(ActionEvent event) {
System.out.println("Kliknięto!");
}
Sposób 2: setOnAction w kodzie
@Override
public void initialize(URL url, ResourceBundle rb) {
btnOblicz.setOnAction(event -> {
// kod obsługi
lblWynik.setText("Obliczono!");
});
}
Typy zdarzeń
| Zdarzenie | Kontrolka | Kiedy |
|---|---|---|
onAction | Button, TextField (Enter) | Kliknięcie / Enter |
onKeyPressed | Wszystkie | Naciśnięcie klawisza |
onMouseClicked | Wszystkie | Kliknięcie myszą |
setOnAction | ComboBox, ChoiceBox | Zmiana wyboru |
Przykład: walidacja przy Enter
txtLiczba.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) {
oblicz();
}
});
private void oblicz() {
try {
double x = Double.parseDouble(txtLiczba.getText());
lblWynik.setText("Kwadrat: " + (x * x));
} catch (NumberFormatException e) {
lblWynik.setText("Błąd!");
}
}
Źródło zdarzenia
@FXML
private void handlePrzycisk(ActionEvent event) {
Button btn = (Button) event.getSource();
String tekst = btn.getText(); // Który przycisk kliknięto
if (tekst.equals("Dodaj")) {
// ...
} else if (tekst.equals("Usuń")) {
// ...
}
}
📝 Zadanie do teorii
Jedna metoda! Użyj event.getSource() do sprawdzenia który przycisk kliknięto:
@FXML
private void handleCyfra(ActionEvent event) {
Button btn = (Button) event.getSource();
txtWyswietlacz.appendText(btn.getText());
}
🔗 FXML + Controller
„FXML definiuje CO wyświetlić. Controller definiuje CO ZROBIĆ. Rozdzielenie widoku od logiki — jak HTML + JavaScript."
Anatomia pliku FXML
<?xml version="1.0" encoding="UTF-8"?>
<!-- Importy (jak import w Java) -->
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<!-- Główny kontener + wskazanie controllera -->
<VBox xmlns:fx="http://javafx.com/fxml"
fx:controller="kalkulator.KalkulatorController"
spacing="10" alignment="CENTER" style="-fx-padding: 20;">
<!-- fx:id = nazwa pola w controllerze -->
<TextField fx:id="txtWynik" editable="false"/>
<!-- onAction = nazwa metody z # -->
<Button text="Oblicz" onAction="#handleOblicz"/>
</VBox>
Anatomia Controllera
package kalkulator;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import java.net.URL;
import java.util.ResourceBundle;
public class KalkulatorController implements Initializable {
// Pola połączone z FXML (fx:id)
@FXML private TextField txtWynik;
@FXML private Button btnOblicz;
// Metoda uruchamiana przy starcie
@Override
public void initialize(URL url, ResourceBundle rb) {
// Inicjalizacja, np. wypełnienie ComboBox
txtWynik.setText("0");
}
// Metoda obsługi zdarzenia (onAction)
@FXML
private void handleOblicz(ActionEvent event) {
// Logika
}
}
Diagram połączeń
FXML Controller
───────────────────── ─────────────────────
fx:controller="..." ───► public class ... implements Initializable
fx:id="txtWynik" ───► @FXML private TextField txtWynik
onAction="#handleOblicz"───► @FXML private void handleOblicz(...)
- Brak
@FXMLprzed polem/metodą → NullPointerException - Literówka w fx:id vs nazwa pola → NullPointerException
- Brak # przed nazwą metody w onAction
📝 Zadanie do teorii
@FXML? Co się stanie, jeśli ją pominiesz?
@FXML oznacza, że pole/metoda ma być wstrzyknięte z pliku FXML (na podstawie fx:id lub onAction).
Bez @FXML: pole będzie null, bo JavaFX nie wie, że ma je połączyć z kontrolką w FXML. Wywołanie metody na takim polu → NullPointerException.
🔥 Kalkulator — Rozgrzewka
„Zanim zbudujemy kalkulator, sprawdźmy czy pamiętasz podstawy JavaFX."
Mini Quiz
- Jaki layout najlepiej użyć do siatki przycisków kalkulatora?
- Jak pobrać tekst z TextField?
- Co oznacza
@FXML? - Jak obsłużyć błąd konwersji String na double?
- GridPane — siatka wierszy i kolumn
txtPole.getText()- Adnotacja łącząca pole/metodę z elementem FXML
try-catchdlaNumberFormatException
📋 Kalkulator — Instrukcja
„To Twoje egzaminowe zadanie. Przeczytaj uważnie wymagania."
Treść zadania
Wykonaj aplikację okienkową "Kalkulator" spełniającą wymagania:
- Interfejs zawiera: pole wyświetlacza, przyciski cyfr 0-9, przyciski operacji (+, -, *, /), przycisk "=" i "C"
- Kliknięcie cyfry dopisuje ją do wyświetlacza
- Kliknięcie operacji zapamiętuje liczbę i operację
- Kliknięcie "=" wykonuje obliczenie i wyświetla wynik
- Kliknięcie "C" czyści wyświetlacz
- Obsłuż dzielenie przez zero (komunikat błędu)
Wymagane komponenty
| Kontrolka | fx:id | Opis |
|---|---|---|
| TextField | txtWyswietlacz | Wyświetlacz (tylko odczyt) |
| Button ×10 | btn0-btn9 | Cyfry |
| Button ×4 | btnDodaj, btnOdejmij, btnMnoz, btnDziel | Operacje |
| Button | btnRowna | Oblicz wynik |
| Button | btnCzysc | Wyczyść |
🏗️ Kalkulator — Budowa UI
„Zaczynamy od Scene Buildera. Będziemy budować interfejs krok po kroku."
Krok 1: Utwórz projekt
- File → New Project → JavaFX FXML Application
- Nazwa:
Kalkulator - Otwórz
FXMLDocument.fxmlw Scene Builder
Krok 2: Struktura layoutu
- Usuń domyślny AnchorPane, dodaj VBox
- W VBox dodaj TextField (wyświetlacz)
- Pod TextField dodaj GridPane (4 kolumny × 4 wiersze)
Krok 3: Dodaj przyciski
Wiersz 0: [7] [8] [9] [/]
Wiersz 1: [4] [5] [6] [*]
Wiersz 2: [1] [2] [3] [-]
Wiersz 3: [0] [C] [=] [+]
Krok 4: Ustaw fx:id
W Inspector → Code ustaw fx:id dla każdej kontrolki:
- TextField:
txtWyswietlacz - Przyciski:
btn0,btn1, ...btn9 - Operacje:
btnDodaj,btnOdejmij,btnMnoz,btnDziel - Specjalne:
btnRowna,btnCzysc
Krok 5: Ustaw onAction
- Wszystkie cyfry:
#handleCyfra - Wszystkie operacje:
#handleOperacja - btnRowna:
#handleRowna - btnCzysc:
#handleCzysc
💻 Kalkulator — Kod
„Teraz implementujemy logikę. Pokażę Ci wzorcowy kod controllera."
FXMLDocumentController.java
package kalkulator;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import java.net.URL;
import java.util.ResourceBundle;
public class FXMLDocumentController implements Initializable {
@FXML private TextField txtWyswietlacz;
private double liczba1 = 0;
private String operacja = "";
private boolean nowaLiczba = true;
@Override
public void initialize(URL url, ResourceBundle rb) {
txtWyswietlacz.setEditable(false);
txtWyswietlacz.setText("0");
}
@FXML
private void handleCyfra(ActionEvent event) {
Button btn = (Button) event.getSource();
String cyfra = btn.getText();
if (nowaLiczba) {
txtWyswietlacz.setText(cyfra);
nowaLiczba = false;
} else {
txtWyswietlacz.setText(txtWyswietlacz.getText() + cyfra);
}
}
@FXML
private void handleOperacja(ActionEvent event) {
Button btn = (Button) event.getSource();
operacja = btn.getText();
try {
liczba1 = Double.parseDouble(txtWyswietlacz.getText());
nowaLiczba = true;
} catch (NumberFormatException e) {
txtWyswietlacz.setText("Błąd");
}
}
@FXML
private void handleRowna(ActionEvent event) {
try {
double liczba2 = Double.parseDouble(txtWyswietlacz.getText());
double wynik = 0;
switch (operacja) {
case "+": wynik = liczba1 + liczba2; break;
case "-": wynik = liczba1 - liczba2; break;
case "*": wynik = liczba1 * liczba2; break;
case "/":
if (liczba2 == 0) {
txtWyswietlacz.setText("Nie dziel przez 0!");
return;
}
wynik = liczba1 / liczba2;
break;
}
txtWyswietlacz.setText(String.valueOf(wynik));
nowaLiczba = true;
} catch (NumberFormatException e) {
txtWyswietlacz.setText("Błąd");
}
}
@FXML
private void handleCzysc(ActionEvent event) {
txtWyswietlacz.setText("0");
liczba1 = 0;
operacja = "";
nowaLiczba = true;
}
}
✅ Kalkulator — Sprawdzian
„Sprawdźmy, czy Twoje rozwiązanie działa poprawnie."
Testy do wykonania
| Test | Oczekiwany wynik |
|---|---|
| 2 + 3 = | 5.0 |
| 10 - 4 = | 6.0 |
| 6 * 7 = | 42.0 |
| 15 / 3 = | 5.0 |
| 5 / 0 = | "Nie dziel przez 0!" lub podobny komunikat |
| C | Wyświetlacz = "0" |
Kryteria oceny (INF.04)
- ☐ Interfejs zgodny z wymaganiami (wszystkie przyciski)
- ☐ Cyfry dopisują się do wyświetlacza
- ☐ Operacje działają poprawnie
- ☐ Obsłużone dzielenie przez zero
- ☐ Przycisk C czyści kalkulator
- ☐ Kod kompiluje się bez błędów
🔥 Lista zadań — Rozgrzewka
„Aplikacja Todo wymaga ListView. Przypomnijmy sobie jak działa."
Mini Quiz
- Jak dodać element do ListView?
- Jak pobrać zaznaczony element?
- Jak usunąć element z listy?
listView.getItems().add("element")listView.getSelectionModel().getSelectedItem()listView.getItems().remove(obiekt)lub.remove(index)
📋 Lista zadań — Instrukcja
„Klasyczna aplikacja Todo — idealna do nauki CRUD w JavaFX."
Treść zadania
Wykonaj aplikację "Lista zadań do zrobienia":
- Pole tekstowe do wpisania nowego zadania
- Przycisk "Dodaj" — dodaje zadanie do listy
- ListView wyświetlający wszystkie zadania
- Przycisk "Usuń" — usuwa zaznaczone zadanie
- Przycisk "Wyczyść wszystko" — usuwa wszystkie zadania
- Walidacja: nie można dodać pustego zadania
🏗️ Lista zadań — Budowa UI
„Prosty układ: VBox z polem, przyciskami i listą."
Struktura
<VBox spacing="10" style="-fx-padding: 20;">
<HBox spacing="10">
<TextField fx:id="txtZadanie" promptText="Nowe zadanie..." HBox.hgrow="ALWAYS"/>
<Button text="Dodaj" onAction="#handleDodaj"/>
</HBox>
<ListView fx:id="listZadania" VBox.vgrow="ALWAYS"/>
<HBox spacing="10">
<Button text="Usuń zaznaczone" onAction="#handleUsun"/>
<Button text="Wyczyść wszystko" onAction="#handleWyczysc"/>
</HBox>
</VBox>
💻 Lista zadań — Kod
package listaZadan;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import java.net.URL;
import java.util.ResourceBundle;
public class FXMLDocumentController implements Initializable {
@FXML private TextField txtZadanie;
@FXML private ListView<String> listZadania;
@Override
public void initialize(URL url, ResourceBundle rb) {
// Opcjonalnie: przykładowe zadania
listZadania.getItems().addAll(
"Nauczyć się JavaFX",
"Zdać egzamin INF.04"
);
}
@FXML
private void handleDodaj(ActionEvent event) {
String zadanie = txtZadanie.getText().trim();
if (zadanie.isEmpty()) {
pokazAlert("Błąd", "Wpisz treść zadania!");
return;
}
listZadania.getItems().add(zadanie);
txtZadanie.clear();
txtZadanie.requestFocus();
}
@FXML
private void handleUsun(ActionEvent event) {
String wybrany = listZadania.getSelectionModel().getSelectedItem();
if (wybrany == null) {
pokazAlert("Błąd", "Zaznacz zadanie do usunięcia!");
return;
}
listZadania.getItems().remove(wybrany);
}
@FXML
private void handleWyczysc(ActionEvent event) {
listZadania.getItems().clear();
}
private void pokazAlert(String tytul, String tresc) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle(tytul);
alert.setHeaderText(null);
alert.setContentText(tresc);
alert.showAndWait();
}
}
✅ Lista zadań — Sprawdzian
Testy
| Test | Oczekiwany wynik |
|---|---|
| Dodaj "Test" | "Test" pojawia się na liście |
| Dodaj puste | Alert z błędem |
| Zaznacz i usuń | Element znika z listy |
| Usuń bez zaznaczenia | Alert z błędem |
| Wyczyść wszystko | Lista pusta |
🔥 Rejestr produktów — Rozgrzewka
„To zadanie wymaga TableView — tabeli z kolumnami. Powtórzmy jak działa."
Mini Quiz
- Jak powiązać kolumnę TableView z polem obiektu?
- Jaka klasa reprezentuje wiersz w tabeli?
- Jak dodać wiersz do tabeli?
kolumna.setCellValueFactory(new PropertyValueFactory<>("nazwaPolaWKlasie"))- Własna klasa (np.
Produkt) z polami i getterami tableView.getItems().add(new Produkt(...))
📋 Rejestr produktów — Instrukcja
Treść zadania
Wykonaj aplikację "Rejestr produktów":
- Pola: nazwa (TextField), cena (TextField), ilość (TextField)
- TableView z kolumnami: Nazwa, Cena, Ilość, Wartość (cena×ilość)
- Przycisk "Dodaj" — dodaje produkt do tabeli
- Przycisk "Usuń" — usuwa zaznaczony produkt
- Label pokazujący sumę wartości wszystkich produktów
- Walidacja: cena i ilość muszą być liczbami
🏗️ Rejestr produktów — Budowa UI
Struktura layoutu
<VBox spacing="10" style="-fx-padding: 20;">
<HBox spacing="10">
<TextField fx:id="txtNazwa" promptText="Nazwa"/>
<TextField fx:id="txtCena" promptText="Cena" prefWidth="80"/>
<TextField fx:id="txtIlosc" promptText="Ilość" prefWidth="80"/>
<Button text="Dodaj" onAction="#handleDodaj"/>
</HBox>
<TableView fx:id="tabProdukty" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="colNazwa" text="Nazwa" prefWidth="150"/>
<TableColumn fx:id="colCena" text="Cena" prefWidth="80"/>
<TableColumn fx:id="colIlosc" text="Ilość" prefWidth="80"/>
<TableColumn fx:id="colWartosc" text="Wartość" prefWidth="100"/>
</columns>
</TableView>
<HBox spacing="10">
<Button text="Usuń zaznaczony" onAction="#handleUsun"/>
<Label text="Suma: "/>
<Label fx:id="lblSuma" text="0.00 zł"/>
</HBox>
</VBox>
💻 Rejestr produktów — Kod
Klasa Produkt.java
package rejestr;
public class Produkt {
private String nazwa;
private double cena;
private int ilosc;
public Produkt(String nazwa, double cena, int ilosc) {
this.nazwa = nazwa;
this.cena = cena;
this.ilosc = ilosc;
}
// Gettery (wymagane przez PropertyValueFactory!)
public String getNazwa() { return nazwa; }
public double getCena() { return cena; }
public int getIlosc() { return ilosc; }
public double getWartosc() { return cena * ilosc; }
}
Controller
package rejestr;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import java.net.URL;
import java.util.ResourceBundle;
public class FXMLDocumentController implements Initializable {
@FXML private TextField txtNazwa, txtCena, txtIlosc;
@FXML private TableView<Produkt> tabProdukty;
@FXML private TableColumn<Produkt, String> colNazwa;
@FXML private TableColumn<Produkt, Double> colCena, colWartosc;
@FXML private TableColumn<Produkt, Integer> colIlosc;
@FXML private Label lblSuma;
@Override
public void initialize(URL url, ResourceBundle rb) {
// Powiązanie kolumn z polami klasy Produkt
colNazwa.setCellValueFactory(new PropertyValueFactory<>("nazwa"));
colCena.setCellValueFactory(new PropertyValueFactory<>("cena"));
colIlosc.setCellValueFactory(new PropertyValueFactory<>("ilosc"));
colWartosc.setCellValueFactory(new PropertyValueFactory<>("wartosc"));
}
@FXML
private void handleDodaj(ActionEvent event) {
try {
String nazwa = txtNazwa.getText().trim();
double cena = Double.parseDouble(txtCena.getText());
int ilosc = Integer.parseInt(txtIlosc.getText());
if (nazwa.isEmpty()) {
pokazAlert("Podaj nazwę produktu!");
return;
}
tabProdukty.getItems().add(new Produkt(nazwa, cena, ilosc));
obliczSume();
wyczyscPola();
} catch (NumberFormatException e) {
pokazAlert("Cena i ilość muszą być liczbami!");
}
}
@FXML
private void handleUsun(ActionEvent event) {
Produkt wybrany = tabProdukty.getSelectionModel().getSelectedItem();
if (wybrany != null) {
tabProdukty.getItems().remove(wybrany);
obliczSume();
}
}
private void obliczSume() {
double suma = 0;
for (Produkt p : tabProdukty.getItems()) {
suma += p.getWartosc();
}
lblSuma.setText(String.format("%.2f zł", suma));
}
private void wyczyscPola() {
txtNazwa.clear();
txtCena.clear();
txtIlosc.clear();
txtNazwa.requestFocus();
}
private void pokazAlert(String tresc) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setContentText(tresc);
alert.showAndWait();
}
}
✅ Rejestr produktów — Sprawdzian
Testy
| Test | Oczekiwany wynik |
|---|---|
| Dodaj "Jabłko", 2.50, 10 | Wiersz w tabeli, Wartość=25.00 |
| Dodaj "Mleko", 3.00, 5 | Drugi wiersz, Suma=40.00 zł |
| Dodaj z tekstem w cenie | Alert z błędem |
| Usuń zaznaczony | Wiersz znika, suma się aktualizuje |
🔥 Dziennik ocen — Rozgrzewka
„To zadanie łączy ComboBox, TableView i obliczenia. Powtórzmy ComboBox."
Mini Quiz
- Jak dodać opcje do ComboBox w metodzie initialize?
- Jak pobrać wybraną wartość z ComboBox?
- Jak obliczyć średnią z listy liczb?
cmbPrzedmiot.getItems().addAll("Matematyka", "Fizyka")cmbPrzedmiot.getValue()- Suma wszystkich / ilość elementów
📋 Dziennik ocen — Instrukcja
Treść zadania
Wykonaj aplikację "Dziennik ocen ucznia":
- ComboBox z przedmiotami (Matematyka, Fizyka, Informatyka, Polski, Angielski)
- Pole tekstowe na imię ucznia
- ComboBox lub RadioButton na ocenę (1-6)
- TableView: Uczeń, Przedmiot, Ocena
- Przycisk "Dodaj ocenę"
- Label ze średnią wszystkich ocen
- Możliwość filtrowania po przedmiocie
🏗️ Dziennik ocen — Budowa UI
Struktura
<VBox spacing="10" style="-fx-padding: 20;">
<HBox spacing="10">
<TextField fx:id="txtUczen" promptText="Imię ucznia"/>
<ComboBox fx:id="cmbPrzedmiot" promptText="Przedmiot"/>
<ComboBox fx:id="cmbOcena" promptText="Ocena"/>
<Button text="Dodaj" onAction="#handleDodaj"/>
</HBox>
<HBox spacing="10">
<Label text="Filtruj:"/>
<ComboBox fx:id="cmbFiltr" promptText="Wszystkie" onAction="#handleFiltr"/>
</HBox>
<TableView fx:id="tabOceny" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="colUczen" text="Uczeń" prefWidth="150"/>
<TableColumn fx:id="colPrzedmiot" text="Przedmiot" prefWidth="120"/>
<TableColumn fx:id="colOcena" text="Ocena" prefWidth="80"/>
</columns>
</TableView>
<HBox spacing="10">
<Label text="Średnia:"/>
<Label fx:id="lblSrednia" text="0.00"/>
<Button text="Usuń zaznaczoną" onAction="#handleUsun"/>
</HBox>
</VBox>
💻 Dziennik ocen — Kod
Klasa Ocena.java
package dziennik;
public class Ocena {
private String uczen;
private String przedmiot;
private int ocena;
public Ocena(String uczen, String przedmiot, int ocena) {
this.uczen = uczen;
this.przedmiot = przedmiot;
this.ocena = ocena;
}
public String getUczen() { return uczen; }
public String getPrzedmiot() { return przedmiot; }
public int getOcena() { return ocena; }
}
Controller
package dziennik;
import javafx.collections.*;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import java.net.URL;
import java.util.ResourceBundle;
public class FXMLDocumentController implements Initializable {
@FXML private TextField txtUczen;
@FXML private ComboBox<String> cmbPrzedmiot, cmbFiltr;
@FXML private ComboBox<Integer> cmbOcena;
@FXML private TableView<Ocena> tabOceny;
@FXML private TableColumn<Ocena, String> colUczen, colPrzedmiot;
@FXML private TableColumn<Ocena, Integer> colOcena;
@FXML private Label lblSrednia;
private ObservableList<Ocena> wszystkieOceny = FXCollections.observableArrayList();
@Override
public void initialize(URL url, ResourceBundle rb) {
// Przedmioty
String[] przedmioty = {"Matematyka", "Fizyka", "Informatyka", "Polski", "Angielski"};
cmbPrzedmiot.getItems().addAll(przedmioty);
cmbFiltr.getItems().add("Wszystkie");
cmbFiltr.getItems().addAll(przedmioty);
cmbFiltr.setValue("Wszystkie");
// Oceny 1-6
cmbOcena.getItems().addAll(1, 2, 3, 4, 5, 6);
// Kolumny tabeli
colUczen.setCellValueFactory(new PropertyValueFactory<>("uczen"));
colPrzedmiot.setCellValueFactory(new PropertyValueFactory<>("przedmiot"));
colOcena.setCellValueFactory(new PropertyValueFactory<>("ocena"));
}
@FXML
private void handleDodaj(ActionEvent event) {
String uczen = txtUczen.getText().trim();
String przedmiot = cmbPrzedmiot.getValue();
Integer ocena = cmbOcena.getValue();
if (uczen.isEmpty() || przedmiot == null || ocena == null) {
pokazAlert("Wypełnij wszystkie pola!");
return;
}
Ocena nowaOcena = new Ocena(uczen, przedmiot, ocena);
wszystkieOceny.add(nowaOcena);
handleFiltr(null); // Odśwież tabelę
obliczSrednia();
txtUczen.clear();
}
@FXML
private void handleFiltr(ActionEvent event) {
String filtr = cmbFiltr.getValue();
if (filtr == null || filtr.equals("Wszystkie")) {
tabOceny.setItems(wszystkieOceny);
} else {
ObservableList<Ocena> przefiltrowane = FXCollections.observableArrayList();
for (Ocena o : wszystkieOceny) {
if (o.getPrzedmiot().equals(filtr)) {
przefiltrowane.add(o);
}
}
tabOceny.setItems(przefiltrowane);
}
}
@FXML
private void handleUsun(ActionEvent event) {
Ocena wybrana = tabOceny.getSelectionModel().getSelectedItem();
if (wybrana != null) {
wszystkieOceny.remove(wybrana);
handleFiltr(null);
obliczSrednia();
}
}
private void obliczSrednia() {
if (wszystkieOceny.isEmpty()) {
lblSrednia.setText("0.00");
return;
}
double suma = 0;
for (Ocena o : wszystkieOceny) {
suma += o.getOcena();
}
double srednia = suma / wszystkieOceny.size();
lblSrednia.setText(String.format("%.2f", srednia));
}
private void pokazAlert(String tresc) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setContentText(tresc);
alert.showAndWait();
}
}
✅ Dziennik ocen — Sprawdzian
Testy
| Test | Oczekiwany wynik |
|---|---|
| Dodaj "Jan", Matematyka, 5 | Wiersz w tabeli |
| Dodaj "Anna", Fizyka, 4 | Drugi wiersz, średnia=4.50 |
| Filtruj: Matematyka | Tylko oceny z matematyki |
| Filtruj: Wszystkie | Wszystkie oceny widoczne |
| Dodaj bez przedmiotu | Alert z błędem |
📜 JavaFX — Ściąga
Podstawowe kontrolki
| Kontrolka | Tworzenie | Pobieranie wartości |
|---|---|---|
| Label | <Label fx:id="lbl"/> | lbl.getText() |
| TextField | <TextField fx:id="txt"/> | txt.getText() |
| Button | <Button onAction="#handle"/> | btn.getText() |
| CheckBox | <CheckBox fx:id="chk"/> | chk.isSelected() |
| RadioButton | <RadioButton toggleGroup="$grp"/> | rb.isSelected() |
| ComboBox | <ComboBox fx:id="cmb"/> | cmb.getValue() |
| ListView | <ListView fx:id="list"/> | list.getSelectionModel().getSelectedItem() |
| TableView | <TableView fx:id="tab"/> | tab.getSelectionModel().getSelectedItem() |
Layouty
| Layout | Opis | Kluczowe atrybuty |
|---|---|---|
| VBox | Pionowo | spacing, alignment |
| HBox | Poziomo | spacing, alignment |
| GridPane | Siatka | hgap, vgap, rowIndex, columnIndex |
| BorderPane | 5 stref | top, left, center, right, bottom |
| AnchorPane | Kotwiczenie | topAnchor, leftAnchor... |
Obsługa zdarzeń
// W FXML: onAction="#metodaNazwa"
@FXML
private void metodaNazwa(ActionEvent event) {
Button btn = (Button) event.getSource(); // Który element wywołał
}
Konwersja typów
// String → int/double (z obsługą błędów!)
try {
int liczba = Integer.parseInt(txt.getText());
double wartosc = Double.parseDouble(txt.getText());
} catch (NumberFormatException e) {
// Obsługa błędu
}
// int/double → String
lbl.setText(String.valueOf(liczba));
lbl.setText(String.format("%.2f", wartosc)); // 2 miejsca po przecinku
Alert (komunikaty)
Alert alert = new Alert(Alert.AlertType.WARNING); // lub INFORMATION, ERROR
alert.setTitle("Tytuł");
alert.setHeaderText(null);
alert.setContentText("Treść komunikatu");
alert.showAndWait();
ListView operacje
list.getItems().add("element"); // Dodaj
list.getItems().remove(obiekt); // Usuń
list.getItems().clear(); // Wyczyść
list.getSelectionModel().getSelectedItem(); // Zaznaczony
TableView setup
// W initialize():
colNazwa.setCellValueFactory(new PropertyValueFactory<>("nazwa"));
// "nazwa" = getNazwa() w klasie modelu
🐛 JavaFX — Typowe błędy
NullPointerException na kontrolce
Przyczyna: Brak @FXML lub literówka w fx:id
// ✅ Poprawnie:
@FXML private TextField txtPole; // fx:id="txtPole" w FXML
NumberFormatException
Przyczyna: Użytkownik wpisał tekst zamiast liczby
// ✅ Zawsze używaj try-catch:
try {
double x = Double.parseDouble(txt.getText());
} catch (NumberFormatException e) {
pokazAlert("Podaj liczbę!");
}
onAction nie działa
Przyczyny:
- Brak # przed nazwą metody:
onAction="#handleKlik" - Brak @FXML przed metodą
- Literówka w nazwie metody
RadioButton nie grupują się
Przyczyna: Brak ToggleGroup
<!-- ✅ Poprawnie: -->
<fx:define>
<ToggleGroup fx:id="grupa"/>
</fx:define>
<RadioButton toggleGroup="$grupa" text="A"/>
<RadioButton toggleGroup="$grupa" text="B"/>
TableView nie pokazuje danych
Przyczyny:
- Brak
setCellValueFactorydla kolumn - Nazwa w PropertyValueFactory nie zgadza się z getterem
// Klasa: getNazwa() → PropertyValueFactory("nazwa")
// ✅ "nazwa" = getNazwa (bez "get", małą literą)
Brak fx:controller
Przyczyna: Brak lub błędny atrybut fx:controller w FXML
<!-- ✅ Na początku głównego kontenera: -->
<VBox xmlns:fx="http://javafx.com/fxml"
fx:controller="pakiet.NazwaController">
📱 Android Studio — Podstawy
„Android Studio to oficjalne IDE do tworzenia aplikacji na Androida. Na egzaminie INF.04 część I możesz wybrać aplikację mobilną zamiast desktopowej."
Co to Android?
Android to system operacyjny dla urządzeń mobilnych (telefony, tablety). Aplikacje pisze się w Java lub Kotlin.
Porównanie z innymi technologiami
| Technologia | Platforma | Język |
|---|---|---|
| Android (natywny) | Android | Java / Kotlin |
| iOS (natywny) | iPhone/iPad | Swift |
| React Native | Android + iOS | JavaScript |
| Flutter | Android + iOS | Dart |
Android Studio
Android Studio to IDE oparte na IntelliJ IDEA. Oferuje:
- Edytor layoutów — wizualne projektowanie UI (jak Scene Builder)
- Emulator — testowanie bez fizycznego telefonu
- Gradle — system budowania projektu
- Logcat — logi i debugowanie
Tworzenie projektu
- File → New Project
- Wybierz Empty Activity
- Podaj nazwę (np.
MojaAplikacja) - Language: Java (lub Kotlin)
- Minimum SDK: API 21 (Android 5.0)
💡 Na egzaminie wybierz API 21 lub wyższe — obsługuje 98% urządzeń.
📝 Zadanie do teorii
- Android Studio → aplikacje mobilne, NetBeans → desktop i web
- Android Studio używa Gradle, NetBeans używa Ant/Maven
- Android Studio ma wbudowany emulator telefonu
📁 Struktura projektu Android
„Projekt Android ma ściśle określoną strukturę. Najważniejsze to wiedzieć gdzie jest layout XML, gdzie Java, i gdzie zasoby."
Typowa struktura
app/
├── src/main/
│ ├── java/com/example/mojaaplikacja/
│ │ └── MainActivity.java # Główna aktywność
│ ├── res/
│ │ ├── layout/
│ │ │ └── activity_main.xml # Layout głównego ekranu
│ │ ├── values/
│ │ │ ├── strings.xml # Teksty
│ │ │ └── colors.xml # Kolory
│ │ └── drawable/ # Obrazki
│ └── AndroidManifest.xml # Konfiguracja aplikacji
└── build.gradle # Zależności
Opis kluczowych plików
| Plik | Rola |
|---|---|
MainActivity.java | Główna klasa — odpowiednik Main w JavaFX |
activity_main.xml | Układ UI — odpowiednik FXML |
strings.xml | Wszystkie teksty (dla wielojęzyczności) |
AndroidManifest.xml | Uprawnienia, konfiguracja aktywności |
Activity — co to jest?
Activity to pojedynczy ekran aplikacji. Każda Activity ma:
- Plik
.java— logika - Plik
.xml— layout (wygląd)
Cykl życia Activity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // Ładowanie layoutu
// Tu inicjalizacja kontrolek
}
}
📝 Zadanie do teorii
setContentView(R.layout.activity_main)?
Ta linia ładuje layout z pliku res/layout/activity_main.xml i wyświetla go jako zawartość ekranu.
R.layout to automatycznie generowana klasa z referencjami do zasobów.
📐 Layouty XML
„Layouty w Androidzie działają podobnie jak w JavaFX. Masz różne kontenery do rozmieszczania kontrolek."
Najważniejsze layouty
| Layout | Opis | Odpowiednik JavaFX |
|---|---|---|
LinearLayout | Pionowo lub poziomo | VBox / HBox |
ConstraintLayout | Względne pozycjonowanie | AnchorPane |
RelativeLayout | Pozycja względem innych | — |
FrameLayout | Jeden na drugim | StackPane |
GridLayout | Siatka | GridPane |
LinearLayout — pionowy
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Witaj!"/>
<EditText
android:id="@+id/txtImie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Wpisz imię"/>
<Button
android:id="@+id/btnPowitaj"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Powitaj"/>
</LinearLayout>
Wymiary
| Wartość | Znaczenie |
|---|---|
match_parent | Wypełnij rodzica (100%) |
wrap_content | Dopasuj do zawartości |
100dp | Stała wielkość (dp = density-independent pixels) |
💡 Używaj dp dla wymiarów i sp dla tekstu — skalują się na różnych ekranach.
📝 Zadanie do teorii
match_parent a wrap_content?
match_parent— element zajmuje całą dostępną przestrzeń rodzicawrap_content— element jest tylko tak duży, jak potrzebuje (dopasowuje się do zawartości)
✏️ TextView i EditText
„TextView to odpowiednik Label, EditText to odpowiednik TextField. Podstawowe kontrolki do tekstu."
TextView — wyświetlanie tekstu
<TextView
android:id="@+id/txtWynik"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Witaj świecie!"
android:textSize="18sp"
android:textColor="#333333"
android:textStyle="bold"/>
// W MainActivity.java
TextView txtWynik = findViewById(R.id.txtWynik);
// Zmiana tekstu
txtWynik.setText("Nowy tekst");
// Pobranie tekstu
String tekst = txtWynik.getText().toString();
EditText — pole tekstowe
<EditText
android:id="@+id/edtImie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Wpisz imię..."
android:inputType="text"/>
<!-- Dla liczb: -->
<EditText
android:id="@+id/edtLiczba"
android:inputType="number"/>
<!-- Dla hasła: -->
<EditText
android:id="@+id/edtHaslo"
android:inputType="textPassword"/>
EditText edtImie = findViewById(R.id.edtImie);
// Pobranie tekstu
String imie = edtImie.getText().toString();
// Ustawienie tekstu
edtImie.setText("Jan");
// Wyczyszczenie
edtImie.setText("");
inputType — typy klawiatury
| inputType | Klawiatura |
|---|---|
text | Standardowa |
number | Tylko cyfry |
numberDecimal | Cyfry + przecinek |
textPassword | Hasło (ukryte) |
textEmailAddress | Email (z @) |
phone | Numer telefonu |
📝 Zadanie do teorii
try {
double liczba = Double.parseDouble(edtLiczba.getText().toString());
// użyj liczby
} catch (NumberFormatException e) {
txtWynik.setText("Błąd: wpisz liczbę!");
}
🔘 Button i ImageView
„Button i ImageView to kolejne podstawowe kontrolki. Zobaczysz jak obsługiwać kliknięcia."
Button
<Button
android:id="@+id/btnOblicz"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Oblicz"
android:onClick="oblicz"/>
// Sposób 1: android:onClick="oblicz" w XML
public void oblicz(View view) {
// obsługa kliknięcia
txtWynik.setText("Kliknięto!");
}
// Sposób 2: setOnClickListener w Java
Button btnOblicz = findViewById(R.id.btnOblicz);
btnOblicz.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
txtWynik.setText("Kliknięto!");
}
});
// Sposób 3: Lambda (krótsza wersja)
btnOblicz.setOnClickListener(v -> {
txtWynik.setText("Kliknięto!");
});
ImageView
<ImageView
android:id="@+id/imgLogo"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/logo"
android:contentDescription="Logo aplikacji"/>
ImageView imgLogo = findViewById(R.id.imgLogo);
// Zmiana obrazka
imgLogo.setImageResource(R.drawable.inny_obrazek);
Umieść pliki PNG/JPG w folderze res/drawable/. Odwołuj się przez @drawable/nazwa (bez rozszerzenia).
📝 Zadanie do teorii
setOnClickListener (sposób 2 lub 3) jest zalecany, bo:
- Logika jest w kodzie Java, łatwiejsza do debugowania
- Możesz użyć jednego listenera dla wielu przycisków i sprawdzić który kliknięto przez
v.getId() - android:onClick wymaga publicznej metody i jest trudniejsze do refaktoryzacji
⚡ onClick — praktyka
„Zobaczmy kompletny przykład — od layoutu po działającą aplikację."
Przykład: Powitanie
Aplikacja pobiera imię i wyświetla powitanie po kliknięciu przycisku.
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/edtImie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Wpisz imię"/>
<Button
android:id="@+id/btnPowitaj"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Powitaj"/>
<TextView
android:id="@+id/txtWynik"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold"/>
</LinearLayout>
MainActivity.java
package com.example.powitanie;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.*;
public class MainActivity extends AppCompatActivity {
private EditText edtImie;
private Button btnPowitaj;
private TextView txtWynik;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Pobieranie referencji do kontrolek
edtImie = findViewById(R.id.edtImie);
btnPowitaj = findViewById(R.id.btnPowitaj);
txtWynik = findViewById(R.id.txtWynik);
// Obsługa kliknięcia
btnPowitaj.setOnClickListener(v -> {
String imie = edtImie.getText().toString().trim();
if (imie.isEmpty()) {
txtWynik.setText("Wpisz imię!");
} else {
txtWynik.setText("Witaj, " + imie + "!");
}
});
}
}
💡 Porównanie z JavaFX: findViewById(R.id.xxx) to odpowiednik @FXML + fx:id
📝 Zadanie do teorii
findViewById() dla kontrolki?
Zmienna będzie null. Próba użycia metody na niej (np. edtImie.getText()) spowoduje NullPointerException i crash aplikacji.
📦 Zasoby (Resources)
„W Androidzie oddzielamy zasoby (teksty, kolory, wymiary) od kodu. To ułatwia tłumaczenie aplikacji i zmianę wyglądu."
Folder res/
| Folder | Zawartość |
|---|---|
res/layout/ | Pliki XML layoutów |
res/values/ | Teksty, kolory, style |
res/drawable/ | Obrazki |
res/mipmap/ | Ikona aplikacji |
strings.xml
<!-- res/values/strings.xml -->
<resources>
<string name="app_name">Moja Aplikacja</string>
<string name="btn_oblicz">Oblicz</string>
<string name="hint_liczba">Wpisz liczbę...</string>
<string name="error_empty">Pole nie może być puste!</string>
</resources>
<!-- Użycie w layout XML: -->
<Button android:text="@string/btn_oblicz"/>
<EditText android:hint="@string/hint_liczba"/>
// Użycie w kodzie Java:
String blad = getString(R.string.error_empty);
txtWynik.setText(blad);
colors.xml
<!-- res/values/colors.xml -->
<resources>
<color name="primary">#6200EE</color>
<color name="error">#B00020</color>
<color name="success">#00C853</color>
</resources>
<!-- Użycie: -->
<TextView android:textColor="@color/error"/>
dimens.xml
<!-- res/values/dimens.xml -->
<resources>
<dimen name="text_size_large">24sp</dimen>
<dimen name="padding_standard">16dp</dimen>
</resources>
- Łatwe tłumaczenie — dodaj folder
values-pl/ - Jedna zmiana = wszędzie zmienione
- Różne zasoby dla różnych ekranów
📝 Zadanie do teorii
primary z colors.xml w layoucie XML i w kodzie Java?
<!-- W XML: -->
android:textColor="@color/primary"
// W Java:
int color = getResources().getColor(R.color.primary);
txtWynik.setTextColor(color);
🔗 ConstraintLayout
„ConstraintLayout to zaawansowany layout — pozycjonujesz elementy względem siebie lub rodzica. Domyślny w Android Studio."
Podstawowe ograniczenia (constraints)
| Atrybut | Znaczenie |
|---|---|
app:layout_constraintTop_toTopOf="parent" | Górna krawędź do góry rodzica |
app:layout_constraintBottom_toBottomOf="parent" | Dolna krawędź do dołu rodzica |
app:layout_constraintStart_toStartOf="parent" | Lewa krawędź do lewej rodzica |
app:layout_constraintEnd_toEndOf="parent" | Prawa krawędź do prawej rodzica |
app:layout_constraintTop_toBottomOf="@id/inne" | Pod innym elementem |
Przykład: Formularz logowania
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<EditText
android:id="@+id/edtLogin"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Login"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="100dp"/>
<EditText
android:id="@+id/edtHaslo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Hasło"
android:inputType="textPassword"
app:layout_constraintTop_toBottomOf="@id/edtLogin"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
<Button
android:id="@+id/btnZaloguj"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Zaloguj"
app:layout_constraintTop_toBottomOf="@id/edtHaslo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
💡 android:layout_width="0dp" + constraints po obu stronach = element rozciąga się między ograniczeniami.
📝 Zadanie do teorii
<Button
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
Dodanie wszystkich 4 constraintów = wyśrodkowanie.
📋 ListView i RecyclerView
„ListView to lista przewijana — odpowiednik ListView w JavaFX. Na egzaminie wystarczy prosta lista."
ListView — podstawowa lista
<ListView
android:id="@+id/listZadania"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
ListView listZadania = findViewById(R.id.listZadania);
// Lista danych
ArrayList<String> zadania = new ArrayList<>();
zadania.add("Nauczyć się Androida");
zadania.add("Zdać egzamin");
// Adapter = most między danymi a widokiem
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_list_item_1,
zadania
);
listZadania.setAdapter(adapter);
// Obsługa kliknięcia elementu
listZadania.setOnItemClickListener((parent, view, position, id) -> {
String wybrany = zadania.get(position);
Toast.makeText(this, "Wybrano: " + wybrany, Toast.LENGTH_SHORT).show();
});
Dodawanie i usuwanie elementów
// Dodaj element
zadania.add("Nowe zadanie");
adapter.notifyDataSetChanged(); // WAŻNE!
// Usuń element
zadania.remove(position);
adapter.notifyDataSetChanged();
adapter.notifyDataSetChanged() żeby odświeżyć widok.
Toast — szybki komunikat
Toast.makeText(this, "Tekst komunikatu", Toast.LENGTH_SHORT).show();
// LENGTH_SHORT = krótki, LENGTH_LONG = dłuższy
📝 Zadanie do teorii
notifyDataSetChanged() po dodaniu elementu?
Dane zostaną dodane do ArrayList, ale ListView NIE odświeży się — użytkownik nie zobaczy nowego elementu do czasu, aż lista sama się odświeży (np. przy przewijaniu).
☕ Java vs Kotlin
„Na egzaminie możesz wybrać Java lub Kotlin. Java jest bardziej tradycyjna i masz ją na innych przedmiotach. Kotlin to nowoczesna alternatywa."
Porównanie składni
| Operacja | Java | Kotlin |
|---|---|---|
| Zmienna | String s = "tekst"; | val s = "tekst" |
| Zmienna (edytowalna) | int x = 5; | var x = 5 |
| Null safety | Może być null! | String? (explicit) |
| Lambda | v -> {...} | { v -> ... } |
Ten sam kod — Java vs Kotlin
Java:
public class MainActivity extends AppCompatActivity {
private EditText edtImie;
private Button btnPowitaj;
private TextView txtWynik;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edtImie = findViewById(R.id.edtImie);
btnPowitaj = findViewById(R.id.btnPowitaj);
txtWynik = findViewById(R.id.txtWynik);
btnPowitaj.setOnClickListener(v -> {
String imie = edtImie.getText().toString();
txtWynik.setText("Witaj " + imie + "!");
});
}
}
Kotlin:
class MainActivity : AppCompatActivity() {
private lateinit var edtImie: EditText
private lateinit var btnPowitaj: Button
private lateinit var txtWynik: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
edtImie = findViewById(R.id.edtImie)
btnPowitaj = findViewById(R.id.btnPowitaj)
txtWynik = findViewById(R.id.txtWynik)
btnPowitaj.setOnClickListener {
val imie = edtImie.text.toString()
txtWynik.text = "Witaj $imie!"
}
}
}
💡 Na egzaminie wybierz Java, jeśli nie znasz Kotlina. Różnice są kosmetyczne dla prostych aplikacji.
📝 Zadanie do teorii
- Null safety — kompilator wymusza obsługę nulli
- Krótszy kod — mniej boilerplate (val/var, brak średników, string templates)
🔥 Kalkulator — Rozgrzewka
„Prosty kalkulator na liczbach całkowitych. Poznamy LinearLayout, Button i onClick."
Mini Quiz
- Czym różni się
match_parentodwrap_content? - Co robi
layout_weight? - Jak połączyć Button z XML z kodem Java?
- Co to jest
setOnClickListener?
match_parent— rozciąga na całą szerokość rodzica;wrap_content— dopasowuje do treści- Rozdziela dostępne miejsce proporcjonalnie między elementy
Button btn = findViewById(R.id.mojPrzycisk);- Metoda podpinająca kod do zdarzenia kliknięcia przycisku
📋 Kalkulator — Instrukcja
„Prosty kalkulator na liczbach całkowitych: + - * / = i C."
Treść zadania
Wykonaj aplikację "Kalkulator" — tylko liczby całkowite:
- Wyświetlacz (TextView) — pokazuje aktualną liczbę
- Przyciski cyfr 0–9
- 4 operacje: + − * /
- Przycisk = (oblicz wynik)
- Przycisk C (wyczyść wszystko)
- Dzielenie przez 0 → komunikat "Błąd"
- Dzielenie całkowite (np. 7 / 2 = 3)
Układ przycisków
┌──────────────────────┐
│ 0 │ ← wyświetlacz
│ [ 7 ][ 8 ][ 9 ][ / ]│
│ [ 4 ][ 5 ][ 6 ][ * ]│
│ [ 1 ][ 2 ][ 3 ][ - ]│
│ [ C ][ 0 ][ = ][ + ]│
└──────────────────────┘
Kolory
| Element | Kolor tła | Kolor tekstu |
|---|---|---|
| Cyfry (0-9) | #333333 | #FFFFFF |
| Operacje (+ − * / =) | #FF9500 | #FFFFFF |
| C (czyszczenie) | #A5A5A5 | #000000 |
🏗️ Kalkulator — Budowa UI
activity_main.xml
Kluczowe zasady:
- Root:
LinearLayout (vertical)zpadding="16dp" - Wyświetlacz:
TextViewztextSize="48sp", wyrównanie do prawej - 4 rzędy po 4 przyciski (siatka 4×4)
- Każdy rząd:
LinearLayout (horizontal)zlayout_height="0dp"+layout_weight="1" - Każdy przycisk:
layout_width="0dp"+layout_weight="1"(równa szerokość)
Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout
Ustaw: orientation="vertical", padding="16dp"
Palette → Text → TextView:
id | wyswietlacz |
text | 0 |
textSize | 48sp |
textAlignment | textEnd |
layout_width | match_parent |
padding | 20dp |
layout_marginBottom | 16dp |
Dodaj LinearLayout (horizontal) z layout_height="0dp", layout_weight="1"
Wewnątrz 4 Button (każdy: layout_width="0dp", layout_weight="1", layout_margin="4dp", textSize="24sp"):
| id | text | backgroundTint |
|---|---|---|
btn7 | 7 | #333333 |
btn8 | 8 | #333333 |
btn9 | 9 | #333333 |
btnDzielenie | / | #FF9500 |
Skopiuj rząd 1 trzy razy (Copy → Paste with new IDs). Zmień:
Rząd 2: btn4(4), btn5(5), btn6(6), btnMnozenie(*)
Rząd 3: btn1(1), btn2(2), btn3(3), btnOdejmowanie(-)
Rząd 4: btnC(C, tło:#A5A5A5), btn0(0), btnRowna(=, tło:#FF9500), btnDodawanie(+, tło:#FF9500)
private TextView wyswietlacz;
private int liczbaA = 0;
private int liczbaB = 0;
private String operacja = "";
private boolean nowaLiczba = true;
// w onCreate:
wyswietlacz = findViewById(R.id.wyswietlacz);
Button btn0 = findViewById(R.id.btn0);
// ... btn1-btn9
Button btnDodawanie = findViewById(R.id.btnDodawanie);
Button btnOdejmowanie = findViewById(R.id.btnOdejmowanie);
Button btnMnozenie = findViewById(R.id.btnMnozenie);
Button btnDzielenie = findViewById(R.id.btnDzielenie);
Button btnRowna = findViewById(R.id.btnRowna);
Button btnC = findViewById(R.id.btnC);
// Jeden listener dla cyfr 0-9
View.OnClickListener kliknijCyfre = v -> {
String cyfra = ((Button) v).getText().toString();
String tekst = wyswietlacz.getText().toString();
if (nowaLiczba || tekst.equals("0")) {
wyswietlacz.setText(cyfra);
nowaLiczba = false;
} else {
wyswietlacz.setText(tekst + cyfra);
}
};
btn0.setOnClickListener(kliknijCyfre);
btn1.setOnClickListener(kliknijCyfre);
// ... btn2-btn9
// Operacje
View.OnClickListener kliknijOp = v -> {
liczbaA = Integer.parseInt(wyswietlacz.getText().toString());
operacja = ((Button) v).getText().toString();
nowaLiczba = true;
};
btnDodawanie.setOnClickListener(kliknijOp);
btnOdejmowanie.setOnClickListener(kliknijOp);
btnMnozenie.setOnClickListener(kliknijOp);
btnDzielenie.setOnClickListener(kliknijOp);
// Równa się
btnRowna.setOnClickListener(v -> {
liczbaB = Integer.parseInt(wyswietlacz.getText().toString());
int wynik = 0;
switch (operacja) {
case "+": wynik = liczbaA + liczbaB; break;
case "-": wynik = liczbaA - liczbaB; break;
case "*": wynik = liczbaA * liczbaB; break;
case "/":
if (liczbaB == 0) { wyswietlacz.setText("Błąd"); return; }
wynik = liczbaA / liczbaB; break;
}
wyswietlacz.setText(String.valueOf(wynik));
nowaLiczba = true;
});
// Czyszczenie
btnC.setOnClickListener(v -> {
wyswietlacz.setText("0");
liczbaA = 0; operacja = ""; nowaLiczba = true;
});
Kliknij ▶ Run. Przetestuj: 5 + 3 = 8, 10 / 3 = 3 (całkowite!), 5 / 0 = Błąd, C = reset.
💻 Kalkulator — Gotowy kod
„Uproszczony kalkulator — tylko int, tylko + - * / = C."
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- Wyświetlacz -->
<TextView
android:id="@+id/wyswietlacz"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0"
android:textSize="48sp"
android:textAlignment="textEnd"
android:padding="20dp"
android:layout_marginBottom="16dp"/>
<!-- Rząd 1: 7 8 9 / -->
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp"
android:layout_weight="1" android:gravity="center">
<Button android:id="@+id/btn7" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="7" android:textSize="24sp"/>
<Button android:id="@+id/btn8" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="8" android:textSize="24sp"/>
<Button android:id="@+id/btn9" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="9" android:textSize="24sp"/>
<Button android:id="@+id/btnDzielenie" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="/" android:textSize="24sp"
android:backgroundTint="#FF9500" android:textColor="#FFFFFF"/>
</LinearLayout>
<!-- Rząd 2: 4 5 6 * -->
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp"
android:layout_weight="1" android:gravity="center">
<Button android:id="@+id/btn4" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="4" android:textSize="24sp"/>
<Button android:id="@+id/btn5" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="5" android:textSize="24sp"/>
<Button android:id="@+id/btn6" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="6" android:textSize="24sp"/>
<Button android:id="@+id/btnMnozenie" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="*" android:textSize="24sp"
android:backgroundTint="#FF9500" android:textColor="#FFFFFF"/>
</LinearLayout>
<!-- Rząd 3: 1 2 3 - -->
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp"
android:layout_weight="1" android:gravity="center">
<Button android:id="@+id/btn1" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="1" android:textSize="24sp"/>
<Button android:id="@+id/btn2" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="2" android:textSize="24sp"/>
<Button android:id="@+id/btn3" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="3" android:textSize="24sp"/>
<Button android:id="@+id/btnOdejmowanie" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="-" android:textSize="24sp"
android:backgroundTint="#FF9500" android:textColor="#FFFFFF"/>
</LinearLayout>
<!-- Rząd 4: C 0 = + -->
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp"
android:layout_weight="1" android:gravity="center">
<Button android:id="@+id/btnC" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="C" android:textSize="24sp"
android:backgroundTint="#A5A5A5"/>
<Button android:id="@+id/btn0" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="0" android:textSize="24sp"/>
<Button android:id="@+id/btnRowna" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="=" android:textSize="24sp"
android:backgroundTint="#FF9500" android:textColor="#FFFFFF"/>
<Button android:id="@+id/btnDodawanie" android:layout_width="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_margin="4dp" android:text="+" android:textSize="24sp"
android:backgroundTint="#FF9500" android:textColor="#FFFFFF"/>
</LinearLayout>
</LinearLayout>
MainActivity.java
package com.example.kalkulator;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.view.View;
public class MainActivity extends AppCompatActivity {
private TextView wyswietlacz;
private int liczbaA = 0;
private String operacja = "";
private boolean nowaLiczba = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
wyswietlacz = findViewById(R.id.wyswietlacz);
Button btn0 = findViewById(R.id.btn0);
Button btn1 = findViewById(R.id.btn1);
Button btn2 = findViewById(R.id.btn2);
Button btn3 = findViewById(R.id.btn3);
Button btn4 = findViewById(R.id.btn4);
Button btn5 = findViewById(R.id.btn5);
Button btn6 = findViewById(R.id.btn6);
Button btn7 = findViewById(R.id.btn7);
Button btn8 = findViewById(R.id.btn8);
Button btn9 = findViewById(R.id.btn9);
Button btnDodawanie = findViewById(R.id.btnDodawanie);
Button btnOdejmowanie = findViewById(R.id.btnOdejmowanie);
Button btnMnozenie = findViewById(R.id.btnMnozenie);
Button btnDzielenie = findViewById(R.id.btnDzielenie);
Button btnRowna = findViewById(R.id.btnRowna);
Button btnC = findViewById(R.id.btnC);
// Cyfry 0-9 — jeden listener
View.OnClickListener kliknijCyfre = v -> {
String cyfra = ((Button) v).getText().toString();
String tekst = wyswietlacz.getText().toString();
if (nowaLiczba || tekst.equals("0")) {
wyswietlacz.setText(cyfra);
nowaLiczba = false;
} else {
wyswietlacz.setText(tekst + cyfra);
}
};
btn0.setOnClickListener(kliknijCyfre);
btn1.setOnClickListener(kliknijCyfre);
btn2.setOnClickListener(kliknijCyfre);
btn3.setOnClickListener(kliknijCyfre);
btn4.setOnClickListener(kliknijCyfre);
btn5.setOnClickListener(kliknijCyfre);
btn6.setOnClickListener(kliknijCyfre);
btn7.setOnClickListener(kliknijCyfre);
btn8.setOnClickListener(kliknijCyfre);
btn9.setOnClickListener(kliknijCyfre);
// Operacje + - * /
View.OnClickListener kliknijOp = v -> {
liczbaA = Integer.parseInt(wyswietlacz.getText().toString());
operacja = ((Button) v).getText().toString();
nowaLiczba = true;
};
btnDodawanie.setOnClickListener(kliknijOp);
btnOdejmowanie.setOnClickListener(kliknijOp);
btnMnozenie.setOnClickListener(kliknijOp);
btnDzielenie.setOnClickListener(kliknijOp);
// = (równa się)
btnRowna.setOnClickListener(v -> {
int liczbaB = Integer.parseInt(wyswietlacz.getText().toString());
int wynik = 0;
switch (operacja) {
case "+": wynik = liczbaA + liczbaB; break;
case "-": wynik = liczbaA - liczbaB; break;
case "*": wynik = liczbaA * liczbaB; break;
case "/":
if (liczbaB == 0) {
wyswietlacz.setText("Błąd");
nowaLiczba = true;
return;
}
wynik = liczbaA / liczbaB;
break;
}
wyswietlacz.setText(String.valueOf(wynik));
nowaLiczba = true;
});
// C — wyczyść
btnC.setOnClickListener(v -> {
wyswietlacz.setText("0");
liczbaA = 0;
operacja = "";
nowaLiczba = true;
});
}
}
Kalkulator — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
7 / 2 w Javie (typ int)?int) wymaga obsługi wyjątku?"42" na liczbę całkowitą w Javie?Testy manualne
| Test | Kroki | Oczekiwany wynik |
|---|---|---|
| Dodawanie | 5 + 3 = | 8 |
| Odejmowanie | 10 - 4 = | 6 |
| Mnożenie | 6 * 7 = | 42 |
| Dzielenie | 15 / 3 = | 5 |
| Dzielenie całkowite | 7 / 2 = | 3 |
| Dzielenie przez 0 | 5 / 0 = | Błąd |
| Czyszczenie | C | 0 |
| Łańcuch | 2 + 3 + 4 = | 9 |
Kalkulator BMI — Rozgrzewka
„W tym projekcie poznamy dwa ważne komponenty Androida: SeekBar (suwak) i Spinner (lista rozwijana). Zamiast wpisywać liczby, użytkownik będzie je wybierał — to wygodniejsze i eliminuje błędy!"
Nowe komponenty
SeekBar — suwak do wybierania wartości liczbowej. Użytkownik przesuwa palcem/myszką.
Spinner — lista rozwijana (dropdown). Użytkownik wybiera jedną opcję z listy.
Mini Quiz
- Czym jest SeekBar i do czego służy?
- Czym jest Spinner i jak działa?
- Jak ustawić tekst w TextView?
- Jaki wzór opisuje BMI?
- SeekBar — suwak z wartością min-max. Nasłuchujemy zmian przez
OnSeekBarChangeListener. - Spinner — rozwijana lista opcji. Wypełniamy przez
ArrayAdapter, nasłuchujemy przezOnItemSelectedListener. textView.setText("tekst")- BMI = waga(kg) / wzrost(m)² — np. 70 / (1.75 × 1.75) = 22.86
Kalkulator BMI — Instrukcja
„Budujemy kalkulator BMI z SeekBar do wagi i Spinner do wzrostu. Dzięki temu uczeń pozna nowe komponenty UI!"
Treść zadania
Wykonaj aplikację "Kalkulator BMI" z komponentami SeekBar i Spinner:
- SeekBar do wyboru wagi (30–200 kg) — z etykietą pokazującą aktualną wartość
- Spinner do wyboru wzrostu (140–220 cm, co 1 cm)
- Przycisk "Oblicz BMI"
- Wyświetlenie wyniku BMI (liczba całkowita)
- Interpretacja wyniku (niedowaga/norma/nadwaga/otyłość) z kolorem
Nowe komponenty do nauki
| Komponent | Opis | Zastosowanie w projekcie |
|---|---|---|
| SeekBar | Suwak z wartością liczbową | Wybór wagi (30–200 kg) |
| Spinner | Lista rozwijana (dropdown) | Wybór wzrostu (140–220 cm) |
Skala BMI
| BMI | Kategoria |
|---|---|
| < 18.5 | Niedowaga |
| 18.5 – 24.9 | Norma |
| 25 – 29.9 | Nadwaga |
| ≥ 30 | Otyłość |
Kalkulator BMI — Budowa UI
File → New → New Project → Empty Views Activity
Nazwa: BMI, język: Java, kliknij Finish.
Otwórz activity_main.xml (zakładka Design).
Component Tree → kliknij prawym na ConstraintLayout → Convert view... → wpisz LinearLayout → OK
W panelu Attributes (prawa strona) ustaw:
orientation | vertical |
padding | 24dp |
orientation jest w sekcji Common Attributes w panelu Attributes po prawej. Padding znajdziesz w sekcji Layout → Padding → kliknij all i wpisz 24dp.
Palette (lewy panel) → Text → przeciągnij TextView na ekran.
Kliknij na dodany TextView. W panelu Attributes po prawej ustaw:
| Atrybut | Wartość | Gdzie znaleźć |
|---|---|---|
text | Kalkulator BMI | Common Attributes → text |
textSize | 24sp | Common Attributes → textSize |
textStyle | bold | Common Attributes → textStyle → zaznacz B |
layout_gravity | center | Layout → layout_gravity |
layout_marginBottom | 24dp | Layout → kliknij dolny margines |
Najpierw dodaj etykietę. Palette → Text → TextView, przeciągnij pod tytuł:
| Atrybut | Wartość |
|---|---|
id | txtWagaLabel |
text | Waga: 70 kg |
textSize | 18sp |
layout_marginTop | 16dp |
Teraz dodaj suwak. Palette → Widgets → SeekBar, przeciągnij pod etykietę:
| Atrybut | Wartość | Opis |
|---|---|---|
id | seekWaga | Identyfikator suwaka |
layout_width | match_parent | Pełna szerokość |
max | 170 | Zakres: 0–170 (+ 30 = 30–200 kg) |
progress | 40 | Domyślnie 70 kg (40 + 30) |
waga = progress + 30.
Dodaj etykietę. Palette → Text → TextView:
text | Wzrost: |
textSize | 18sp |
layout_marginTop | 24dp |
Teraz dodaj Spinner. Palette → Containers → Spinner, przeciągnij pod etykietę:
| Atrybut | Wartość | Opis |
|---|---|---|
id | spinnerWzrost | Identyfikator spinnera |
layout_width | match_parent | Pełna szerokość |
layout_marginTop | 8dp | Odstęp od etykiety |
Palette → Text → TextView — dodaj dwa TextViews pod Spinnerem:
TextView 1 — wynik liczbowy:
id | txtWynik |
textSize | 48sp |
textStyle | bold |
layout_gravity | center |
layout_marginTop | 32dp |
TextView 2 — kategoria:
id | txtKategoria |
textSize | 22sp |
layout_gravity | center |
layout_marginTop | 8dp |
Twoje Component Tree powinno wyglądać tak:
LinearLayout (vertical)
├── TextView "Kalkulator BMI"
├── TextView "Waga: 70 kg" (id: txtWagaLabel)
├── SeekBar (id: seekWaga)
├── TextView "Wzrost:"
├── Spinner (id: spinnerWzrost)
├── TextView (id: txtWynik)
└── TextView (id: txtKategoria)
Jeśli kolejność się nie zgadza — przeciągnij elementy w Component Tree we właściwe miejsce.
Otwórz MainActivity.java. Dodaj pola klasy nad metodą onCreate:
private SeekBar seekWaga;
private Spinner spinnerWzrost;
private TextView txtWagaLabel, txtWynik, txtKategoria;
private int waga = 70; // domyślna waga
private int wzrost = 170; // domyślny wzrost
Wewnątrz onCreate, pod setContentView, dodaj:
seekWaga = findViewById(R.id.seekWaga);
spinnerWzrost = findViewById(R.id.spinnerWzrost);
txtWagaLabel = findViewById(R.id.txtWagaLabel);
txtWynik = findViewById(R.id.txtWynik);
txtKategoria = findViewById(R.id.txtKategoria);
Dodaj w onCreate nasłuchiwanie zmian suwaka:
seekWaga.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
waga = progress + 30; // SeekBar: 0-170, waga: 30-200
txtWagaLabel.setText("Waga: " + waga + " kg");
obliczBMI(); // automatyczne przeliczenie!
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
onProgressChanged jest wywoływane automatycznie. Wartość progress (0–170) zamieniamy na wagę (30–200) dodając 30. Na końcu wywołujemy obliczBMI() — wynik aktualizuje się natychmiast!
Najpierw wypełnij Spinner wartościami. Dodaj w onCreate:
// Tworzymy listę wzrostów: 140, 141, 142, ..., 220
ArrayList<String> wzrosty = new ArrayList<>();
for (int i = 140; i <= 220; i++) {
wzrosty.add(i + " cm");
}
// Tworzymy adapter (łączy dane z widokiem)
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_spinner_item,
wzrosty
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerWzrost.setAdapter(adapter);
// Domyślnie: 170 cm (index 30, bo 170 - 140 = 30)
spinnerWzrost.setSelection(30);
Teraz dodaj nasłuchiwanie wyboru:
spinnerWzrost.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, android.view.View view, int position, long id) {
wzrost = position + 140; // index 0 = 140 cm
obliczBMI(); // automatyczne przeliczenie!
}
@Override
public void onNothingSelected(AdapterView<?> parent) { }
});
ArrayAdapter łączy listę Stringów ze Spinnerem. Gdy użytkownik wybierze opcję, onItemSelected daje nam position (index w liście). Index 0 = "140 cm", więc: wzrost = position + 140. Automatycznie przeliczamy BMI!
Dodaj metodę obliczBMI() pod metodą onCreate. Jest wywoływana automatycznie z SeekBar i Spinner:
private void obliczBMI() {
double wzrostM = wzrost / 100.0; // cm → m
double bmi = waga / (wzrostM * wzrostM);
int wynik = (int) bmi; // liczba całkowita
txtWynik.setText(String.valueOf(wynik));
// Kategoria + kolor
String kategoria;
int kolor;
if (bmi < 18.5) {
kategoria = "Niedowaga";
kolor = Color.BLUE;
} else if (bmi < 25) {
kategoria = "Waga prawidłowa";
kolor = Color.parseColor("#4CAF50");
} else if (bmi < 30) {
kategoria = "Nadwaga";
kolor = Color.parseColor("#FFA500");
} else {
kategoria = "Otyłość";
kolor = Color.RED;
}
txtKategoria.setText(kategoria);
txtKategoria.setTextColor(kolor);
}
Pod txtKategoria dodaj wizualną skalę BMI. W activity_main.xml (zakładka Code/Split):
Etykieta: Palette → Text → TextView:
text | Skala BMI: |
textSize | 14sp |
layout_marginTop | 24dp |
SeekBar wynikowy: Palette → Widgets → SeekBar:
id | seekBmiSkala |
layout_width | match_parent |
max | 40 |
progress | 0 |
enabled | false |
Pod tym SeekBar dodaj LinearLayout (horizontal) z 5 etykietami: 10, 18.5, 25, 30, 50. Każda etykieta: layout_weight="1", textSize="11sp".
To daje wizualną skalę z oznaczeniami progów BMI.
Dodaj pole klasy:
private SeekBar seekWaga, seekBmiSkala;
W onCreate dodaj:
seekBmiSkala = findViewById(R.id.seekBmiSkala);
W metodzie obliczBMI(), po txtWynik.setText(...) dodaj:
// Skala BMI: SeekBar 0-40 odpowiada BMI 10-50
int skalaPozycja = Math.max(0, Math.min(40, wynik - 10));
seekBmiSkala.setProgress(skalaPozycja);
Math.max/Math.min pilnują, żeby wartość nie wyszła poza zakres 0–40.
Na górze pliku MainActivity.java powinny być importy:
import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.*;
import java.util.ArrayList;
Uruchom aplikację: zielony trójkąt ▶ na pasku lub Shift + F10.
Przetestuj: przesuń suwak wagi, wybierz wzrost ze Spinnera, kliknij "Oblicz BMI". Suwak skali BMI powinien przesunąć się na odpowiednią pozycję!
Kalkulator BMI — Gotowy kod
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Kalkulator BMI"
android:textSize="24sp"
android:textStyle="bold"
android:layout_gravity="center"
android:layout_marginBottom="24dp" />
<TextView
android:id="@+id/txtWagaLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Waga: 70 kg"
android:textSize="18sp"
android:layout_marginTop="16dp" />
<SeekBar
android:id="@+id/seekWaga"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="170"
android:progress="40" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Wzrost:"
android:textSize="18sp"
android:layout_marginTop="24dp" />
<Spinner
android:id="@+id/spinnerWzrost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<TextView
android:id="@+id/txtWynik"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="48sp"
android:textStyle="bold"
android:layout_gravity="center"
android:layout_marginTop="32dp" />
<TextView
android:id="@+id/txtKategoria"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
android:layout_gravity="center"
android:layout_marginTop="8dp" />
<!-- Skala BMI -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skala BMI:"
android:textSize="14sp"
android:layout_marginTop="24dp" />
<SeekBar
android:id="@+id/seekBmiSkala"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="40"
android:progress="0"
android:enabled="false"
android:layout_marginTop="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="10"
android:textSize="11sp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="18.5"
android:textSize="11sp"
android:gravity="center" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="25"
android:textSize="11sp"
android:gravity="center" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="30"
android:textSize="11sp"
android:gravity="center" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="50"
android:textSize="11sp"
android:gravity="end" />
</LinearLayout>
</LinearLayout>
MainActivity.java
package com.example.bmi;
import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.*;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private SeekBar seekWaga, seekBmiSkala;
private Spinner spinnerWzrost;
private TextView txtWagaLabel, txtWynik, txtKategoria;
private int waga = 70;
private int wzrost = 170;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
seekWaga = findViewById(R.id.seekWaga);
seekBmiSkala = findViewById(R.id.seekBmiSkala);
spinnerWzrost = findViewById(R.id.spinnerWzrost);
txtWagaLabel = findViewById(R.id.txtWagaLabel);
txtWynik = findViewById(R.id.txtWynik);
txtKategoria = findViewById(R.id.txtKategoria);
// SeekBar — suwak wagi (automatycznie przelicza BMI)
seekWaga.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
waga = progress + 30;
txtWagaLabel.setText("Waga: " + waga + " kg");
obliczBMI();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
// Spinner — lista wzrostów
ArrayList<String> wzrosty = new ArrayList<>();
for (int i = 140; i <= 220; i++) {
wzrosty.add(i + " cm");
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_item, wzrosty
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerWzrost.setAdapter(adapter);
spinnerWzrost.setSelection(30); // 170 cm
spinnerWzrost.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, android.view.View view,
int position, long id) {
wzrost = position + 140;
obliczBMI();
}
@Override
public void onNothingSelected(AdapterView<?> parent) { }
});
// Oblicz BMI na starcie z domyślnymi wartościami
obliczBMI();
}
private void obliczBMI() {
double wzrostM = wzrost / 100.0;
double bmi = waga / (wzrostM * wzrostM);
int wynik = (int) bmi;
txtWynik.setText(String.valueOf(wynik));
// Skala BMI: SeekBar 0-40 odpowiada BMI 10-50
int skalaPozycja = Math.max(0, Math.min(40, wynik - 10));
seekBmiSkala.setProgress(skalaPozycja);
String kategoria;
int kolor;
if (bmi < 18.5) {
kategoria = "Niedowaga";
kolor = Color.BLUE;
} else if (bmi < 25) {
kategoria = "Waga prawidłowa";
kolor = Color.parseColor("#4CAF50");
} else if (bmi < 30) {
kategoria = "Nadwaga";
kolor = Color.parseColor("#FFA500");
} else {
kategoria = "Otyłość";
kolor = Color.RED;
}
txtKategoria.setText(kategoria);
txtKategoria.setTextColor(kolor);
}
}
Kalkulator BMI — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
max="170". Chcemy zakres wagi 30–200 kg. Jak obliczyć wagę z wartości progress?try-catch do parsowania liczb?Testy manualne
| Waga (SeekBar) | Wzrost (Spinner) | BMI | Kategoria | Skala |
|---|---|---|---|---|
| 70 kg | 175 cm | 22 | Waga prawidłowa | Suwak w strefie zielonej |
| 50 kg | 180 cm | 15 | Niedowaga | Suwak na lewo |
| 90 kg | 170 cm | 31 | Otyłość | Suwak na prawo |
| 85 kg | 175 cm | 27 | Nadwaga | Suwak w strefie pomarańczowej |
🔥 Lista zakupów — Rozgrzewka
„Lista z możliwością dodawania i usuwania. ListView + adapter."
Mini Quiz
- Jak utworzyć ArrayAdapter?
- Jak dodać element do listy?
- Co musi być wywołane po zmianie danych?
new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, lista)lista.add("element")adapter.notifyDataSetChanged()
📋 Lista zakupów — Instrukcja
Treść zadania
Wykonaj aplikację "Lista zakupów":
- Pole tekstowe na nowy produkt
- Przycisk "Dodaj"
- ListView z produktami
- Kliknięcie elementu = usunięcie (z potwierdzeniem)
- Przycisk "Wyczyść listę"
- Walidacja: nie można dodać pustego produktu
🏗️ Lista zakupów — Budowa UI
Nowy projekt: Empty Views Activity, nazwa Zakupy, język Java.
Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout
Ustaw: orientation="vertical", padding="16dp"
Dodaj LinearLayout (horizontal) — layout_width="match_parent", layout_height="wrap_content".
Wewnątrz:
| Widżet | id | Atrybuty |
|---|---|---|
| EditText | edtProdukt | layout_width="0dp", layout_weight="1", hint="Nazwa produktu" |
| Button | btnDodaj | layout_width="wrap_content", text="Dodaj" |
ListView: id="listProdukty", layout_width="match_parent", layout_height="0dp", layout_weight="1", layout_marginTop="16dp"
Button: id="btnWyczysc", layout_width="match_parent", text="Wyczyść listę"
private EditText edtProdukt;
private Button btnDodaj, btnWyczysc;
private ListView listProdukty;
private ArrayList<String> produkty;
private ArrayAdapter<String> adapter;
// w onCreate:
edtProdukt = findViewById(R.id.edtProdukt);
btnDodaj = findViewById(R.id.btnDodaj);
btnWyczysc = findViewById(R.id.btnWyczysc);
listProdukty = findViewById(R.id.listProdukty);
// w onCreate (po findViewById):
produkty = new ArrayList<>();
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, produkty);
listProdukty.setAdapter(adapter);
btnDodaj.setOnClickListener(v -> dodajProdukt());
btnWyczysc.setOnClickListener(v -> wyczyscListe());
private void dodajProdukt() {
String produkt = edtProdukt.getText().toString().trim();
if (produkt.isEmpty()) {
Toast.makeText(this, "Wpisz nazwę produktu!", Toast.LENGTH_SHORT).show();
return;
}
produkty.add(produkt);
adapter.notifyDataSetChanged();
edtProdukt.setText("");
}
private void wyczyscListe() {
produkty.clear();
adapter.notifyDataSetChanged();
}
Dodaj w onCreate listener na kliknięcie elementu listy:
// Kliknięcie = usuń z potwierdzeniem
listProdukty.setOnItemClickListener((parent, view, position, id) -> {
String produkt = produkty.get(position);
potwierdzUsuniecie(produkt, position);
});
// Nowa metoda:
private void potwierdzUsuniecie(String produkt, int position) {
new AlertDialog.Builder(this)
.setTitle("Usunąć produkt?")
.setMessage("Czy na pewno usunąć: " + produkt + "?")
.setPositiveButton("Tak", (dialog, which) -> {
produkty.remove(position);
adapter.notifyDataSetChanged();
})
.setNegativeButton("Nie", null)
.show();
}
💻 Lista zakupów — Kod
package com.example.zakupy;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.*;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private EditText edtProdukt;
private Button btnDodaj, btnWyczysc;
private ListView listProdukty;
private ArrayList<String> produkty;
private ArrayAdapter<String> adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edtProdukt = findViewById(R.id.edtProdukt);
btnDodaj = findViewById(R.id.btnDodaj);
btnWyczysc = findViewById(R.id.btnWyczysc);
listProdukty = findViewById(R.id.listProdukty);
produkty = new ArrayList<>();
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, produkty);
listProdukty.setAdapter(adapter);
btnDodaj.setOnClickListener(v -> dodajProdukt());
btnWyczysc.setOnClickListener(v -> wyczyscListe());
// Kliknięcie = usuń z potwierdzeniem
listProdukty.setOnItemClickListener((parent, view, position, id) -> {
String produkt = produkty.get(position);
potwierdzUsuniecie(produkt, position);
});
}
private void dodajProdukt() {
String produkt = edtProdukt.getText().toString().trim();
if (produkt.isEmpty()) {
Toast.makeText(this, "Wpisz nazwę produktu!", Toast.LENGTH_SHORT).show();
return;
}
produkty.add(produkt);
adapter.notifyDataSetChanged();
edtProdukt.setText("");
}
private void potwierdzUsuniecie(String produkt, int position) {
new AlertDialog.Builder(this)
.setTitle("Usunąć produkt?")
.setMessage("Czy na pewno usunąć: " + produkt + "?")
.setPositiveButton("Tak", (dialog, which) -> {
produkty.remove(position);
adapter.notifyDataSetChanged();
})
.setNegativeButton("Nie", null)
.show();
}
private void wyczyscListe() {
produkty.clear();
adapter.notifyDataSetChanged();
}
}
Lista zakupów — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
position?Testy manualne
| Test | Oczekiwany wynik |
|---|---|
| Dodaj "Mleko" | Mleko pojawia się na liście |
| Dodaj pusty produkt | Toast z błędem |
| Kliknij produkt na liście | Dialog z pytaniem o usunięcie |
| Potwierdź usunięcie | Produkt znika z listy |
| Kliknij "Wyczyść" | Lista pusta |
🔥 Notatnik — Rozgrzewka
„Notatnik z możliwością zapisu do pliku. Dodatkowa funkcjonalność!"
Mini Quiz
- Jak uzyskać dostęp do wieloliniowego pola tekstowego?
- Jak zapisać tekst do pliku w Android?
- EditText z
android:inputType="textMultiLine"iandroid:lines="10" FileOutputStreamlubSharedPreferences(prostsze dla tekstu)
📋 Notatnik — Instrukcja
Treść zadania
Wykonaj aplikację "Notatnik":
- Pole tekstowe wieloliniowe na notatkę
- Przycisk "Zapisz" — zapisuje notatkę
- Przycisk "Wczytaj" — wczytuje zapisaną notatkę
- Przycisk "Wyczyść" — czyści pole
- Notatka zapisywana w SharedPreferences
- Komunikat Toast przy zapisie/wczytaniu
🏗️ Notatnik — Budowa UI
Nowy projekt: Empty Views Activity, nazwa Notatnik, język Java.
Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout
Ustaw: orientation="vertical", padding="16dp"
Palette → Text → TextView:
text | Notatnik |
textSize | 24sp |
textStyle | bold |
layout_marginBottom | 16dp |
Palette → Text → Multiline Text:
id | edtNotatka |
layout_width | match_parent |
layout_height | 0dp |
layout_weight | 1 |
gravity | top |
hint | Wpisz notatke... |
inputType | textMultiLine |
background | @android:drawable/edit_text |
Dodaj LinearLayout (horizontal) z layout_marginTop="16dp".
Wewnątrz 3 Button (każdy: layout_width="0dp", layout_weight="1"):
| id | text |
|---|---|
btnZapisz | Zapisz |
btnWczytaj | Wczytaj |
btnWyczysc | Wyczysc |
private EditText edtNotatka;
private Button btnZapisz, btnWczytaj, btnWyczysc;
private static final String PREFS_NAME = "NotatnikPrefs";
private static final String KEY_NOTATKA = "notatka";
// w onCreate:
edtNotatka = findViewById(R.id.edtNotatka);
btnZapisz = findViewById(R.id.btnZapisz);
btnWczytaj = findViewById(R.id.btnWczytaj);
btnWyczysc = findViewById(R.id.btnWyczysc);
btnZapisz.setOnClickListener(v -> zapiszNotatke());
btnWczytaj.setOnClickListener(v -> wczytajNotatke());
btnWyczysc.setOnClickListener(v -> wyczyscNotatke());
// Automatyczne wczytanie przy starcie
wczytajNotatke();
private void zapiszNotatke() {
String notatka = edtNotatka.getText().toString();
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(KEY_NOTATKA, notatka);
editor.apply(); // lub commit() (synchroniczny)
Toast.makeText(this, "Zapisano!", Toast.LENGTH_SHORT).show();
}
private void wczytajNotatke() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
String notatka = prefs.getString(KEY_NOTATKA, ""); // "" = wartosc domyslna
edtNotatka.setText(notatka);
if (!notatka.isEmpty()) {
Toast.makeText(this, "Wczytano notatke", Toast.LENGTH_SHORT).show();
}
}
private void wyczyscNotatke() {
edtNotatka.setText("");
}
💻 Notatnik — Kod
package com.example.notatnik;
import androidx.appcompat.app.AppCompatActivity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.*;
public class MainActivity extends AppCompatActivity {
private EditText edtNotatka;
private Button btnZapisz, btnWczytaj, btnWyczysc;
private static final String PREFS_NAME = "NotatnikPrefs";
private static final String KEY_NOTATKA = "notatka";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edtNotatka = findViewById(R.id.edtNotatka);
btnZapisz = findViewById(R.id.btnZapisz);
btnWczytaj = findViewById(R.id.btnWczytaj);
btnWyczysc = findViewById(R.id.btnWyczysc);
btnZapisz.setOnClickListener(v -> zapiszNotatke());
btnWczytaj.setOnClickListener(v -> wczytajNotatke());
btnWyczysc.setOnClickListener(v -> wyczyscNotatke());
// Automatyczne wczytanie przy starcie
wczytajNotatke();
}
private void zapiszNotatke() {
String notatka = edtNotatka.getText().toString();
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(KEY_NOTATKA, notatka);
editor.apply(); // lub commit() (synchroniczny)
Toast.makeText(this, "Zapisano!", Toast.LENGTH_SHORT).show();
}
private void wczytajNotatke() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
String notatka = prefs.getString(KEY_NOTATKA, ""); // "" = wartość domyślna
edtNotatka.setText(notatka);
if (!notatka.isEmpty()) {
Toast.makeText(this, "Wczytano notatkę", Toast.LENGTH_SHORT).show();
}
}
private void wyczyscNotatke() {
edtNotatka.setText("");
}
}
Prosty sposób na przechowywanie par klucz-wartość. Idealny dla ustawień i małych danych tekstowych.
Notatnik — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
MODE_PRIVATE w SharedPreferences?Testy manualne
| Test | Oczekiwany wynik |
|---|---|
| Wpisz tekst + Zapisz | Toast "Zapisano!" |
| Wyczyść + Wczytaj | Poprzedni tekst wraca |
| Zamknij i otwórz aplikację | Notatka nadal jest |
| Wyczyść | Pole puste |
🔥 Quiz — Rozgrzewka
„Quiz to świetny projekt łączący logikę programowania z UI."
Mini Quiz
- Jak przechowywać pytania quizu?
- Jak grupować RadioButtony?
- Jak sprawdzić który RadioButton jest zaznaczony?
- Tablica obiektów lub ArrayList klasy Pytanie
<RadioGroup>w XMLradioGroup.getCheckedRadioButtonId()zwraca ID zaznaczonego
📋 Quiz — Instrukcja
Treść zadania
Wykonaj aplikację "Quiz":
- Baza 12 pytań jednokrotnego wyboru
- Losowanie 3 pytań z bazy przy każdym uruchomieniu
- 4 odpowiedzi do każdego pytania (RadioButton)
- Przycisk "Następne" — przejście do kolejnego pytania
- Na końcu — wyświetlenie wyniku (X/3)
- Możliwość rozpoczęcia od nowa (nowe losowanie!)
- Numeracja pytań (1/3, 2/3...)
🏗️ Quiz — Budowa UI
Nowy projekt: Empty Views Activity, nazwa Quiz, język Java.
Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout
Ustaw: orientation="vertical", padding="16dp"
TextView (numer):
id | txtNumerPytania |
text | Pytanie 1/3 |
textSize | 16sp |
TextView (tresc pytania):
id | txtPytanie |
text | Tresc pytania |
textSize | 20sp |
textStyle | bold |
layout_marginTop | 16dp |
layout_marginBottom | 24dp |
Palette → Buttons → RadioGroup (id="radioGroup", layout_width="match_parent")
Wewnątrz 4 RadioButton (każdy: layout_width="match_parent"):
| id | text |
|---|---|
rbOdp1 | Odpowiedz 1 |
rbOdp2 | Odpowiedz 2 |
rbOdp3 | Odpowiedz 3 |
rbOdp4 | Odpowiedz 4 |
Button: id="btnNastepne", layout_width="match_parent", text="Nastepne", layout_marginTop="32dp"
TextView (wynik): id="txtWynik", textSize="24sp", textStyle="bold", gravity="center", layout_marginTop="32dp", visibility="gone"
Prawym na pakiet → New → Java Class → Pytanie:
public class Pytanie {
String tresc;
String[] odpowiedzi;
int poprawna; // indeks 0-3
public Pytanie(String tresc, String[] odpowiedzi, int poprawna) {
this.tresc = tresc;
this.odpowiedzi = odpowiedzi;
this.poprawna = poprawna;
}
}
private TextView txtNumerPytania, txtPytanie, txtWynik;
private RadioGroup radioGroup;
private RadioButton rbOdp1, rbOdp2, rbOdp3, rbOdp4;
private Button btnNastepne;
private Pytanie[] pytania; // wylosowane 3 pytania
private int aktualnePytanie = 0;
private int wynik = 0;
// w onCreate:
txtNumerPytania = findViewById(R.id.txtNumerPytania);
txtPytanie = findViewById(R.id.txtPytanie);
txtWynik = findViewById(R.id.txtWynik);
radioGroup = findViewById(R.id.radioGroup);
rbOdp1 = findViewById(R.id.rbOdp1);
rbOdp2 = findViewById(R.id.rbOdp2);
rbOdp3 = findViewById(R.id.rbOdp3);
rbOdp4 = findViewById(R.id.rbOdp4);
btnNastepne = findViewById(R.id.btnNastepne);
losujPytania();
wyswietlPytanie();
btnNastepne.setOnClickListener(v -> sprawdzOdpowiedz());
import java.util.Random;
Metoda losujPytania() — baza 12 pytań, losujemy 3:
private void losujPytania() {
Pytanie[] baza = {
new Pytanie("Stolica Polski to:",
new String[]{"Kraków", "Warszawa", "Gdańsk", "Poznań"}, 1),
new Pytanie("2 + 2 = ?",
new String[]{"3", "4", "5", "22"}, 1),
new Pytanie("Ile dni ma tydzień?",
new String[]{"5", "6", "7", "8"}, 2),
new Pytanie("Jaki kolor ma trawa?",
new String[]{"Niebieski", "Czerwony", "Zielony", "Żółty"}, 2),
new Pytanie("Który język to Android?",
new String[]{"Python", "Java", "C++", "Ruby"}, 1),
new Pytanie("Ile miesięcy ma rok?",
new String[]{"10", "11", "12", "13"}, 2),
new Pytanie("Największy ocean to:",
new String[]{"Atlantycki", "Indyjski", "Spokojny", "Arktyczny"}, 2),
new Pytanie("Ile nóg ma pająk?",
new String[]{"6", "8", "10", "4"}, 1),
new Pytanie("Który pierwiastek ma symbol O?",
new String[]{"Złoto", "Tlen", "Ołów", "Osmium"}, 1),
new Pytanie("Ile centymetrów ma metr?",
new String[]{"10", "100", "1000", "50"}, 1),
new Pytanie("Kto namalował Mona Lisę?",
new String[]{"Picasso", "Van Gogh", "Da Vinci", "Rembrandt"}, 2),
new Pytanie("Ile planet ma Układ Słoneczny?",
new String[]{"7", "8", "9", "10"}, 1)
};
// Losujemy 3 różne indeksy
Random random = new Random();
pytania = new Pytanie[3];
boolean[] uzyte = new boolean[baza.length];
for (int i = 0; i < 3; i++) {
int los;
do {
los = random.nextInt(baza.length);
} while (uzyte[los]);
uzyte[los] = true;
pytania[i] = baza[los];
}
}
uzyte[] pamięta które pytania już wylosowaliśmy. Pętla do-while losuje tak długo, aż trafi na nieużyty indeks. Proste i skuteczne!
private void wyswietlPytanie() {
Pytanie p = pytania[aktualnePytanie];
txtNumerPytania.setText("Pytanie " + (aktualnePytanie + 1) + "/" + pytania.length);
txtPytanie.setText(p.tresc);
rbOdp1.setText(p.odpowiedzi[0]);
rbOdp2.setText(p.odpowiedzi[1]);
rbOdp3.setText(p.odpowiedzi[2]);
rbOdp4.setText(p.odpowiedzi[3]);
radioGroup.clearCheck();
}
private void sprawdzOdpowiedz() {
int zaznaczonyId = radioGroup.getCheckedRadioButtonId();
if (zaznaczonyId == -1) {
Toast.makeText(this, "Wybierz odpowiedz!", Toast.LENGTH_SHORT).show();
return;
}
int wybranaOdp = -1;
if (zaznaczonyId == R.id.rbOdp1) wybranaOdp = 0;
else if (zaznaczonyId == R.id.rbOdp2) wybranaOdp = 1;
else if (zaznaczonyId == R.id.rbOdp3) wybranaOdp = 2;
else if (zaznaczonyId == R.id.rbOdp4) wybranaOdp = 3;
if (wybranaOdp == pytania[aktualnePytanie].poprawna) {
wynik++;
}
aktualnePytanie++;
if (aktualnePytanie < pytania.length) {
wyswietlPytanie();
} else {
pokazWynik();
}
}
private void pokazWynik() {
txtPytanie.setVisibility(View.GONE);
radioGroup.setVisibility(View.GONE);
txtNumerPytania.setVisibility(View.GONE);
txtWynik.setVisibility(View.VISIBLE);
txtWynik.setText("Twoj wynik: " + wynik + "/" + pytania.length);
btnNastepne.setText("Zagraj ponownie");
btnNastepne.setOnClickListener(v -> restartQuiz());
}
private void restartQuiz() {
aktualnePytanie = 0;
wynik = 0;
txtPytanie.setVisibility(View.VISIBLE);
radioGroup.setVisibility(View.VISIBLE);
txtNumerPytania.setVisibility(View.VISIBLE);
txtWynik.setVisibility(View.GONE);
btnNastepne.setText("Nastepne");
btnNastepne.setOnClickListener(v -> sprawdzOdpowiedz());
losujPytania(); // nowe losowanie!
wyswietlPytanie();
}
losujPytania() — gracz dostaje nowy zestaw 3 pytań!
💻 Quiz — Kod
Klasa Pytanie.java
package com.example.quiz;
public class Pytanie {
String tresc;
String[] odpowiedzi;
int poprawna; // indeks 0-3
public Pytanie(String tresc, String[] odpowiedzi, int poprawna) {
this.tresc = tresc;
this.odpowiedzi = odpowiedzi;
this.poprawna = poprawna;
}
}
MainActivity.java
package com.example.quiz;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.util.Random;
public class MainActivity extends AppCompatActivity {
private TextView txtNumerPytania, txtPytanie, txtWynik;
private RadioGroup radioGroup;
private RadioButton rbOdp1, rbOdp2, rbOdp3, rbOdp4;
private Button btnNastepne;
private Pytanie[] pytania; // wylosowane 3 pytania
private int aktualnePytanie = 0;
private int wynik = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txtNumerPytania = findViewById(R.id.txtNumerPytania);
txtPytanie = findViewById(R.id.txtPytanie);
txtWynik = findViewById(R.id.txtWynik);
radioGroup = findViewById(R.id.radioGroup);
rbOdp1 = findViewById(R.id.rbOdp1);
rbOdp2 = findViewById(R.id.rbOdp2);
rbOdp3 = findViewById(R.id.rbOdp3);
rbOdp4 = findViewById(R.id.rbOdp4);
btnNastepne = findViewById(R.id.btnNastepne);
losujPytania();
wyswietlPytanie();
btnNastepne.setOnClickListener(v -> sprawdzOdpowiedz());
}
private void losujPytania() {
Pytanie[] baza = {
new Pytanie("Stolica Polski to:",
new String[]{"Kraków", "Warszawa", "Gdańsk", "Poznań"}, 1),
new Pytanie("2 + 2 = ?",
new String[]{"3", "4", "5", "22"}, 1),
new Pytanie("Ile dni ma tydzień?",
new String[]{"5", "6", "7", "8"}, 2),
new Pytanie("Jaki kolor ma trawa?",
new String[]{"Niebieski", "Czerwony", "Zielony", "Żółty"}, 2),
new Pytanie("Który język to Android?",
new String[]{"Python", "Java", "C++", "Ruby"}, 1),
new Pytanie("Ile miesięcy ma rok?",
new String[]{"10", "11", "12", "13"}, 2),
new Pytanie("Największy ocean to:",
new String[]{"Atlantycki", "Indyjski", "Spokojny", "Arktyczny"}, 2),
new Pytanie("Ile nóg ma pająk?",
new String[]{"6", "8", "10", "4"}, 1),
new Pytanie("Który pierwiastek ma symbol O?",
new String[]{"Złoto", "Tlen", "Ołów", "Osmium"}, 1),
new Pytanie("Ile centymetrów ma metr?",
new String[]{"10", "100", "1000", "50"}, 1),
new Pytanie("Kto namalował Mona Lisę?",
new String[]{"Picasso", "Van Gogh", "Da Vinci", "Rembrandt"}, 2),
new Pytanie("Ile planet ma Układ Słoneczny?",
new String[]{"7", "8", "9", "10"}, 1)
};
Random random = new Random();
pytania = new Pytanie[3];
boolean[] uzyte = new boolean[baza.length];
for (int i = 0; i < 3; i++) {
int los;
do {
los = random.nextInt(baza.length);
} while (uzyte[los]);
uzyte[los] = true;
pytania[i] = baza[los];
}
}
private void wyswietlPytanie() {
Pytanie p = pytania[aktualnePytanie];
txtNumerPytania.setText("Pytanie " + (aktualnePytanie + 1) + "/" + pytania.length);
txtPytanie.setText(p.tresc);
rbOdp1.setText(p.odpowiedzi[0]);
rbOdp2.setText(p.odpowiedzi[1]);
rbOdp3.setText(p.odpowiedzi[2]);
rbOdp4.setText(p.odpowiedzi[3]);
radioGroup.clearCheck();
}
private void sprawdzOdpowiedz() {
int zaznaczonyId = radioGroup.getCheckedRadioButtonId();
if (zaznaczonyId == -1) {
Toast.makeText(this, "Wybierz odpowiedź!", Toast.LENGTH_SHORT).show();
return;
}
int wybranaOdp = -1;
if (zaznaczonyId == R.id.rbOdp1) wybranaOdp = 0;
else if (zaznaczonyId == R.id.rbOdp2) wybranaOdp = 1;
else if (zaznaczonyId == R.id.rbOdp3) wybranaOdp = 2;
else if (zaznaczonyId == R.id.rbOdp4) wybranaOdp = 3;
if (wybranaOdp == pytania[aktualnePytanie].poprawna) {
wynik++;
}
aktualnePytanie++;
if (aktualnePytanie < pytania.length) {
wyswietlPytanie();
} else {
pokazWynik();
}
}
private void pokazWynik() {
txtPytanie.setVisibility(View.GONE);
radioGroup.setVisibility(View.GONE);
txtNumerPytania.setVisibility(View.GONE);
txtWynik.setVisibility(View.VISIBLE);
txtWynik.setText("Twój wynik: " + wynik + "/" + pytania.length);
btnNastepne.setText("Zagraj ponownie");
btnNastepne.setOnClickListener(v -> restartQuiz());
}
private void restartQuiz() {
aktualnePytanie = 0;
wynik = 0;
txtPytanie.setVisibility(View.VISIBLE);
radioGroup.setVisibility(View.VISIBLE);
txtNumerPytania.setVisibility(View.VISIBLE);
txtWynik.setVisibility(View.GONE);
btnNastepne.setText("Następne");
btnNastepne.setOnClickListener(v -> sprawdzOdpowiedz());
losujPytania();
wyswietlPytanie();
}
}
Quiz — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
getCheckedRadioButtonId() gdy nic nie jest zaznaczone?Testy manualne
| Test | Oczekiwany wynik |
|---|---|
| Rozpocznij quiz | Pytanie 1/3 wyświetlone |
| Kliknij Następne bez wyboru | Toast "Wybierz odpowiedź!" |
| Odpowiedz poprawnie | Przejście do następnego pytania |
| Ukończ quiz (5 poprawnych) | Wynik: 5/5 |
| Zagraj ponownie | Quiz od początku |
Rozgrzewka — Ekran logowania
Szybkie pytania na początek lekcji — kliknij pytanie żeby zobaczyć odpowiedź.
1. Co to jest Activity?
Activity to jeden ekran w aplikacji Android. Każdy ekran (logowanie, lista, szczegóły) to osobna Activity z własnym layoutem.
2. Czym jest Intent?
Intent (pol. "zamiar") to obiekt który mówi Androidowi "chcę otworzyć inny ekran". Jest jak wiadomość: "z ekranu A przejdź do ekranu B".
3. Jak przekazać dane między dwoma Activity?
Przez putExtra("klucz", wartosc) w Intencie. W drugiej Activity odczytujemy: getIntent().getStringExtra("klucz"). Klucze MUSZĄ być identyczne!
4. Jak ukryć tekst hasła (kropki zamiast liter)?
W EditText ustaw android:inputType="textPassword". Tekst będzie automatycznie zamieniany na kropki.
5. Co robi finish()?
Zamyka bieżącą Activity i wraca do poprzedniej. Jak zamknięcie karty w przeglądarce.
Ekran logowania — Instrukcja
Treść zadania
Wykonaj aplikację z dwoma ekranami:
- Ekran 1 (MainActivity): formularz logowania — pole nazwy użytkownika, pole hasła, przycisk "Zaloguj"
- Pole hasła musi maskować tekst (kropki zamiast liter)
- Walidacja: puste pola, hasło min. 4 znaki
- Poprawne dane:
admin/1234— przekierowanie na ekran 2 - Ekran 2 (WelcomeActivity): napis "Witaj, [nazwa]!" + przycisk "Wyloguj"
- Wylogowanie wraca do ekranu logowania
- Komunikaty błędów przez Toast
Czego się nauczysz
| Pojęcie | Co robi |
|---|---|
Intent | Przejście między dwoma Activity |
| Druga Activity | Tworzenie kolejnego ekranu (New → Activity) |
putExtra / getStringExtra | Przekazywanie danych między ekranami |
inputType="textPassword" | Maskowanie hasła kropkami |
Toast | Krótki komunikat dla użytkownika |
finish() | Zamknięcie Activity (powrót) |
Ekran logowania — Budowa krok po kroku
New Project → Empty Views Activity, nazwa Logowanie, język Java.
W activity_main.xml Component Tree → prawym na ConstraintLayout → Convert view → LinearLayout.
Ustaw atrybuty root layoutu:
orientation | vertical |
padding | 32dp |
gravity | center |
TextView (tytuł):
id | tytul |
text | Ekran logowania |
textSize | 28sp |
textStyle | bold |
layout_marginBottom | 40dp |
EditText (login): Palette → Text → Plain Text
id | poleUzytkownik |
hint | Nazwa uzytkownika |
inputType | text |
layout_marginBottom | 16dp |
EditText (hasło): Palette → Text → Password (NIE Plain Text!)
id | poleHaslo |
hint | Haslo |
inputType | textPassword |
layout_marginBottom | 24dp |
inputType="textPassword" automatycznie zamienia litery na kropki (●●●●). Dzięki temu nikt nie podejrzy hasła zerkając na ekran.
id | btnZaloguj |
text | ZALOGUJ |
layout_width | match_parent |
backgroundTint | #2196F3 |
textColor | #FFFFFF |
Component Tree powinno wyglądać tak:
LinearLayout (vertical, gravity=center)
├── TextView "tytul"
├── EditText "poleUzytkownik"
├── EditText "poleHaslo"
└── Button "btnZaloguj"
To kluczowy moment lekcji — pierwszy raz tworzysz drugi ekran!
- W panelu Project kliknij prawym na pakiet
com.example.logowanie - New → Activity → Empty Views Activity
- Activity Name:
WelcomeActivity - Layout Name:
activity_welcome(wypełni się automatycznie) - Kliknij Finish
WelcomeActivity.java— logika drugiego ekranuactivity_welcome.xml— wygląd drugiego ekranu- Wpis w
AndroidManifest.xmlrejestrujący nową Activity
AndroidManifest.xml — tam są wszystkie Activity w aplikacji.
Otwórz activity_welcome.xml i zmień root na LinearLayout (vertical, padding 32dp, gravity center) — tak samo jak w activity_main.
TextView (powitanie):
id | tekstPowitalny |
text | Witaj! |
textSize | 32sp |
textStyle | bold |
layout_marginBottom | 40dp |
Button (wyloguj):
id | btnWyloguj |
text | WYLOGUJ |
layout_width | match_parent |
backgroundTint | #F44336 |
textColor | #FFFFFF |
Otwórz MainActivity.java. Dodaj pola klasy nad metodą onCreate:
private EditText poleUzytkownik, poleHaslo;
private Button btnZaloguj;
// Poprawne dane logowania (na sztywno)
private String poprawnyUzytkownik = "admin";
private String poprawneHaslo = "1234";
Wewnątrz onCreate, pod setContentView:
poleUzytkownik = findViewById(R.id.poleUzytkownik);
poleHaslo = findViewById(R.id.poleHaslo);
btnZaloguj = findViewById(R.id.btnZaloguj);
btnZaloguj.setOnClickListener(v -> zaloguj());
Dodaj metodę zaloguj() pod onCreate:
private void zaloguj() {
String uzytkownik = poleUzytkownik.getText().toString().trim();
String haslo = poleHaslo.getText().toString().trim();
// Walidacja
if (uzytkownik.isEmpty()) {
Toast.makeText(this, "Wpisz nazwe uzytkownika!", Toast.LENGTH_SHORT).show();
return;
}
if (haslo.isEmpty()) {
Toast.makeText(this, "Wpisz haslo!", Toast.LENGTH_SHORT).show();
return;
}
if (haslo.length() < 4) {
Toast.makeText(this, "Haslo musi miec minimum 4 znaki!", Toast.LENGTH_SHORT).show();
return;
}
// Sprawdzenie poprawności
if (uzytkownik.equals(poprawnyUzytkownik) && haslo.equals(poprawneHaslo)) {
Intent zamiar = new Intent(MainActivity.this, WelcomeActivity.class);
zamiar.putExtra("nazwaUzytkownika", uzytkownik);
startActivity(zamiar);
} else {
Toast.makeText(this, "Bledna nazwa uzytkownika lub haslo!", Toast.LENGTH_SHORT).show();
}
}
.trim()— usuwa spacje na początku i końcu (np. " admin " → "admin")return— kończy metodę, dalszy kod się nie wykonaIntent— "zamiar" przejścia do innego ekranuputExtra("klucz", wartosc)— dokleja dane do Intentu (jak list w kopercie)startActivity(zamiar)— wykonuje przejście
Otwórz WelcomeActivity.java. Dodaj pola klasy:
private TextView tekstPowitalny;
private Button btnWyloguj;
W onCreate, pod setContentView:
tekstPowitalny = findViewById(R.id.tekstPowitalny);
btnWyloguj = findViewById(R.id.btnWyloguj);
// Odbierz dane z Intentu
String uzytkownik = getIntent().getStringExtra("nazwaUzytkownika");
tekstPowitalny.setText("Witaj, " + uzytkownik + "!");
// Wyloguj = zamknij ten ekran
btnWyloguj.setOnClickListener(v -> finish());
Intent= poszedłem do sklepu (otworzyłem nowy ekran)putExtra= wziąłem ze sobą listę zakupów (dane)getStringExtra= w sklepie odczytałem listęfinish()= wyszedłem ze sklepu, wracam do domu (poprzedni ekran)
putExtra("nazwaUzytkownika", ...) i getStringExtra("nazwaUzytkownika") klucz musi być co do litery taki sam — inaczej dostaniesz null.
Ekran logowania — Gotowy kod
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="32dp"
android:gravity="center">
<TextView
android:id="@+id/tytul"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Ekran logowania"
android:textSize="28sp"
android:textStyle="bold"
android:textAlignment="center"
android:layout_marginBottom="40dp" />
<EditText
android:id="@+id/poleUzytkownik"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nazwa uzytkownika"
android:inputType="text"
android:layout_marginBottom="16dp" />
<EditText
android:id="@+id/poleHaslo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Haslo"
android:inputType="textPassword"
android:layout_marginBottom="24dp" />
<Button
android:id="@+id/btnZaloguj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ZALOGUJ"
android:textSize="16sp"
android:backgroundTint="#2196F3"
android:textColor="#FFFFFF" />
</LinearLayout>
activity_welcome.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="32dp"
android:gravity="center">
<TextView
android:id="@+id/tekstPowitalny"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Witaj!"
android:textSize="32sp"
android:textStyle="bold"
android:textAlignment="center"
android:layout_marginBottom="40dp" />
<Button
android:id="@+id/btnWyloguj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="WYLOGUJ"
android:textSize="16sp"
android:backgroundTint="#F44336"
android:textColor="#FFFFFF" />
</LinearLayout>
MainActivity.java
package com.example.logowanie;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private EditText poleUzytkownik, poleHaslo;
private Button btnZaloguj;
private String poprawnyUzytkownik = "admin";
private String poprawneHaslo = "1234";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
poleUzytkownik = findViewById(R.id.poleUzytkownik);
poleHaslo = findViewById(R.id.poleHaslo);
btnZaloguj = findViewById(R.id.btnZaloguj);
btnZaloguj.setOnClickListener(v -> zaloguj());
}
private void zaloguj() {
String uzytkownik = poleUzytkownik.getText().toString().trim();
String haslo = poleHaslo.getText().toString().trim();
if (uzytkownik.isEmpty()) {
Toast.makeText(this, "Wpisz nazwe uzytkownika!", Toast.LENGTH_SHORT).show();
return;
}
if (haslo.isEmpty()) {
Toast.makeText(this, "Wpisz haslo!", Toast.LENGTH_SHORT).show();
return;
}
if (haslo.length() < 4) {
Toast.makeText(this, "Haslo musi miec minimum 4 znaki!", Toast.LENGTH_SHORT).show();
return;
}
if (uzytkownik.equals(poprawnyUzytkownik) && haslo.equals(poprawneHaslo)) {
Intent zamiar = new Intent(MainActivity.this, WelcomeActivity.class);
zamiar.putExtra("nazwaUzytkownika", uzytkownik);
startActivity(zamiar);
} else {
Toast.makeText(this, "Bledna nazwa uzytkownika lub haslo!", Toast.LENGTH_SHORT).show();
}
}
}
WelcomeActivity.java
package com.example.logowanie;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
public class WelcomeActivity extends AppCompatActivity {
private TextView tekstPowitalny;
private Button btnWyloguj;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
tekstPowitalny = findViewById(R.id.tekstPowitalny);
btnWyloguj = findViewById(R.id.btnWyloguj);
String uzytkownik = getIntent().getStringExtra("nazwaUzytkownika");
tekstPowitalny.setText("Witaj, " + uzytkownik + "!");
btnWyloguj.setOnClickListener(v -> finish());
}
}
Scenariusze testowe
| Test | Oczekiwany wynik |
|---|---|
| Puste pola → kliknij ZALOGUJ | Toast "Wpisz nazwe uzytkownika!" |
| Tylko hasło → kliknij ZALOGUJ | Toast "Wpisz nazwe uzytkownika!" |
| Tylko login → kliknij ZALOGUJ | Toast "Wpisz haslo!" |
| admin / 12 → ZALOGUJ | Toast "Haslo musi miec minimum 4 znaki!" |
| jan / 5678 → ZALOGUJ | Toast "Bledna nazwa uzytkownika lub haslo!" |
| admin / 1234 → ZALOGUJ | Otwiera ekran "Witaj, admin!" |
| Kliknij WYLOGUJ | Powrót do ekranu logowania |
| Wpisz hasło | Widać kropki, nie litery |
Ekran logowania — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
Intent w Androidzie?finish()?getStringExtra jest INNY niż w putExtra?📜 Android — Ściąga
Podstawowe kontrolki
| Kontrolka | XML | Java |
|---|---|---|
| TextView | android:text="..." | txt.setText(...) |
| EditText | android:hint="..." | edt.getText().toString() |
| Button | android:onClick="metoda" | btn.setOnClickListener(...) |
| ImageView | android:src="@drawable/..." | img.setImageResource(...) |
| CheckBox | android:checked="true" | cb.isChecked() |
| RadioButton | W RadioGroup | rg.getCheckedRadioButtonId() |
Layouty
| Layout | Użycie |
|---|---|
| LinearLayout | android:orientation="vertical|horizontal" |
| ConstraintLayout | app:layout_constraint...="parent|@id/xxx" |
| RelativeLayout | android:layout_below="@id/xxx" |
Wymiary
| Wartość | Znaczenie |
|---|---|
match_parent | Wypełnij rodzica |
wrap_content | Dopasuj do zawartości |
dp | Density-independent pixels (wymiary) |
sp | Scale-independent pixels (tekst) |
Pobieranie kontrolek
// W onCreate():
TextView txt = findViewById(R.id.txtNazwa);
EditText edt = findViewById(R.id.edtPole);
Button btn = findViewById(R.id.btnAkcja);
Obsługa kliknięcia
btn.setOnClickListener(v -> {
// kod obsługi
});
// Lub dla wielu przycisków:
btn.setOnClickListener(v -> {
if (v.getId() == R.id.btn1) { ... }
else if (v.getId() == R.id.btn2) { ... }
});
Toast
Toast.makeText(this, "Komunikat", Toast.LENGTH_SHORT).show();
AlertDialog
new AlertDialog.Builder(this)
.setTitle("Tytuł")
.setMessage("Treść")
.setPositiveButton("OK", (dialog, which) -> { /* akcja */ })
.setNegativeButton("Anuluj", null)
.show();
ListView + Adapter
ArrayList<String> lista = new ArrayList<>();
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this, android.R.layout.simple_list_item_1, lista);
listView.setAdapter(adapter);
// Po zmianach:
adapter.notifyDataSetChanged();
SharedPreferences
// Zapis
SharedPreferences prefs = getSharedPreferences("Nazwa", MODE_PRIVATE);
prefs.edit().putString("klucz", "wartość").apply();
// Odczyt
String wartosc = prefs.getString("klucz", "domyślna");
🐛 Android — Typowe błędy
NullPointerException na kontrolce
Przyczyny:
- Literówka w ID:
R.id.txtWynik≠android:id="@+id/txtwynik" - Wywołanie przed
setContentView() - ID z innego layoutu
NumberFormatException
// ✅ Zawsze try-catch:
try {
double x = Double.parseDouble(edt.getText().toString());
} catch (NumberFormatException e) {
Toast.makeText(this, "Podaj liczbę!", Toast.LENGTH_SHORT).show();
}
Lista nie odświeża się
Przyczyna: Brak adapter.notifyDataSetChanged()
lista.add("nowy");
adapter.notifyDataSetChanged(); // WYMAGANE!
RadioGroup nie zwraca wyboru
Przyczyny:
- Użytkownik nic nie zaznaczył — sprawdź warunek!
- RadioButtony nie są w RadioGroup
int id = radioGroup.getCheckedRadioButtonId();
if (id == -1) {
Toast.makeText(this, "Wybierz opcję!", Toast.LENGTH_SHORT).show();
return;
}
Crash przy orientacji ekranu
Przyczyna: Activity jest tworzone od nowa przy obrocie.
Rozwiązanie: Zapisz stan w onSaveInstanceState() lub zablokuj orientację:
<!-- W AndroidManifest.xml: -->
<activity android:screenOrientation="portrait">
Zasób nie znaleziony
Przyczyny:
- Brak pliku w
res/drawable/ - Błędna ścieżka w
setContentView() - Brak wpisu w strings.xml
1. Podstawy Python
Python to interpretowany język o dynamicznej typizacji. Bloki kodu wyznacza indentacja (4 spacje), nie klamry.
Zmienne i typy
imie = "Maks" # str
wiek = 18 # int
srednia = 4.75 # float
zdaje = True # bool
print(type(imie)) # <class 'str'>
input() i print()
# input() ZAWSZE zwraca string!
tekst = input("Podaj imię: ")
liczba = int(input("Podaj wiek: "))
print(f"Cześć {tekst}, masz {liczba} lat") # f-string
Operatory
| Operator | Opis | Przykład |
|---|---|---|
+ - * / | Arytmetyczne | 10 / 3 → 3.333 |
// | Dzielenie całkowite | 10 // 3 → 3 |
% | Reszta z dzielenia | 10 % 3 → 1 |
** | Potęgowanie | 2 ** 3 → 8 |
== != < > | Porównania | 5 == 5 → True |
and or not | Logiczne | True and False → False |
Konwersja typów
int("42") # 42
float("3.14") # 3.14
str(100) # "100"
bool(0) # False — 0, "", [], None → False
📝 Zadanie praktyczne
Napisz program, który pobiera od użytkownika dwie liczby całkowite i wyświetla ich sumę, różnicę, iloczyn oraz resztę z dzielenia pierwszej przez drugą.
a = int(input("Podaj pierwszą liczbę: "))
b = int(input("Podaj drugą liczbę: "))
print(f"Suma: {a + b}")
print(f"Różnica: {a - b}")
print(f"Iloczyn: {a * b}")
print(f"Reszta z dzielenia: {a % b}")
2. Instrukcje warunkowe
if / elif / else
ocena = int(input("Podaj ocenę: "))
if ocena >= 5:
print("Bardzo dobrze!")
elif ocena >= 3:
print("Zaliczone")
else:
print("Niezaliczone")
if/elif/else i indentacji 4 spacje w bloku.Operatory porównania
x == y # równe
x != y # różne
x > y # większe
x >= y # większe lub równe
x < y # mniejsze
x <= y # mniejsze lub równe
Operatory logiczne
if wiek >= 18 and posiada_prawo_jazdy:
print("Może jeździć")
if miasto == "Kraków" or miasto == "Warszawa":
print("Duże miasto")
if not zalogowany:
print("Zaloguj się")
in / not in
if "Python" in jezyki:
print("Zna Pythona!")
if extension not in [".jpg", ".png", ".gif"]:
print("Nieobsługiwany format")
Zagnieżdżone warunki
if wiek >= 18:
if ma_bilet:
print("Wejście dozwolone")
else:
print("Kup bilet")
else:
print("Za młody")
📝 Zadanie praktyczne
Napisz program, który pobiera punkty z egzaminu (0-100) i wyświetla ocenę: 90-100 → 5, 75-89 → 4, 60-74 → 3, 50-59 → 2, poniżej 50 → 1.
punkty = int(input("Podaj punkty (0-100): "))
if punkty >= 90:
ocena = 5
elif punkty >= 75:
ocena = 4
elif punkty >= 60:
ocena = 3
elif punkty >= 50:
ocena = 2
else:
ocena = 1
print(f"Twoja ocena: {ocena}")
3. Pętle
for + range()
# range(stop) — od 0 do stop-1
for i in range(5):
print(i) # 0, 1, 2, 3, 4
# range(start, stop, step)
for i in range(1, 11, 2):
print(i) # 1, 3, 5, 7, 9
for po kolekcji
owoce = ["jabłko", "banan", "gruszka"]
for owoc in owoce:
print(owoc)
# z indeksem
for i, owoc in enumerate(owoce):
print(f"{i}: {owoc}")
while
licznik = 0
while licznik < 5:
print(licznik)
licznik += 1 # WAŻNE: bez tego → nieskończona pętla!
break i continue
# break — przerwij pętlę
for i in range(100):
if i == 5:
break # wychodzi z pętli
print(i) # 0, 1, 2, 3, 4
# continue — pomiń iterację
for i in range(5):
if i == 2:
continue # pomija 2
print(i) # 0, 1, 3, 4
Zagnieżdżone pętle
# Tabliczka mnożenia 3×3
for i in range(1, 4):
for j in range(1, 4):
print(f"{i}×{j}={i*j}", end=" ")
print() # nowa linia po wierszu
📝 Zadanie praktyczne
Napisz program, który pobiera 5 liczb od użytkownika (w pętli) i oblicza ich sumę oraz średnią.
suma = 0
ilosc = 5
for i in range(ilosc):
liczba = float(input(f"Podaj liczbę {i + 1}: "))
suma += liczba
srednia = suma / ilosc
print(f"Suma: {suma}")
print(f"Średnia: {srednia:.2f}")
4. Funkcje
Podstawowa składnia
def powitaj(imie):
return f"Cześć, {imie}!"
wynik = powitaj("Maks")
print(wynik) # Cześć, Maks!
Parametry domyślne
def potega(baza, wykladnik=2):
return baza ** wykladnik
print(potega(3)) # 9 (domyślnie kwadrat)
print(potega(2, 10)) # 1024
Wiele wartości (tuple)
def min_max(lista):
return min(lista), max(lista)
najmn, najw = min_max([3, 1, 7, 2])
print(najmn, najw) # 1 7
Zasięg zmiennych
x = 10 # globalna
def test():
x = 5 # lokalna — NIE zmienia globalnej
print(x) # 5
test()
print(x) # 10 (globalna niezmieniona)
Funkcja jako argument
# sorted() z key=
produkty = [("Laptop", 3500), ("Mysz", 130), ("Monitor", 1300)]
posortowane = sorted(produkty, key=lambda p: p[1])
# → [("Mysz", 130), ("Monitor", 1300), ("Laptop", 3500)]
📝 Zadanie praktyczne
Napisz funkcję pole_trojkata(a, h), która przyjmuje podstawę i wysokość, a zwraca pole trójkąta. Program ma pobierać dane i wywołać funkcję.
def pole_trojkata(a, h):
return (a * h) / 2
podstawa = float(input("Podaj podstawę: "))
wysokosc = float(input("Podaj wysokość: "))
wynik = pole_trojkata(podstawa, wysokosc)
print(f"Pole trójkąta: {wynik:.2f}")
5. Stringi
Tworzenie i f-string
imie = "Maks"
ocena = 4.5
# f-string (Python 3.6+)
print(f"{imie} ma średnią {ocena:.1f}")
# → Maks ma średnią 4.5
# Formatowanie liczb
cena = 1299.5
print(f"Cena: {cena:.2f} zł") # 1299.50 zł
print(f"ID: {42:05d}") # 00042
Metody stringów
| Metoda | Opis | Przykład |
|---|---|---|
.upper() | Wielkie litery | "abc".upper() → "ABC" |
.lower() | Małe litery | "ABC".lower() → "abc" |
.strip() | Usuwa białe znaki | " hi ".strip() → "hi" |
.split(sep) | Dzieli na listę | "a;b;c".split(";") → ["a","b","c"] |
.join(list) | Łączy listę | ", ".join(["a","b"]) → "a, b" |
.replace(a,b) | Zamiana | "kot".replace("o","a") → "kat" |
.find(x) | Indeks (lub -1) | "hello".find("ll") → 2 |
.startswith(x) | Czy zaczyna | "python".startswith("py") → True |
.count(x) | Ile wystąpień | "banana".count("a") → 3 |
Slicing
tekst = "Python"
tekst[0] # "P"
tekst[-1] # "n"
tekst[0:3] # "Pyt"
tekst[2:] # "thon"
tekst[::-1] # "nohtyP" (odwrócenie)
📝 Zadanie praktyczne
Napisz program, który pobiera imię i nazwisko użytkownika (osobno) i wyświetla: inicjały (np. "M.K."), nazwisko wielkimi literami, oraz ilość liter w imieniu.
imie = input("Podaj imię: ")
nazwisko = input("Podaj nazwisko: ")
inicjaly = f"{imie[0].upper()}.{nazwisko[0].upper()}."
print(f"Inicjały: {inicjaly}")
print(f"Nazwisko: {nazwisko.upper()}")
print(f"Ilość liter w imieniu: {len(imie)}")
6. Listy
Tworzenie i indeksowanie
oceny = [5, 4, 3, 5, 4]
print(oceny[0]) # 5 (pierwszy)
print(oceny[-1]) # 4 (ostatni)
print(len(oceny)) # 5
Metody list
| Metoda | Opis |
|---|---|
.append(x) | Dodaj na koniec |
.insert(i, x) | Wstaw na pozycji i |
.remove(x) | Usuń pierwszą wartość x |
.pop(i) | Usuń i zwróć element i |
.sort() | Sortuj rosnąco (mutuje!) |
.reverse() | Odwróć (mutuje!) |
.index(x) | Indeks pierwszego x |
.count(x) | Ile razy x |
List comprehension
# Kwadraty od 1 do 5
kwadraty = [x**2 for x in range(1, 6)]
# → [1, 4, 9, 16, 25]
# Z warunkiem — tylko parzyste
parzyste = [x for x in range(10) if x % 2 == 0]
# → [0, 2, 4, 6, 8]
# Transformacja listy stringów
imiona = ["jan", "ola", "maks"]
duze = [i.capitalize() for i in imiona]
# → ["Jan", "Ola", "Maks"]
Sortowanie
# sorted() — zwraca NOWĄ listę
posortowane = sorted(oceny) # rosnąco
malejaco = sorted(oceny, reverse=True) # malejąco
# .sort() — mutuje oryginalną listę
oceny.sort()
# Sortowanie obiektów po kluczu
produkty = [{"nazwa": "B", "cena": 50}, {"nazwa": "A", "cena": 30}]
produkty.sort(key=lambda p: p["cena"])
Slicing list
l = [10, 20, 30, 40, 50]
l[1:3] # [20, 30]
l[:2] # [10, 20]
l[::2] # [10, 30, 50] (co drugi)
📝 Zadanie praktyczne
Napisz program, który tworzy listę 10 losowych liczb (1-100). Następnie wyświetl: największą, najmniejszą, średnią oraz listę posortowaną malejąco.
import random
liczby = [random.randint(1, 100) for _ in range(10)]
print(f"Lista: {liczby}")
print(f"Maksimum: {max(liczby)}")
print(f"Minimum: {min(liczby)}")
print(f"Średnia: {sum(liczby) / len(liczby):.2f}")
print(f"Malejąco: {sorted(liczby, reverse=True)}")
7. Słowniki i krotki
Słownik (dict)
uczen = {
"imie": "Maks",
"wiek": 18,
"oceny": [5, 4, 5, 3]
}
print(uczen["imie"]) # Maks
print(uczen.get("klasa", "?")) # ? (domyślna wartość)
Modyfikacja słownika
uczen["email"] = "[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
; 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
1. Projektujesz formularz wizualnie w Qt Creator (Designer) — przeciągasz widżety myszką
2. Zapisujesz formularz jako plik
.ui3. Konwertujesz na Python komendą:
pyuic6 mainwindow.ui -o mainwindow.py4. Tworzysz
main.py w VS Code — importujesz wygenerowany formularz i dodajesz logikę (funkcje, SQL, sygnały)Qt Creator — tworzenie formularza
- Otwórz Qt Creator → File → New → Qt Designer Form → wybierz Main Window → Create
- Z panelu Widget Box (po lewej) przeciągnij potrzebne widżety na formularz (np. QLabel, QPushButton, QListWidget)
- W panelu Property Editor (po prawej) ustaw
objectNamekażdego widżetu — to nazwa, przez którą odwołujesz się w kodzie (np.listWidget,comboBox,btnFiltruj,lblWartosc) - Ustaw layout: zaznacz formularz → prawy klik → Lay out in a Grid (lub Vertically / Horizontally)
- Zapisz jako
mainwindow.uiw folderze projektu
Konwersja: pyuic6
W terminalu (w folderze projektu) uruchom:
pyuic6 mainwindow.ui -o mainwindow.py
Komenda wygeneruje plik mainwindow.py z klasą Ui_MainWindow. Tego pliku nie edytujemy ręcznie — przy zmianach w formularzu uruchamiamy komendę ponownie.
Minimalne okno z formularzem
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Dostęp do widżetów
Po self.ui.setupUi(self) wszystkie widżety są dostępne przez self.ui:
# QLabel — odczyt i zapis tekstu
self.ui.label.setText("Nowy tekst")
# QLineEdit — pole tekstowe
tekst = self.ui.lineEdit.text()
self.ui.lineEdit.clear()
# QPushButton — podłączenie sygnału
self.ui.pushButton.clicked.connect(self.moja_funkcja)
# QListWidget — lista elementów
self.ui.listWidget.clear()
self.ui.listWidget.addItem("Element")
# QComboBox — lista rozwijana
self.ui.comboBox.addItems(["A", "B", "C"])
wybrane = self.ui.comboBox.currentText()
# QCheckBox — pole wyboru
if self.ui.checkBox.isChecked():
print("zaznaczony")
Tabela widżetów
| Widżet | Opis | Ważne metody |
|---|---|---|
| QLabel | Etykieta tekstowa | .setText(), .text() |
| QLineEdit | Pole tekstowe (input) | .text(), .setText(), .clear() |
| QPushButton | Przycisk | .clicked.connect(fn) |
| QListWidget | Lista elementów | .addItem(), .clear(), .currentRow() |
| QComboBox | Lista rozwijana | .addItems(), .currentText() |
| QCheckBox | Pole wyboru | .isChecked(), .stateChanged.connect(fn) |
| QStackedWidget | Kontener stron (widoków) | .setCurrentIndex(n), .currentIndex(), .count() |
Layouty (w Qt Creator)
| Grid Layout | Vertical / Horizontal Layout |
|---|---|
| Siatka (wiersze × kolumny) | Stackowanie (pionowo / poziomo) |
| Precyzyjna kontrola pozycji | Prosty układ liniowy |
| Prawy klik → Lay out in a Grid | Prawy klik → Lay out Vertically/Horizontally |
QMessageBox
from PyQt6.QtWidgets import QMessageBox
QMessageBox.information(window, "Sukces", "Dane zapisane!")
QMessageBox.critical(window, "Błąd", "Plik nie znaleziony")
odp = QMessageBox.question(window, "Pytanie", "Usunąć?")
📝 Zadanie praktyczne
W Qt Creator utwórz formularz (Main Window) z polem QLineEdit (objectName: lineEdit), przyciskiem QPushButton „Dodaj" (objectName: btnDodaj) i QListWidget (objectName: listWidget). Zapisz jako mainwindow.ui, skonwertuj komendą pyuic6 mainwindow.ui -o mainwindow.py. Następnie w main.py napisz logikę: po kliknięciu przycisku tekst z QLineEdit dodaje się do QListWidget.
# main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.btnDodaj.clicked.connect(self.dodaj)
def dodaj(self):
tekst = self.ui.lineEdit.text()
if tekst:
self.ui.listWidget.addItem(tekst)
self.ui.lineEdit.clear()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
10. SQLite
sqlite3 to wbudowany moduł Python do baz danych SQL. Na egzaminie INF.04 prawie zawsze trzeba utworzyć bazę, tabelę i wykonać zapytania.
Połączenie i tworzenie tabeli
import sqlite3
conn = sqlite3.connect("magazyn.db") # tworzy plik
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS produkty (
id INTEGER PRIMARY KEY,
nazwa TEXT NOT NULL,
kategoria TEXT,
cena REAL,
ilosc INTEGER
)
""")
conn.commit()
INSERT — wstawianie danych
# Jeden rekord
cursor.execute(
"INSERT INTO produkty VALUES (?, ?, ?, ?, ?)",
(1, "Laptop", "elektronika", 3499.99, 15)
)
# Wiele rekordów z pętli
for p in produkty:
cursor.execute(
"INSERT INTO produkty VALUES (?, ?, ?, ?, ?)",
(p["id"], p["nazwa"], p["kategoria"], p["cena"], p["ilosc"])
)
conn.commit()
? (parametry) zamiast f-stringów! Chroni przed SQL injection i błędami w danych.SELECT — zapytania
# Wszystkie rekordy
cursor.execute("SELECT * FROM produkty")
wyniki = cursor.fetchall() # lista krotek
for wiersz in wyniki:
print(wiersz) # (1, 'Laptop', 'elektronika', 3499.99, 15)
# Filtrowanie
cursor.execute(
"SELECT * FROM produkty WHERE kategoria = ?",
("elektronika",) # ← tuple z jednym elementem (przecinek!)
)
# Sortowanie
cursor.execute("SELECT * FROM produkty ORDER BY cena DESC")
# Agregacja
cursor.execute("SELECT kategoria, SUM(cena * ilosc) FROM produkty GROUP BY kategoria")
UPDATE i DELETE
# Aktualizacja
cursor.execute("UPDATE produkty SET cena = ? WHERE id = ?", (2999.99, 1))
# Usuwanie
cursor.execute("DELETE FROM produkty WHERE id = ?", (5,))
conn.commit()
Zamykanie połączenia
# Zawsze na końcu:
conn.close()
# Lub z with:
with sqlite3.connect("magazyn.db") as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM produkty")
print(cursor.fetchall())
Schemat typowy na egzaminie
# 1. Wczytaj dane z pliku
# 2. Utwórz bazę + tabelę
# 3. Wstaw dane z pliku do bazy
# 4. Wykonaj zapytania (filter, sort, aggregate)
# 5. Wyświetl wyniki w PyQt6
📝 Zadanie praktyczne
Utwórz bazę SQLite sklep.db z tabelą produkty (id, nazwa, cena). Wstaw 3 produkty i napisz zapytanie pokazujące produkty z ceną powyżej 100 zł, posortowane malejąco.
import sqlite3
conn = sqlite3.connect("sklep.db")
cursor = conn.cursor()
# Tworzenie tabeli
cursor.execute("""
CREATE TABLE IF NOT EXISTS produkty (
id INTEGER PRIMARY KEY,
nazwa TEXT NOT NULL,
cena REAL
)
""")
# Wstawianie danych
produkty = [
(1, "Laptop", 3500.00),
(2, "Mysz", 80.00),
(3, "Monitor", 1200.00)
]
cursor.executemany("INSERT OR REPLACE INTO produkty VALUES (?, ?, ?)", produkty)
conn.commit()
# Zapytanie
cursor.execute("SELECT * FROM produkty WHERE cena > 100 ORDER BY cena DESC")
wyniki = cursor.fetchall()
for p in wyniki:
print(f"{p[1]}: {p[2]:.2f} zł")
conn.close()
🔥 Rozgrzewka — Rejestr produktów
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
with open("dane.txt", "r", encoding="utf-8") as plik:. Dzięki with plik automatycznie się zamyka. Do odczytu: plik.read() (cały tekst) lub plik.readlines() (lista linii).split(";") i co zwraca?split(";") dzieli string na listę po podanym separatorze. Np. "a;b;c".split(";") zwraca ["a", "b", "c"]. Każdy element jest stringiem — jeśli potrzebujemy liczby, musimy rzutować: int() lub float().? zamiast f-stringa do wstawiania wartości??) chronią przed SQL Injection i unikają problemów z apostrofami w danych. cursor.execute("SELECT * FROM produkty WHERE kategoria = ?", (kategoria,)) — drugi argument to krotka z wartościami.CREATE TABLE IF NOT EXISTS. Przykład: cursor.execute("CREATE TABLE IF NOT EXISTS produkty (id INTEGER PRIMARY KEY, nazwa TEXT, cena REAL)"). Bez IF NOT EXISTS przy ponownym uruchomieniu pojawi się błąd.list_widget.addItem("tekst"). Aby wyczyścić listę przed ponownym załadowaniem: list_widget.clear(). QListWidget to widżet wyświetlający listę elementów tekstowych — odpowiednik Listboxa z tkinter.#01 — Instrukcja z arkusza egzaminacyjnego
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
5. Częste blokady
| Problem | Podpowiedź |
|---|---|
| IndentationError | „Sprawdź wcięcia — 4 spacje, nie tabulatory." |
| FileNotFoundError | „Czy dane.txt jest w tym samym folderze co main.py?" |
| split() nie dzieli poprawnie | „Jaki separator? Średnik? split(';')" |
| Cena to string po split | „split() daje stringi. float(parts[3]) — rzutuj." |
| SQL — tuple z jednym elementem | „(wartość,) — nie zapomnij przecinka!" |
| QListWidget puste | „Czy wywołujesz funkcję ładującą dane? Czy window.show() jest na końcu?" |
| ModuleNotFoundError: PyQt6 | „Zainstaluj: pip install PyQt6" |
6. Po zbudowaniu
„Dlaczego używamy '?' w SQL zamiast f-stringa?"
Odp: SQL injection + apostrofy w danych mogą zepsuć zapytanie.
„Co by się stało gdybyś zapomniał conn.commit()?"
Odp: Dane nie zapiszą się na dysk. Po zamknięciu programu — pusta baza.
#01 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Wczytanie dane.txt | with open(), split(";"), parsowanie typów | ✅ |
| Baza magazyn.db | sqlite3.connect() | ✅ |
| Tabela produkty | CREATE TABLE IF NOT EXISTS | ✅ |
| INSERT danych | Parametryzowane (?) z pętli | ✅ |
| SELECT wszystkie | ORDER BY cena DESC | ✅ |
| SELECT po kategorii | WHERE kategoria = ? | ✅ |
| Agregacja | SUM(cena * ilosc) GROUP BY | ✅ |
| PyQt6 okno | QMainWindow + setWindowTitle(„Rejestr produktów") | ✅ |
| QListWidget | Wyświetla produkty | ✅ |
| QComboBox kategorii | QComboBox + addItems() z listą kategorii | ✅ |
| Przycisk Filtruj | QPushButton + clicked.connect() → odśwież listę | ✅ |
| QLabel wartość | Łączna wartość = cena × ilość | ✅ |
| Znaczące nazwy | produkty, kategoria, wartosc, filtruj | ✅ |
#01 — Budowa krok po kroku
„Startujemy. Najpierw zaprojektujemy formularz w Qt Creator, a potem napiszemy logikę w VS Code."
1. Otwórz Qt Creator → File → New → Qt Designer Form → Main Window → Create
2. Przeciągnij na formularz widżety:
•
QLabel — tekst „Rejestr produktów" (objectName: lblTytul)•
QLabel — tekst „Kategoria:" (objectName: lblKategoria)•
QComboBox (objectName: comboBox)•
QPushButton — tekst „Filtruj" (objectName: btnFiltruj)•
QListWidget (objectName: listWidget)•
QLabel — tekst „Wartość: 0.00 zł" (objectName: lblWartosc)3. Zaznacz formularz → prawy klik → Lay out in a Grid
4. Zapisz jako
mainwindow.ui„Teraz konwertujemy formularz na kod Python. W terminalu wpisz:"
pyuic6 mainwindow.ui -o mainwindow.py
„Mamy plik mainwindow.py — tego nie ruszamy. Teraz tworzymy main.py w VS Code."
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
„Uruchom — widzisz okno z widżetami z formularza? Działa? Super, to nasz szkielet."
Uwaga: self.ui.setupUi(self) tworzy wszystkie widżety z formularza. Odwołujesz się do nich przez self.ui.nazwa, np. self.ui.listWidget, self.ui.comboBox.
„Najpierw stwórz plik dane.txt obok main.py — skopiuj 8 wierszy z instrukcji."
# Wczytaj dane z pliku do listy słowników
produkty = []
with open("dane.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
produkty.append({
"id": int(parts[0]),
"nazwa": parts[1],
"kategoria": parts[2],
"cena": float(parts[3]),
"ilosc": int(parts[4])
})
print(f"Wczytano {len(produkty)} produktów") # test
for p in produkty:
print(p)
„Dlaczego strip() przed split()?"
Odp: Każda linia ma \n na końcu. strip() go usuwa.
Jeśli ValueError przy int/float:
„Wydrukuj parts — może pusta linia na końcu pliku? Dodaj: if not linia.strip(): continue"
„Teraz tworzymy bazę magazyn.db i tabelę produkty. Python sam stworzy plik .db."
conn = sqlite3.connect("magazyn.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS produkty (
id INTEGER PRIMARY KEY,
nazwa TEXT NOT NULL,
kategoria TEXT,
cena REAL,
ilosc INTEGER
)
""")
conn.commit()
„Co robi IF NOT EXISTS?"
Odp: Nie wyrzuci błędu gdy tabela już istnieje — bezpieczne przy wielokrotnym uruchomieniu.
# Wyczyść starą tabelę (przy ponownym uruchomieniu)
cursor.execute("DELETE FROM produkty")
for p in produkty:
cursor.execute(
"INSERT INTO produkty VALUES (?, ?, ?, ?, ?)",
(p["id"], p["nazwa"], p["kategoria"], p["cena"], p["ilosc"])
)
conn.commit()
print("Dane wstawione do bazy")
„Dlaczego 5 znaków zapytania?"
Odp: Bo tabela ma 5 kolumn. Każdy ? to jedno pole.
Jeśli zapomni przecinka w tuple:
„(wartość) to nawiasy. (wartość,) to tuple. Python wymaga tuple."
„Przetestujmy zapytania w konsoli, zanim podepniemy pod GUI."
# Wszystkie posortowane po cenie
cursor.execute("SELECT * FROM produkty ORDER BY cena DESC")
for row in cursor.fetchall():
print(row)
# Filtr po kategorii
cursor.execute(
"SELECT * FROM produkty WHERE kategoria = ?",
("elektronika",) # ← przecinek = tuple!
)
print(cursor.fetchall())
# Wartość magazynu per kategoria
cursor.execute("""
SELECT kategoria, SUM(cena * ilosc) as wartosc
FROM produkty
GROUP BY kategoria
""")
for row in cursor.fetchall():
print(f"{row[0]}: {row[1]:.2f} zł")
„Co robi GROUP BY?"
Odp: Grupuje wiersze po kategorii i liczy SUM osobno dla każdej grupy.
„Widżety już mamy z Qt Creator! Teraz uzupełniamy comboBox kategoriami z bazy."
# Pobierz kategorie z bazy i dodaj do comboBox
cursor.execute("SELECT DISTINCT kategoria FROM produkty ORDER BY kategoria")
kategorie = ["wszystkie"] + [row[0] for row in cursor.fetchall()]
self.ui.comboBox.addItems(kategorie)
self.ui.comboBox.setCurrentText("wszystkie")
„Dlaczego pobieramy kategorie z bazy zamiast wpisywać ręcznie?"
Odp: Bo dane mogą się zmienić. DISTINCT + ORDER BY daje unikalne, posortowane kategorie.
Dostęp do widżetów: Wszystkie widżety z formularza Qt Creator są dostępne przez self.ui.nazwa. Nazwy ustawiłeś w objectName w Qt Creator.
„Piszemy funkcję, która pobiera dane z bazy i wrzuca do QListWidget."
def zaladuj_dane(self, kategoria="wszystkie"):
self.ui.listWidget.clear()
if kategoria == "wszystkie":
cursor.execute("SELECT * FROM produkty ORDER BY cena DESC")
else:
cursor.execute(
"SELECT * FROM produkty WHERE kategoria = ? ORDER BY cena DESC",
(kategoria,)
)
wyniki = cursor.fetchall()
wartosc = 0
for row in wyniki:
linia = f"{row[1]:<25} {row[3]:>10.2f} zł x{row[4]}"
self.ui.listWidget.addItem(linia)
wartosc += row[3] * row[4]
self.ui.lblWartosc.setText(f"Wartość: {wartosc:,.2f} zł")
# Załaduj dane od razu (na starcie)
self.zaladuj_dane()
„Co robi self.ui.listWidget.clear()?"
Odp: Czyści listę przed załadowaniem nowych danych. Bez tego — zduplikowane wpisy.
Dostęp do widżetów: self.ui.listWidget to QListWidget z formularza. self.ui.lblWartosc to QLabel. Nazwy odpowiadają objectName z Qt Creator.
„Ostatni krok — podpinamy przycisk Filtruj do funkcji filtrowania."
def filtruj(self):
wybrana = self.ui.comboBox.currentText()
self.zaladuj_dane(wybrana)
# Podłącz sygnał clicked do metody:
self.ui.btnFiltruj.clicked.connect(self.filtruj)
„Dlaczego self.ui.btnFiltruj.clicked.connect(self.filtruj) a nie clicked.connect(self.filtruj())?"
Odp: Z nawiasami wywołuje się od razu. Bez — przekazujesz referencję do funkcji. To tzw. mechanizm sygnałów i slotów w Qt.
Bonus: Dodaj dynamiczne kategorie — zamiast hardcodowanych, pobierz z bazy: SELECT DISTINCT kategoria FROM produkty
#01 — Gotowy kod
Formularz Qt Creator (mainwindow.ui)
| Widżet | objectName | Właściwości |
|---|---|---|
| QLabel | lblTytul | text: „Rejestr produktów", font: bold 18pt |
| QLabel | lblKategoria | text: „Kategoria:" |
| QComboBox | comboBox | (elementy dodawane z kodu) |
| QPushButton | btnFiltruj | text: „Filtruj" |
| QListWidget | listWidget | font: Consolas 10pt |
| QLabel | lblWartosc | text: „Wartość: 0.00 zł", font: bold 12pt |
Konwersja: pyuic6 mainwindow.ui -o mainwindow.py
main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# ===== 1. WCZYTANIE DANYCH =====
produkty = []
with open("dane.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
produkty.append({
"id": int(parts[0]),
"nazwa": parts[1],
"kategoria": parts[2],
"cena": float(parts[3]),
"ilosc": int(parts[4])
})
# ===== 2. BAZA DANYCH =====
self.conn = sqlite3.connect("magazyn.db")
self.cursor = self.conn.cursor()
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS produkty (
id INTEGER PRIMARY KEY,
nazwa TEXT NOT NULL,
kategoria TEXT,
cena REAL,
ilosc INTEGER
)
""")
self.cursor.execute("DELETE FROM produkty")
for p in produkty:
self.cursor.execute(
"INSERT INTO produkty VALUES (?, ?, ?, ?, ?)",
(p["id"], p["nazwa"], p["kategoria"], p["cena"], p["ilosc"])
)
self.conn.commit()
self.setup_logic()
def setup_logic(self):
# ===== 3. WIDŻETY =====
self.cursor.execute("SELECT DISTINCT kategoria FROM produkty ORDER BY kategoria")
kategorie = ["wszystkie"] + [row[0] for row in self.cursor.fetchall()]
self.ui.comboBox.addItems(kategorie)
self.ui.comboBox.setCurrentText("wszystkie")
# ===== 4. SYGNAŁY =====
self.ui.btnFiltruj.clicked.connect(self.filtruj)
self.zaladuj_dane()
# ===== 5. METODY =====
def zaladuj_dane(self, kategoria="wszystkie"):
self.ui.listWidget.clear()
if kategoria == "wszystkie":
self.cursor.execute("SELECT * FROM produkty ORDER BY cena DESC")
else:
self.cursor.execute(
"SELECT * FROM produkty WHERE kategoria = ? ORDER BY cena DESC",
(kategoria,)
)
wyniki = self.cursor.fetchall()
wartosc = 0
for row in wyniki:
linia = f"{row[1]:<25} {row[3]:>10.2f} zł x{row[4]}"
self.ui.listWidget.addItem(linia)
wartosc += row[3] * row[4]
self.ui.lblWartosc.setText(f"Wartość: {wartosc:,.2f} zł")
def filtruj(self):
wybrana = self.ui.comboBox.currentText()
self.zaladuj_dane(wybrana)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
dane.txt
1;Laptop Dell;elektronika;3499.99;15
2;Mysz Logitech;elektronika;129.99;50
3;Koszulka Nike;odzież;89.99;120
4;Słuchawki Sony;elektronika;249.99;30
5;Spodnie Levi's;odzież;199.99;45
6;Monitor LG;elektronika;1299.99;20
7;Bluza Adidas;odzież;149.99;60
8;Klawiatura Corsair;elektronika;359.99;25
📝 Sprawdzian — Rejestr produktów
5 pytań · 5 minut · minimum 60% żeby zdać
"10;Laptop;3499.99".split(";")?? w zapytaniach SQL zamiast f-stringów?filtruj?🔥 Rozgrzewka — Dziennik ucznia
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
sum(lista) / len(lista). Przykład: oceny = [4, 5, 3]; srednia = sum(oceny) / len(oceny). Można też użyć statistics.mean(lista) po imporcie modułu statistics.SELECT uczen, AVG(ocena) FROM oceny GROUP BY uczen. Funkcja AVG() liczy średnią, a GROUP BY grupuje wyniki według unikalnych wartości w kolumnie uczen.fetchone() od fetchall()?fetchone() zwraca jeden wiersz (krotkę) lub None. fetchall() zwraca listę wszystkich wierszy. Dla dużych zbiorów danych lepiej używać fetchone() w pętli lub for row in cursor:.f"{srednia:.2f}". Lub funkcji round(srednia, 2). Przykład: srednia = 4.666667 → f"{srednia:.2f}" → "4.67".list_widget.addItem("tekst") — dodaje na koniec. Do wyczyszczenia: list_widget.clear(). QListWidget to odpowiednik Listboxa — wyświetla listę elementów tekstowych.#02 — Instrukcja z arkusza egzaminacyjnego
INF.04-06-24.06-SG · Czerwiec 2024 · Czas: 180 min · Część I: Aplikacja desktopowa
Kontekst
Z zastosowaniem języka Python wykonaj aplikację desktopową realizującą funkcję dziennika ocen ucznia.
Archiwum materiałów: pliki2.zip, hasło: Dzi3nn!k
Dane wejściowe — dane_oceny.txt
Plik zawiera dane ocen oddzielone średnikiem (bez nagłówka):
1;Jan Kowalski;matematyka;5
2;Jan Kowalski;fizyka;4
3;Jan Kowalski;informatyka;6
4;Anna Nowak;matematyka;3
5;Anna Nowak;fizyka;5
6;Anna Nowak;informatyka;4
7;Piotr Wiśniewski;matematyka;4
8;Piotr Wiśniewski;fizyka;3
9;Piotr Wiśniewski;informatyka;5
10;Maria Zielińska;matematyka;2
11;Maria Zielińska;fizyka;4
12;Maria Zielińska;informatyka;3
Format: id;imie_nazwisko;przedmiot;ocena
Baza danych
- Utwórz bazę SQLite: dziennik.db
- Tabela oceny: id (INTEGER PK), uczen (TEXT), przedmiot (TEXT), ocena (INTEGER)
- Wstaw dane z pliku do tabeli
Zapytania SQL
- Wyświetl wszystkie oceny posortowane po uczniu i przedmiocie
- Oblicz średnią ocen per przedmiot
- Znajdź ucznia z najwyższą średnią
- Filtruj oceny po wybranym przedmiocie
GUI — PyQt6
- Tytuł okna: „Dziennik ucznia"
- QListWidget wyświetlający oceny (uczeń — przedmiot — ocena)
- QComboBox z przedmiotami + przycisk „Filtruj"
- QLabel ze średnią ocen wyświetlonych pozycji
- QLabel z najlepszym uczniem
Wymagania techniczne
- Pętle i warunki, znaczące nazwy zmiennych
- Obsługa pliku z
with open() - Parametryzowane zapytania SQL (
?)
Na lekcji: Uczeń powinien rozpoznać schemat: plik → baza → zapytania → GUI — identyczny jak w #01.
#02 — Wprowadzenie dla nauczyciela
1. Otwarcie
„Drugi egzamin. Schemat ten sam: plik → baza → zapytania → PyQt6. Ale teraz mamy oceny i średnie — trochę więcej matmy."
„Po #01 wiesz jak wczytać plik i zrobić bazę. Teraz nacisk na agregację: AVG, GROUP BY, ORDER BY."
2. Diagnoza wiedzy
„Jak obliczysz średnią w SQL?"
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „AVG(ocena)" | „Dokładnie! A z GROUP BY daje średnią per przedmiot." |
| „sum / count?" | „W Pythonie tak. W SQL masz gotowy AVG()." |
| (nie wie) | „AVG() to wbudowana funkcja SQL. Średnia automatycznie." |
3. Architektura
mainwindow.ui (Qt Creator → Main Window):
QLabel „Dziennik ucznia", QLabel „Przedmiot:",
QComboBox, QPushButton „Filtruj", QListWidget,
QLabel średnia, QLabel najlepszy
Terminal: pyuic6 mainwindow.ui -o mainwindow.py
main.py (VS Code):
class MainWindow(QtWidgets.QMainWindow):
1. Wczytaj dane_oceny.txt → lista dict-ów
2. Utwórz dziennik.db + tabela oceny
3. INSERT z pętli
4. from mainwindow import Ui_MainWindow
5. self.ui.comboBox — przedmioty z bazy
6. Metoda zaladuj() → self.ui.listWidget
7. self.ui.btnFiltruj.clicked.connect(self.filtruj)
4. Zasady prowadzenia
5. Częste blokady
| Problem | Podpowiedź |
|---|---|
| Ocena to string po split | „int(parts[3]) — zawsze rzutuj po split." |
| AVG zwraca None | „Brak danych? Sprawdź czy INSERT przeszedł (commit!)." |
| Nie wie jak sformatować średnią | „f'{avg:.2f}' — dwa miejsca po przecinku." |
| GROUP BY bez AVG | „GROUP BY grupuje, AVG() liczy średnią per grupę." |
6. Po zbudowaniu
„Czym różni się ROUND(AVG(ocena),2) od formatowania w Pythonie f-stringiem?"
Odp: SQL zaokrągla w bazie — dostaniesz float. f-string formatuje tylko wyświetlanie.
#02 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Wczytanie dane_oceny.txt | with open(), split(";"), int(ocena) | ✅ |
| Baza dziennik.db | sqlite3.connect() | ✅ |
| Tabela oceny | CREATE TABLE IF NOT EXISTS | ✅ |
| INSERT danych | Parametryzowane z pętli | ✅ |
| SELECT wszystkie | ORDER BY uczen, przedmiot | ✅ |
| AVG per przedmiot | SELECT przedmiot, AVG(ocena) GROUP BY | ✅ |
| Najlepszy uczeń | AVG + ORDER BY DESC LIMIT 1 | ✅ |
| Filtr po przedmiocie | WHERE przedmiot = ? | ✅ |
| PyQt6 okno | QMainWindow + setWindowTitle(„Dziennik ucznia") | ✅ |
| QListWidget | Wyświetla oceny | ✅ |
| QComboBox przedmiotów | QComboBox + addItems() z DISTINCT | ✅ |
| QLabel średnia | Średnia wyświetlonych ocen | ✅ |
| QLabel najlepszy | Uczeń z najwyższą średnią | ✅ |
#02 — Budowa krok po kroku
„Identyczny start jak w #01. Formularz w Qt Creator, potem pyuic6."
1. File → New → Qt Designer Form → Main Window → Create
2. Przeciągnij widżety:
•
QLabel — „Dziennik ucznia" (objectName: lblTytul)•
QLabel — „Przedmiot:" (objectName: lblPrzedmiot)•
QComboBox (objectName: comboBox)•
QPushButton — „Filtruj" (objectName: btnFiltruj)•
QListWidget (objectName: listWidget)•
QLabel — „Średnia: —" (objectName: lblSrednia)•
QLabel — „Najlepszy: —" (objectName: lblNajlepszy)3. Zaznacz formularz → prawy klik → Lay out in a Grid
4. Zapisz jako
mainwindow.uipyuic6 mainwindow.ui -o mainwindow.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
„Pamiętasz ten schemat z #01? Uruchom — widzisz okno z widżetami."
„Stwórz dane_oceny.txt z 12 wierszami z instrukcji. Potem wczytaj."
dane = []
with open("dane_oceny.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
dane.append({
"id": int(parts[0]),
"uczen": parts[1],
"przedmiot": parts[2],
"ocena": int(parts[3])
})
print(f"Wczytano {len(dane)} ocen")
„Co się zmieni jeśli ocena to 3.5? Jakiego typu użyjesz?"
Odp: float() zamiast int(). Ale w tym zadaniu oceny są całkowite.
„Teraz baza, tabela, wstawienie — 3 kroki w jednym. Znasz to z #01."
conn = sqlite3.connect("dziennik.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS oceny (
id INTEGER PRIMARY KEY,
uczen TEXT NOT NULL,
przedmiot TEXT NOT NULL,
ocena INTEGER
)
""")
cursor.execute("DELETE FROM oceny")
for d in dane:
cursor.execute(
"INSERT INTO oceny VALUES (?, ?, ?, ?)",
(d["id"], d["uczen"], d["przedmiot"], d["ocena"])
)
conn.commit()
print("Dane w bazie")
„Dlaczego DELETE przed INSERT?"
Odp: Żeby przy ponownym uruchomieniu nie dublować danych. IF NOT EXISTS chroni tabelę, nie dane.
# Średnia per przedmiot
cursor.execute("""
SELECT przedmiot, ROUND(AVG(ocena), 2) as srednia
FROM oceny
GROUP BY przedmiot
""")
for row in cursor.fetchall():
print(f"{row[0]}: {row[1]}")
# Najlepszy uczeń (najwyższa średnia)
cursor.execute("""
SELECT uczen, ROUND(AVG(ocena), 2) as srednia
FROM oceny
GROUP BY uczen
ORDER BY srednia DESC
LIMIT 1
""")
najlepszy = cursor.fetchone()
print(f"Najlepszy: {najlepszy[0]} ({najlepszy[1]})")
„Co robi LIMIT 1?"
Odp: Zwraca tylko pierwszy wynik. ORDER BY DESC + LIMIT 1 = rekord z najwyższą wartością.
„Widżety mamy z Qt Creator. Teraz uzupełniamy comboBox przedmiotami z bazy."
# Pobierz przedmioty z bazy i dodaj do comboBox
self.cursor.execute("SELECT DISTINCT przedmiot FROM oceny ORDER BY przedmiot")
przedmioty = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(przedmioty)
self.ui.comboBox.setCurrentText("wszystkie")
Dostęp do widżetów: self.ui.comboBox, self.ui.listWidget, self.ui.lblSrednia, self.ui.lblNajlepszy — nazwy z objectName w Qt Creator.
def zaladuj(self, przedmiot="wszystkie"):
self.ui.listWidget.clear()
if przedmiot == "wszystkie":
self.cursor.execute("SELECT * FROM oceny ORDER BY uczen, przedmiot")
else:
self.cursor.execute(
"SELECT * FROM oceny WHERE przedmiot = ? ORDER BY uczen",
(przedmiot,)
)
wyniki = self.cursor.fetchall()
suma = 0
for row in wyniki:
linia = f"{row[1]:<22} {row[2]:<15} {row[3]}"
self.ui.listWidget.addItem(linia)
suma += row[3]
srednia = suma / len(wyniki) if wyniki else 0
self.ui.lblSrednia.setText(f"Średnia: {srednia:.2f}")
# Najlepszy uczeń (ze wszystkich danych)
self.cursor.execute("""
SELECT uczen, ROUND(AVG(ocena), 2)
FROM oceny GROUP BY uczen ORDER BY AVG(ocena) DESC LIMIT 1
""")
naj = self.cursor.fetchone()
if naj:
self.ui.lblNajlepszy.setText(f"🏆 Najlepszy: {naj[0]} (śr. {naj[1]})")
self.zaladuj()
„Co się stanie w linii ze średnią gdy lista jest pusta?"
Odp: Dzielenie przez zero! Dlatego: if wyniki else 0.
def filtruj(self):
wybrana = self.ui.comboBox.currentText()
self.zaladuj(wybrana)
self.ui.btnFiltruj.clicked.connect(self.filtruj)
Bonus: Dodaj przycisk „Statystyki" wyświetlający średnią per przedmiot w QMessageBox.
#02 — Gotowy kod
Formularz Qt Creator (mainwindow.ui)
| Widżet | objectName | Właściwości |
|---|---|---|
| QLabel | lblTytul | text: „Dziennik ucznia", font: bold 18pt |
| QLabel | lblPrzedmiot | text: „Przedmiot:" |
| QComboBox | comboBox | (elementy z kodu) |
| QPushButton | btnFiltruj | text: „Filtruj" |
| QListWidget | listWidget | font: Consolas 10pt |
| QLabel | lblSrednia | text: „Średnia: —", font: 12pt |
| QLabel | lblNajlepszy | text: „Najlepszy: —", font: bold 12pt |
Konwersja: pyuic6 mainwindow.ui -o mainwindow.py
main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# ===== 1. WCZYTANIE DANYCH =====
dane = []
with open("dane_oceny.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
dane.append({
"id": int(parts[0]),
"uczen": parts[1],
"przedmiot": parts[2],
"ocena": int(parts[3])
})
# ===== 2. BAZA DANYCH =====
self.conn = sqlite3.connect("dziennik.db")
self.cursor = self.conn.cursor()
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS oceny (
id INTEGER PRIMARY KEY,
uczen TEXT NOT NULL,
przedmiot TEXT NOT NULL,
ocena INTEGER
)
""")
self.cursor.execute("DELETE FROM oceny")
for d in dane:
self.cursor.execute(
"INSERT INTO oceny VALUES (?, ?, ?, ?)",
(d["id"], d["uczen"], d["przedmiot"], d["ocena"])
)
self.conn.commit()
self.setup_logic()
def setup_logic(self):
# ===== 3. WIDŻETY =====
self.cursor.execute("SELECT DISTINCT przedmiot FROM oceny ORDER BY przedmiot")
przedmioty = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(przedmioty)
self.ui.comboBox.setCurrentText("wszystkie")
# ===== 4. SYGNAŁY =====
self.ui.btnFiltruj.clicked.connect(self.filtruj)
self.zaladuj()
# ===== 5. METODY =====
def zaladuj(self, przedmiot="wszystkie"):
self.ui.listWidget.clear()
if przedmiot == "wszystkie":
self.cursor.execute("SELECT * FROM oceny ORDER BY uczen, przedmiot")
else:
self.cursor.execute(
"SELECT * FROM oceny WHERE przedmiot = ? ORDER BY uczen",
(przedmiot,)
)
wyniki = self.cursor.fetchall()
suma = 0
for row in wyniki:
linia = f"{row[1]:<22} {row[2]:<15} {row[3]}"
self.ui.listWidget.addItem(linia)
suma += row[3]
srednia = suma / len(wyniki) if wyniki else 0
self.ui.lblSrednia.setText(f"Średnia: {srednia:.2f}")
self.cursor.execute("""
SELECT uczen, ROUND(AVG(ocena), 2)
FROM oceny GROUP BY uczen ORDER BY AVG(ocena) DESC LIMIT 1
""")
naj = self.cursor.fetchone()
if naj:
self.ui.lblNajlepszy.setText(f"🏆 Najlepszy: {naj[0]} (śr. {naj[1]})")
def filtruj(self):
wybrana = self.ui.comboBox.currentText()
self.zaladuj(wybrana)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
dane_oceny.txt
1;Jan Kowalski;matematyka;5
2;Jan Kowalski;fizyka;4
3;Jan Kowalski;informatyka;6
4;Anna Nowak;matematyka;3
5;Anna Nowak;fizyka;5
6;Anna Nowak;informatyka;4
7;Piotr Wiśniewski;matematyka;4
8;Piotr Wiśniewski;fizyka;3
9;Piotr Wiśniewski;informatyka;5
10;Maria Zielińska;matematyka;2
11;Maria Zielińska;fizyka;4
12;Maria Zielińska;informatyka;3
📝 Sprawdzian — Dziennik ucznia
5 pytań · 5 minut · minimum 60% żeby zdać
oceny = [4, 5, 3]?cursor.fetchone() gdy nie ma więcej wierszy?3.14159 do 2 miejsc po przecinku?🔥 Rozgrzewka — Wypożyczalnia książek
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
cursor.execute("UPDATE tabela SET kolumna = ? WHERE id = ?", (nowa_wartosc, id)). Pamiętaj o WHERE — bez niego zmienisz WSZYSTKIE wiersze! Po UPDATE wywołaj conn.commit().item = list_widget.currentItem() zwraca zaznaczony element (lub None). Tekst: item.text(). Indeks: list_widget.currentRow() (zwraca -1 jeśli nic nie zaznaczono).dostepna INTEGER DEFAULT 1. Przy wypożyczeniu: UPDATE ksiazki SET dostepna = 0 WHERE id = ?.SELECT * FROM ksiazki WHERE dostepna = 1. Lub odwrotnie dla wypożyczonych: WHERE dostepna = 0. Można też użyć aliasu: SELECT *, CASE WHEN dostepna=0 THEN 'Tak' ELSE 'Nie' END as wypozyczona.try/except przy operacjach na bazie?try: ... except sqlite3.Error as e: print(f"Błąd: {e}"). Zapobiega crashowi programu.#03 — Instrukcja z arkusza egzaminacyjnego
INF.04-02-24.01-SG · Styczeń 2024 · Czas: 180 min · Część I: Aplikacja desktopowa
Kontekst
Z zastosowaniem języka Python wykonaj aplikację desktopową realizującą funkcję systemu wypożyczalni książek.
Archiwum materiałów: pliki3.zip, hasło: Ksi@zki1
Dane wejściowe — dane_ksiazki.txt
Plik zawiera dane książek oddzielone średnikiem (bez nagłówka):
1;Wiedźmin;Andrzej Sapkowski;fantasy;1993;1
2;Lalka;Bolesław Prus;powieść;1890;1
3;Pan Tadeusz;Adam Mickiewicz;epopeja;1834;0
4;Solaris;Stanisław Lem;sci-fi;1961;1
5;Ferdydurke;Witold Gombrowicz;powieść;1937;1
6;Hobbit;J.R.R. Tolkien;fantasy;1937;0
7;Diune;Frank Herbert;sci-fi;1965;1
8;Quo Vadis;Henryk Sienkiewicz;powieść;1896;1
9;Cyberiada;Stanisław Lem;sci-fi;1965;1
10;Krzyżacy;Henryk Sienkiewicz;powieść;1900;0
Format: id;tytul;autor;gatunek;rok;dostepna (1 = dostępna, 0 = wypożyczona)
Baza danych
- Utwórz bazę SQLite: biblioteka.db
- Tabela ksiazki: id (INTEGER PK), tytul (TEXT), autor (TEXT), gatunek (TEXT), rok (INTEGER), dostepna (INTEGER)
- Wstaw dane z pliku do tabeli
Zapytania SQL
- Wyświetl książki posortowane po tytule
- Filtruj po gatunku
- Pokaż tylko dostępne (dostepna = 1)
- Policz ilość książek per gatunek (COUNT + GROUP BY)
- UPDATE — zmień status wypożyczenia
GUI — PyQt6
- Tytuł okna: „Wypożyczalnia książek"
- QListWidget wyświetlający książki (tytuł — autor — rok — status)
- QComboBox z gatunkami + przycisk „Filtruj"
- QCheckBox „Tylko dostępne"
- Przycisk „Wypożycz / Zwróć" — zmienia status zaznaczonej książki
- QLabel ze statystykami (dostępne / wszystkie)
Na lekcji: Nowością jest UPDATE i QCheckBox. Reszta to znany schemat.
#03 — Wprowadzenie dla nauczyciela
1. Otwarcie
„Trzecie zadanie. Schemat znasz — plik → baza → GUI. Ale teraz mamy nowy element: UPDATE — zmianę danych w bazie."
„W #01 i #02 baza była read-only. Teraz użytkownik klika i zmienia status książki."
2. Diagnoza wiedzy
„Jak zmienisz wartość w bazie SQL?"
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „UPDATE tabela SET..." | „Dokładnie! UPDATE + WHERE żeby nie zmienić wszystkiego." |
| „INSERT?" | „INSERT dodaje nowy wiersz. UPDATE zmienia istniejący." |
| (nie wie) | „UPDATE ksiazki SET dostepna = 0 WHERE id = 3" |
3. Architektura
mainwindow.ui (Qt Creator → Main Window):
QLabel „Wypożyczalnia książek", QLabel „Gatunek:",
QComboBox, QPushButton „Filtruj", QCheckBox „Tylko dostępne",
QListWidget, QLabel statystyki, QPushButton „Wypożycz / Zwróć"
Terminal: pyuic6 mainwindow.ui -o mainwindow.py
main.py (VS Code):
class MainWindow(QtWidgets.QMainWindow):
1. Wczytaj dane_ksiazki.txt
2. Utwórz biblioteka.db + tabela ksiazki
3. INSERT dane
4. from mainwindow import Ui_MainWindow
5. self.ui.comboBox — gatunki z bazy
6. Metoda zaladuj() → self.ui.listWidget
7. self.ui.btnWypozycz → UPDATE dostepna → odśwież
8. self.ui.lblStat: „Dostępne: 7/10"
4. Częste blokady
| Problem | Podpowiedź |
|---|---|
| QListWidget — currentRow() == -1 | „Nic nie zaznaczono. Sprawdź: if list_widget.currentRow() == -1: return" |
| UPDATE bez WHERE | „Zmieni WSZYSTKIE wiersze! Zawsze dodaj WHERE id = ?" |
| Po UPDATE brak zmiany w QListWidget | „Musisz commit() + przeładować zaladuj()." |
| Status 1/0 niezrozumiały | „Mapuj: 'dostępna' if row[5]==1 else 'wypożyczona'" |
#03 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Wczytanie dane_ksiazki.txt | with open(), split(";"), int(rok), int(dostepna) | ✅ |
| Baza biblioteka.db | sqlite3.connect() | ✅ |
| Tabela ksiazki | CREATE TABLE IF NOT EXISTS, 6 kolumn | ✅ |
| INSERT danych | Parametryzowane, z pętli | ✅ |
| SELECT + sortowanie | ORDER BY tytul | ✅ |
| Filtr po gatunku | WHERE gatunek = ? | ✅ |
| Filtr „tylko dostępne" | WHERE dostepna = 1 | ✅ |
| COUNT per gatunek | SELECT gatunek, COUNT(*) GROUP BY | ✅ |
| UPDATE statusu | UPDATE ksiazki SET dostepna = ? WHERE id = ? | ✅ |
| PyQt6 okno | QMainWindow + setWindowTitle(„Wypożyczalnia książek") | ✅ |
| QListWidget | Tytuł, autor, rok, status | ✅ |
| QComboBox gatunek | DISTINCT + „wszystkie" | ✅ |
| QCheckBox dostępne | QCheckBox + isChecked() | ✅ |
| Przycisk Wypożycz/Zwróć | UPDATE + odśwież | ✅ |
| QLabel statystyki | Dostępne X / Y | ✅ |
#03 — Budowa krok po kroku
1. File → New → Qt Designer Form → Main Window → Create
2. Przeciągnij widżety:
•
QLabel — „Wypożyczalnia książek" (objectName: lblTytul)•
QLabel — „Gatunek:" (objectName: lblGatunek)•
QComboBox (objectName: comboBox)•
QPushButton — „Filtruj" (objectName: btnFiltruj)•
QCheckBox — „Tylko dostępne" (objectName: chkDostepne)•
QListWidget (objectName: listWidget)•
QLabel — „Dostępne: 0/0" (objectName: lblStat)•
QPushButton — „Wypożycz / Zwróć" (objectName: btnWypozycz)3. Zaznacz formularz → prawy klik → Lay out in a Grid
4. Zapisz jako
mainwindow.uipyuic6 mainwindow.ui -o mainwindow.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
„Klasa MainWindow dziedziczy po QMainWindow — to standardowy wzorzec PyQt6."
ksiazki = []
with open("dane_ksiazki.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
ksiazki.append({
"id": int(parts[0]),
"tytul": parts[1],
"autor": parts[2],
"gatunek": parts[3],
"rok": int(parts[4]),
"dostepna": int(parts[5])
})
print(f"Wczytano {len(ksiazki)} książek")
„Ile pól ma tutaj split? Ile w #01?"
Odp: 6 pól (id, tytuł, autor, gatunek, rok, dostępna). W #01 było 5.
conn = sqlite3.connect("biblioteka.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS ksiazki (
id INTEGER PRIMARY KEY,
tytul TEXT NOT NULL,
autor TEXT,
gatunek TEXT,
rok INTEGER,
dostepna INTEGER DEFAULT 1
)
""")
cursor.execute("DELETE FROM ksiazki")
for k in ksiazki:
cursor.execute(
"INSERT INTO ksiazki VALUES (?, ?, ?, ?, ?, ?)",
(k["id"], k["tytul"], k["autor"], k["gatunek"], k["rok"], k["dostepna"])
)
conn.commit()
„Co robi DEFAULT 1?"
Odp: Jeśli nie podasz wartości przy INSERT, domyślnie = 1 (dostępna).
„Widżety mamy z Qt Creator — łącznie z QCheckBox. Uzupełniamy comboBox gatunkami z bazy."
# Pobierz gatunki z bazy i dodaj do comboBox
self.cursor.execute("SELECT DISTINCT gatunek FROM ksiazki ORDER BY gatunek")
gatunki = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(gatunki)
self.ui.comboBox.setCurrentText("wszystkie")
„Co robi QCheckBox?"
Odp: To pole wyboru True/False. Sprawdzamy stan metodą self.ui.chkDostepne.isChecked().
def zaladuj(self, gatunek="wszystkie"):
self.ui.listWidget.clear()
self.widoczne_id = []
sql = "SELECT * FROM ksiazki WHERE 1=1"
params = []
if gatunek != "wszystkie":
sql += " AND gatunek = ?"
params.append(gatunek)
if self.ui.chkDostepne.isChecked():
sql += " AND dostepna = 1"
sql += " ORDER BY tytul"
self.cursor.execute(sql, params)
wyniki = self.cursor.fetchall()
for row in wyniki:
status = "✅ dostępna" if row[5] == 1 else "❌ wypożyczona"
linia = f"{row[1]:<22} {row[2]:<22} {row[4]} {status}"
self.ui.listWidget.addItem(linia)
self.widoczne_id.append(row[0])
# Statystyki
self.cursor.execute("SELECT COUNT(*) FROM ksiazki WHERE dostepna = 1")
dost = self.cursor.fetchone()[0]
self.cursor.execute("SELECT COUNT(*) FROM ksiazki")
total = self.cursor.fetchone()[0]
self.ui.lblStat.setText(f"Dostępne: {dost}/{total}")
self.zaladuj()
„self.widoczne_id to lista – mapujemy indeks QListWidget do id w bazie. Bez tego nie wiemy, którą książkę kliknął."
„To najważniejszy nowy element. Klikasz książkę → przycisk zmienia jej status."
def wypozycz_zwroc(self):
row_idx = self.ui.listWidget.currentRow()
if row_idx == -1:
QtWidgets.QMessageBox.warning(self, "Uwaga", "Zaznacz książkę!")
return
book_id = self.widoczne_id[row_idx]
# Sprawdź aktualny status
self.cursor.execute("SELECT dostepna FROM ksiazki WHERE id = ?", (book_id,))
obecny = self.cursor.fetchone()[0]
nowy = 0 if obecny == 1 else 1
self.cursor.execute("UPDATE ksiazki SET dostepna = ? WHERE id = ?", (nowy, book_id))
self.conn.commit()
akcja = "Wypożyczono" if nowy == 0 else "Zwrócono"
QtWidgets.QMessageBox.information(self, "OK", f"{akcja} książkę")
self.zaladuj(self.ui.comboBox.currentText())
self.ui.btnWypozycz.clicked.connect(self.wypozycz_zwroc)
„Dlaczego po UPDATE wywołujemy zaladuj()?"
Odp: Żeby QListWidget pokazał nowy status. Baza się zmieniła, ale ekran sam się nie odświeży.
def filtruj(self):
self.zaladuj(self.ui.comboBox.currentText())
self.ui.btnFiltruj.clicked.connect(self.filtruj)
# QCheckBox też filtruje przy kliknięciu
self.ui.chkDostepne.stateChanged.connect(self.filtruj)
Bonus: Dodaj kolorowanie — zielone tło dla dostępnych, czerwone dla wypożyczonych:item = self.ui.listWidget.item(i); item.setBackground(QtGui.QColor("#e8f5e9") if row[5]==1 else QtGui.QColor("#ffebee"))
#03 — Gotowy kod
Formularz Qt Creator (mainwindow.ui)
| Widżet | objectName | Właściwości |
|---|---|---|
| QLabel | lblTytul | text: „Wypożyczalnia książek", font: bold 18pt |
| QLabel | lblGatunek | text: „Gatunek:" |
| QComboBox | comboBox | (elementy z kodu) |
| QPushButton | btnFiltruj | text: „Filtruj" |
| QCheckBox | chkDostepne | text: „Tylko dostępne" |
| QListWidget | listWidget | font: Consolas 10pt |
| QLabel | lblStat | text: „Dostępne: 0/0", font: 12pt |
| QPushButton | btnWypozycz | text: „Wypożycz / Zwróć" |
Konwersja: pyuic6 mainwindow.ui -o mainwindow.py
main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# ===== 1. WCZYTANIE DANYCH =====
ksiazki = []
with open("dane_ksiazki.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
ksiazki.append({
"id": int(parts[0]),
"tytul": parts[1],
"autor": parts[2],
"gatunek": parts[3],
"rok": int(parts[4]),
"dostepna": int(parts[5])
})
# ===== 2. BAZA DANYCH =====
self.conn = sqlite3.connect("biblioteka.db")
self.cursor = self.conn.cursor()
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS ksiazki (
id INTEGER PRIMARY KEY,
tytul TEXT NOT NULL,
autor TEXT,
gatunek TEXT,
rok INTEGER,
dostepna INTEGER DEFAULT 1
)
""")
self.cursor.execute("DELETE FROM ksiazki")
for k in ksiazki:
self.cursor.execute(
"INSERT INTO ksiazki VALUES (?, ?, ?, ?, ?, ?)",
(k["id"], k["tytul"], k["autor"], k["gatunek"], k["rok"], k["dostepna"])
)
self.conn.commit()
self.setup_logic()
def setup_logic(self):
# ===== 3. WIDŻETY =====
self.cursor.execute("SELECT DISTINCT gatunek FROM ksiazki ORDER BY gatunek")
gatunki = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(gatunki)
self.ui.comboBox.setCurrentText("wszystkie")
# ===== 4. SYGNAŁY =====
self.ui.btnFiltruj.clicked.connect(self.filtruj)
self.ui.chkDostepne.stateChanged.connect(self.filtruj)
self.ui.btnWypozycz.clicked.connect(self.wypozycz_zwroc)
self.zaladuj()
# ===== 5. METODY =====
def zaladuj(self, gatunek="wszystkie"):
self.ui.listWidget.clear()
self.widoczne_id = []
sql = "SELECT * FROM ksiazki WHERE 1=1"
params = []
if gatunek != "wszystkie":
sql += " AND gatunek = ?"
params.append(gatunek)
if self.ui.chkDostepne.isChecked():
sql += " AND dostepna = 1"
sql += " ORDER BY tytul"
self.cursor.execute(sql, params)
wyniki = self.cursor.fetchall()
for row in wyniki:
status = "✅ dostępna" if row[5] == 1 else "❌ wypożyczona"
linia = f"{row[1]:<22} {row[2]:<22} {row[4]} {status}"
self.ui.listWidget.addItem(linia)
self.widoczne_id.append(row[0])
self.cursor.execute("SELECT COUNT(*) FROM ksiazki WHERE dostepna = 1")
dost = self.cursor.fetchone()[0]
self.cursor.execute("SELECT COUNT(*) FROM ksiazki")
total = self.cursor.fetchone()[0]
self.ui.lblStat.setText(f"Dostępne: {dost}/{total}")
def wypozycz_zwroc(self):
row_idx = self.ui.listWidget.currentRow()
if row_idx == -1:
QtWidgets.QMessageBox.warning(self, "Uwaga", "Zaznacz książkę!")
return
book_id = self.widoczne_id[row_idx]
self.cursor.execute("SELECT dostepna FROM ksiazki WHERE id = ?", (book_id,))
obecny = self.cursor.fetchone()[0]
nowy = 0 if obecny == 1 else 1
self.cursor.execute("UPDATE ksiazki SET dostepna = ? WHERE id = ?", (nowy, book_id))
self.conn.commit()
akcja = "Wypożyczono" if nowy == 0 else "Zwrócono"
QtWidgets.QMessageBox.information(self, "OK", f"{akcja} książkę")
self.zaladuj(self.ui.comboBox.currentText())
def filtruj(self):
self.zaladuj(self.ui.comboBox.currentText())
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
dane_ksiazki.txt
1;Wiedźmin;Andrzej Sapkowski;fantasy;1993;1
2;Lalka;Bolesław Prus;powieść;1890;1
3;Pan Tadeusz;Adam Mickiewicz;epopeja;1834;0
4;Solaris;Stanisław Lem;sci-fi;1961;1
5;Ferdydurke;Witold Gombrowicz;powieść;1937;1
6;Hobbit;J.R.R. Tolkien;fantasy;1937;0
7;Diune;Frank Herbert;sci-fi;1965;1
8;Quo Vadis;Henryk Sienkiewicz;powieść;1896;1
9;Cyberiada;Stanisław Lem;sci-fi;1965;1
10;Krzyżacy;Henryk Sienkiewicz;powieść;1900;0
📝 Sprawdzian — Wypożyczalnia książek
5 pytań · 5 minut · minimum 60% żeby zdać
try/except przy operacjach bazodanowych?🔥 Rozgrzewka — Stacja pogodowa
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
SELECT AVG(temperatura), MIN(temperatura), MAX(temperatura) FROM pomiary. Funkcje agregujące: AVG() — średnia, MIN() — najmniejsza, MAX() — największa, SUM() — suma, COUNT() — liczba wierszy.SELECT miasto, AVG(temperatura) FROM pomiary GROUP BY miasto. GROUP BY tworzy grupy po unikalnych wartościach kolumny, a funkcja agregująca działa osobno dla każdej grupy.YYYY-MM-DD. Porównania tekstowe działają poprawnie: WHERE data > '2025-01-10'. W Pythonie: datetime.strptime(tekst, "%Y-%m-%d").SELECT * FROM pomiary WHERE data BETWEEN '2025-01-10' AND '2025-01-15'. Lub: WHERE data >= '2025-01-10' AND data <= '2025-01-15'. Format daty musi być YYYY-MM-DD.HAVING w SQL?HAVING filtruje wyniki PO grupowaniu (w przeciwieństwie do WHERE, które filtruje PRZED). Przykład: SELECT miasto, AVG(temp) FROM pomiary GROUP BY miasto HAVING AVG(temp) > 5 — tylko miasta ze średnią > 5°C.#04 — Instrukcja z arkusza egzaminacyjnego
INF.04-03-23.06-SG · Czerwiec 2023 · Czas: 180 min · Część I: Aplikacja desktopowa
Kontekst
Z zastosowaniem języka Python wykonaj aplikację desktopową realizującą funkcję stacji pogodowej — przeglądanie i analizę danych pomiarowych.
Archiwum materiałów: pliki4.zip, hasło: P0g0da#
Dane wejściowe — dane_pogoda.txt
Plik zawiera pomiary pogodowe oddzielone średnikiem (bez nagłówka):
1;2025-01-10;Kraków;-5.2;78;1013
2;2025-01-10;Warszawa;-3.1;82;1015
3;2025-01-10;Gdańsk;-1.5;88;1010
4;2025-01-11;Kraków;-2.8;72;1018
5;2025-01-11;Warszawa;-0.5;76;1016
6;2025-01-11;Gdańsk;1.2;85;1012
7;2025-01-12;Kraków;0.3;65;1020
8;2025-01-12;Warszawa;2.1;70;1019
9;2025-01-12;Gdańsk;3.5;80;1014
10;2025-01-13;Kraków;3.7;60;1022
11;2025-01-13;Warszawa;5.0;68;1021
12;2025-01-13;Gdańsk;4.8;75;1017
Format: id;data;miasto;temperatura;wilgotnosc;cisnienie
Baza danych
- Utwórz bazę SQLite: pogoda.db
- Tabela pomiary: id (INTEGER PK), data (TEXT), miasto (TEXT), temperatura (REAL), wilgotnosc (INTEGER), cisnienie (INTEGER)
- Wstaw dane z pliku do tabeli
Zapytania SQL
- Wyświetl pomiary posortowane po dacie i mieście
- Filtruj po mieście
- Oblicz MIN, MAX, AVG temperatury per miasto
- Znajdź dzień z najniższą temperaturą
- Oblicz średnią wilgotność per miasto
GUI — PyQt6
- Tytuł okna: „Stacja pogodowa"
- QListWidget wyświetlający pomiary (data — miasto — temp — wilgotność — ciśnienie)
- QComboBox z miastami + przycisk „Filtruj"
- QLabel z min/max/avg temperatury
- QLabel z dniem najzimniejszym
- Przycisk „Statystyki" → QMessageBox ze średnimi per miasto
Na lekcji: Więcej typów danych (float, daty) i agregacji (MIN/MAX/AVG). Schemat ten sam, ale dane bardziej realistyczne.
#04 — Wprowadzenie dla nauczyciela
1. Otwarcie
„Czwarte zadanie — stacja pogodowa. Temat realny: temperatury, wilgotność, ciśnienie. Dane z kilku miast i kilku dni."
„Schemat identyczny: plik → baza → zapytania → GUI. Ale masz teraz REAL (float) i daty jako TEXT."
2. Diagnoza wiedzy
„Jak w SQL znajdziesz najniższą temperaturę?"
| Co mówi uczeń | Twoja reakcja |
|---|---|
| „MIN(temperatura)" | „Idealnie! A ORDER BY + LIMIT 1 da ci cały wiersz." |
| „sort i wziąć pierwszy" | „Można. Ale SQL ma gotowy MIN(). Czyściej i szybciej." |
| (nie wie) | „SQL: MIN() = minimum, MAX() = maksimum, AVG() = średnia." |
3. Architektura
mainwindow.ui (Qt Creator → Main Window):
QLabel „Stacja pogodowa", QLabel „Miasto:",
QComboBox, QPushButton „Filtruj", QPushButton „Statystyki",
QListWidget, QLabel temperatury, QLabel najzimniejszy
Terminal: pyuic6 mainwindow.ui -o mainwindow.py
main.py (VS Code):
class MainWindow(QtWidgets.QMainWindow):
1. Wczytaj dane_pogoda.txt
2. Utwórz pogoda.db + tabela pomiary
3. INSERT dane
4. from mainwindow import Ui_MainWindow
5. self.ui.comboBox — miasta z bazy
6. Metoda zaladuj() → self.ui.listWidget + self.ui.lblTemp
7. self.ui.btnStatystyki → QMessageBox ze średnimi
8. self.ui.lblZimno: najzimniejszy dzień
4. Częste blokady
| Problem | Podpowiedź |
|---|---|
| Temperatura ujemna się nie parsuje | „float('-5.2') działa! Python radzi sobie z minusem." |
| Data jako tekst, nie datetime | „W SQLite daty to TEXT. Sortuj normalnie — format YYYY-MM-DD sortuje się poprawnie." |
| MIN() vs ORDER BY LIMIT 1 | „MIN() daje samą wartość. Jeśli chcesz cały wiersz → ORDER BY temp ASC LIMIT 1." |
| Messagebox za długi tekst | „Buduj stringa z pętli: tekst += f'{row[0]}: {row[1]:.1f}°C\n'" |
#04 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Wczytanie dane_pogoda.txt | with open(), split(";"), float(temp), int(wilg) | ✅ |
| Baza pogoda.db | sqlite3.connect() | ✅ |
| Tabela pomiary | CREATE TABLE IF NOT EXISTS, 6 kolumn | ✅ |
| INSERT danych | Parametryzowane, z pętli | ✅ |
| SELECT + sortowanie | ORDER BY data, miasto | ✅ |
| Filtr po mieście | WHERE miasto = ? | ✅ |
| MIN/MAX/AVG temp | Agregacja per wyświetlone dane | ✅ |
| Najzimniejszy dzień | ORDER BY temperatura ASC LIMIT 1 | ✅ |
| Średnia wilgotność | AVG(wilgotnosc) GROUP BY miasto | ✅ |
| PyQt6 okno | QMainWindow + setWindowTitle(„Stacja pogodowa") | ✅ |
| QListWidget pomiarów | Data, miasto, temp, wilg, ciśn | ✅ |
| QComboBox miast | DISTINCT + „wszystkie" | ✅ |
| QLabel min/max/avg | Temperatury | ✅ |
| QLabel najzimniejszy | Data + miasto + temperatura | ✅ |
| Przycisk Statystyki | QMessageBox ze średnimi per miasto | ✅ |
#04 — Budowa krok po kroku
1. File → New → Qt Designer Form → Main Window → Create
2. Przeciągnij widżety:
•
QLabel — „🌤️ Stacja pogodowa" (objectName: lblTytul)•
QLabel — „Miasto:" (objectName: lblMiasto)•
QComboBox (objectName: comboBox)•
QPushButton — „Filtruj" (objectName: btnFiltruj)•
QPushButton — „📊 Statystyki" (objectName: btnStatystyki)•
QListWidget (objectName: listWidget)•
QLabel — „Min: — | Max: — | Avg: —" (objectName: lblTemp)•
QLabel — „Najzimniej: —" (objectName: lblZimno)3. Zaznacz formularz → prawy klik → Lay out in a Grid
4. Zapisz jako
mainwindow.uipyuic6 mainwindow.ui -o mainwindow.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
„Znasz ten schemat. Formularz z Qt Creator, pyuic6, main.py. Jazda."
pomiary = []
with open("dane_pogoda.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
pomiary.append({
"id": int(parts[0]),
"data": parts[1],
"miasto": parts[2],
"temperatura": float(parts[3]),
"wilgotnosc": int(parts[4]),
"cisnienie": int(parts[5])
})
print(f"Wczytano {len(pomiary)} pomiarów")
„Dlaczego temperatura to float a wilgotność to int?"
Odp: Temperatura ma część dziesiętną (-5.2). Wilgotność to procent — całkowita.
conn = sqlite3.connect("pogoda.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS pomiary (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
miasto TEXT NOT NULL,
temperatura REAL,
wilgotnosc INTEGER,
cisnienie INTEGER
)
""")
cursor.execute("DELETE FROM pomiary")
for p in pomiary:
cursor.execute(
"INSERT INTO pomiary VALUES (?, ?, ?, ?, ?, ?)",
(p["id"], p["data"], p["miasto"], p["temperatura"],
p["wilgotnosc"], p["cisnienie"])
)
conn.commit()
print("Dane w bazie")
„Dlaczego data to TEXT a nie DATE?"
Odp: SQLite nie ma typu DATE. Używa TEXT. Format YYYY-MM-DD sortuje się poprawnie alfanumerycznie.
# MIN / MAX / AVG temperatury
cursor.execute("""
SELECT MIN(temperatura), MAX(temperatura), ROUND(AVG(temperatura), 1)
FROM pomiary
""")
row = cursor.fetchone()
print(f"Min: {row[0]}°C, Max: {row[1]}°C, Avg: {row[2]}°C")
# Najzimniejszy dzień
cursor.execute("""
SELECT data, miasto, temperatura FROM pomiary
ORDER BY temperatura ASC LIMIT 1
""")
naj = cursor.fetchone()
print(f"Najzimniej: {naj[0]} {naj[1]} → {naj[2]}°C")
# Średnia per miasto
cursor.execute("""
SELECT miasto, ROUND(AVG(temperatura), 1), ROUND(AVG(wilgotnosc), 0)
FROM pomiary GROUP BY miasto
""")
for row in cursor.fetchall():
print(f"{row[0]}: temp {row[1]}°C, wilg {row[2]}%")
„Czym się różni ROUND(AVG(...),1) od ROUND(AVG(...),2)?"
Odp: Liczba miejsc po przecinku. 1 → jedno (np. 3.5), 2 → dwa (3.50).
„Widżety mamy z Qt Creator. Uzupełniamy comboBox miastami z bazy."
self.cursor.execute("SELECT DISTINCT miasto FROM pomiary ORDER BY miasto")
miasta = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(miasta)
self.ui.comboBox.setCurrentText("wszystkie")
Dostęp do widżetów: self.ui.comboBox, self.ui.listWidget, self.ui.lblTemp, self.ui.lblZimno — nazwy z objectName w Qt Creator.
def zaladuj(self, miasto="wszystkie"):
self.ui.listWidget.clear()
if miasto == "wszystkie":
self.cursor.execute("SELECT * FROM pomiary ORDER BY data, miasto")
else:
self.cursor.execute(
"SELECT * FROM pomiary WHERE miasto = ? ORDER BY data",
(miasto,)
)
wyniki = self.cursor.fetchall()
for row in wyniki:
linia = f"{row[1]} {row[2]:<12} {row[3]:>6.1f}°C 💧{row[4]}% {row[5]} hPa"
self.ui.listWidget.addItem(linia)
# Statystyki temperatur (z wyświetlonych danych)
if wyniki:
temps = [r[3] for r in wyniki]
mn, mx, avg = min(temps), max(temps), sum(temps)/len(temps)
self.ui.lblTemp.setText(f"🌡️ Min: {mn:.1f}°C | Max: {mx:.1f}°C | Avg: {avg:.1f}°C")
else:
self.ui.lblTemp.setText("Brak danych")
# Najzimniejszy (ze wszystkich)
self.cursor.execute("""
SELECT data, miasto, temperatura FROM pomiary
ORDER BY temperatura ASC LIMIT 1
""")
naj = self.cursor.fetchone()
if naj:
self.ui.lblZimno.setText(f"❄️ Najzimniej: {naj[0]} {naj[1]} → {naj[2]}°C")
self.zaladuj()
„Statystyki liczymy w Pythonie z wyfiltrowanych danych. Najzimniejszy dzień — zawsze ze wszystkich."
def filtruj(self):
self.zaladuj(self.ui.comboBox.currentText())
self.ui.btnFiltruj.clicked.connect(self.filtruj)
„Przetestuj — wybierz Kraków i kliknij Filtruj. Labele się aktualizują?"
„Ostatni krok — przycisk otwierający podsumowanie per miasto."
def pokaz_statystyki(self):
self.cursor.execute("""
SELECT miasto,
ROUND(AVG(temperatura), 1),
ROUND(MIN(temperatura), 1),
ROUND(MAX(temperatura), 1),
ROUND(AVG(wilgotnosc), 0)
FROM pomiary GROUP BY miasto ORDER BY miasto
""")
tekst = ""
for row in self.cursor.fetchall():
tekst += f"📍 {row[0]}\n"
tekst += f" Temp: avg {row[1]}°C (min {row[2]}, max {row[3]})\n"
tekst += f" Wilgotność: {int(row[4])}%\n\n"
QtWidgets.QMessageBox.information(self, "Statystyki per miasto", tekst)
self.ui.btnStatystyki.clicked.connect(self.pokaz_statystyki)
Bonus: Dodaj kolorowanie w QListWidget: temperatury ujemne → niebieskie tło, powyżej 0 → zielone.
#04 — Gotowy kod
Formularz Qt Creator (mainwindow.ui)
| Widżet | objectName | Właściwości |
|---|---|---|
| QLabel | lblTytul | text: „🌤️ Stacja pogodowa", font: bold 18pt |
| QLabel | lblMiasto | text: „Miasto:" |
| QComboBox | comboBox | (elementy z kodu) |
| QPushButton | btnFiltruj | text: „Filtruj" |
| QPushButton | btnStatystyki | text: „📊 Statystyki" |
| QListWidget | listWidget | font: Consolas 10pt |
| QLabel | lblTemp | text: „Min: — | Max: — | Avg: —", font: 11pt |
| QLabel | lblZimno | text: „Najzimniej: —", font: bold 11pt |
Konwersja: pyuic6 mainwindow.ui -o mainwindow.py
main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
import sqlite3
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# ===== 1. WCZYTANIE DANYCH =====
pomiary = []
with open("dane_pogoda.txt", "r", encoding="utf-8") as plik:
for linia in plik:
if not linia.strip():
continue
parts = linia.strip().split(";")
pomiary.append({
"id": int(parts[0]),
"data": parts[1],
"miasto": parts[2],
"temperatura": float(parts[3]),
"wilgotnosc": int(parts[4]),
"cisnienie": int(parts[5])
})
# ===== 2. BAZA DANYCH =====
self.conn = sqlite3.connect("pogoda.db")
self.cursor = self.conn.cursor()
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS pomiary (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
miasto TEXT NOT NULL,
temperatura REAL,
wilgotnosc INTEGER,
cisnienie INTEGER
)
""")
self.cursor.execute("DELETE FROM pomiary")
for p in pomiary:
self.cursor.execute(
"INSERT INTO pomiary VALUES (?, ?, ?, ?, ?, ?)",
(p["id"], p["data"], p["miasto"], p["temperatura"],
p["wilgotnosc"], p["cisnienie"])
)
self.conn.commit()
self.setup_logic()
def setup_logic(self):
# ===== 3. WIDŻETY =====
self.cursor.execute("SELECT DISTINCT miasto FROM pomiary ORDER BY miasto")
miasta = ["wszystkie"] + [r[0] for r in self.cursor.fetchall()]
self.ui.comboBox.addItems(miasta)
self.ui.comboBox.setCurrentText("wszystkie")
# ===== 4. SYGNAŁY =====
self.ui.btnFiltruj.clicked.connect(self.filtruj)
self.ui.btnStatystyki.clicked.connect(self.pokaz_statystyki)
self.zaladuj()
# ===== 5. METODY =====
def zaladuj(self, miasto="wszystkie"):
self.ui.listWidget.clear()
if miasto == "wszystkie":
self.cursor.execute("SELECT * FROM pomiary ORDER BY data, miasto")
else:
self.cursor.execute(
"SELECT * FROM pomiary WHERE miasto = ? ORDER BY data",
(miasto,)
)
wyniki = self.cursor.fetchall()
for row in wyniki:
linia = f"{row[1]} {row[2]:<12} {row[3]:>6.1f}°C 💧{row[4]}% {row[5]} hPa"
self.ui.listWidget.addItem(linia)
if wyniki:
temps = [r[3] for r in wyniki]
mn, mx, avg = min(temps), max(temps), sum(temps)/len(temps)
self.ui.lblTemp.setText(f"🌡️ Min: {mn:.1f}°C | Max: {mx:.1f}°C | Avg: {avg:.1f}°C")
else:
self.ui.lblTemp.setText("Brak danych")
self.cursor.execute("""
SELECT data, miasto, temperatura FROM pomiary
ORDER BY temperatura ASC LIMIT 1
""")
naj = self.cursor.fetchone()
if naj:
self.ui.lblZimno.setText(f"❄️ Najzimniej: {naj[0]} {naj[1]} → {naj[2]}°C")
def filtruj(self):
self.zaladuj(self.ui.comboBox.currentText())
def pokaz_statystyki(self):
self.cursor.execute("""
SELECT miasto,
ROUND(AVG(temperatura), 1),
ROUND(MIN(temperatura), 1),
ROUND(MAX(temperatura), 1),
ROUND(AVG(wilgotnosc), 0)
FROM pomiary GROUP BY miasto ORDER BY miasto
""")
tekst = ""
for row in self.cursor.fetchall():
tekst += f"📍 {row[0]}\n"
tekst += f" Temp: avg {row[1]}°C (min {row[2]}, max {row[3]})\n"
tekst += f" Wilgotność: {int(row[4])}%\n\n"
QtWidgets.QMessageBox.information(self, "Statystyki per miasto", tekst)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
dane_pogoda.txt
1;2025-01-10;Kraków;-5.2;78;1013
2;2025-01-10;Warszawa;-3.1;82;1015
3;2025-01-10;Gdańsk;-1.5;88;1010
4;2025-01-11;Kraków;-2.8;72;1018
5;2025-01-11;Warszawa;-0.5;76;1016
6;2025-01-11;Gdańsk;1.2;85;1012
7;2025-01-12;Kraków;0.3;65;1020
8;2025-01-12;Warszawa;2.1;70;1019
9;2025-01-12;Gdańsk;3.5;80;1014
10;2025-01-13;Kraków;3.7;60;1022
11;2025-01-13;Warszawa;5.0;68;1021
12;2025-01-13;Gdańsk;4.8;75;1017
📝 Sprawdzian — Stacja pogodowa
5 pytań · 5 minut · minimum 60% żeby zdać
WHERE od HAVING?🔥 Rozgrzewka — Logowanie z nawigacją menu
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
QStackedWidget w PyQt6?QStackedWidget to kontener stron — wyświetla jedną stronę naraz. Przełączamy strony: stacked.setCurrentIndex(n). Idealny do nawigacji przez menu — użytkownik wybiera opcję z rozwijanego menu, a QStackedWidget pokazuje odpowiedni widok.QLineEdit?.text(): wartosc = entry.text(). Zwraca string. Aby wyczyścić pole: entry.clear(). Aby wstawić tekst: entry.setText("tekst").with open("plik.txt", "r", encoding="utf-8") as f:. Następnie iterujemy: for linia in f:. Każda linia ma na końcu \n, więc warto użyć linia.strip() żeby usunąć białe znaki.==: if wpisane_imie == "Jan":. Uwaga: porównanie jest case-sensitive — "jan" != "Jan". Aby ignorować wielkość liter: wpisane_imie.strip().lower() == "jan".QStackedWidget przez rozwijane menu?QMenu i podpinamy akcje: menu.addAction("Nazwa"). Każda akcja przełącza stronę: akcja.triggered.connect(lambda: stacked.setCurrentIndex(n)). Menu podpinamy do przycisku: btn.setMenu(menu).#05 — Instrukcja — Logowanie z nawigacją menu
Ćwiczenie szkolne · Czas: ~45 min · Aplikacja desktopowa w Pythonie
Kontekst
Z zastosowaniem języka Python i biblioteki PyQt6 wykonaj aplikację desktopową realizującą prosty system logowania z nawigacją menu.
Aplikacja zawiera 3 widoki przełączane rozwijanym menu (navbar) opartym o QPushButton + QMenu + QStackedWidget.
Dane wejściowe — dane.txt
Plik tekstowy zawiera dane jednego użytkownika (imię, nazwisko i klasę), każda wartość w osobnej linii:
Jan
Kowalski
3TIP
Format pliku: linia 1 = imię, linia 2 = nazwisko, linia 3 = klasa.
Widok 1 — „Zaloguj"
- Wyświetla napis powitalny, np. „Witaj! Kliknij ☰ Menu → Logowanie."
- Może zawierać krótką instrukcję obsługi
Widok 2 — „Logowanie"
- Dwa pola tekstowe (
QLineEdit): Imię i Nazwisko - Przycisk „Zaloguj"
- Po kliknięciu program sprawdza, czy wpisane imię i nazwisko zgadzają się z danymi w pliku
dane.txt - Jeśli dane są poprawne — przejście do widoku Profil i wyświetlenie danych
- Jeśli dane są niepoprawne — komunikat o błędzie (np.
QMessageBox.critical)
Widok 3 — „Profil"
- Wyświetla imię, nazwisko i klasę zalogowanego użytkownika
- Opcja „Profil" w menu powinna być niedostępna (szara) dopóki użytkownik się nie zaloguje
Wymagania techniczne
- Użycie
QStackedWidget+QMenudo nawigacji między widokami - Odczyt danych z pliku
dane.txtza pomocąwith open() - Porównanie stringów (imię + nazwisko)
- Obsługa błędnego logowania —
QMessageBox - Znaczące nazwy zmiennych
Na lekcji: Przeczytajcie instrukcję razem. Niech uczeń zidentyfikuje 3 główne etapy: wczytanie danych z pliku → budowa GUI z navbar i widokami → logika logowania.
#05 — Wprowadzenie dla nauczyciela
1. Otwarcie
„Dzisiejsze zadanie jest trochę inne niż poprzednie — tym razem nie pracujemy z bazą danych. Skupiamy się na nawigacji przez rozwijane menu (navbar), przełączaniu widoków i sprawdzaniu danych z pliku."
„Na egzaminie często pada zadanie z nawigacją i przełączaniem widoków — to bardzo powszechny element GUI. Nauczysz się go w ~30 minut."
2. Diagnoza wiedzy
„Czy korzystałeś kiedyś z rozwijanego menu w programie? Takie ☰ na górze — klikasz i rozwijają się opcje?"
Cel: Sprawdzić czy uczeń rozumie koncept nawigacji — jedno okno, kilka widoków przełączanych przez menu.
„Jak odczytać plik tekstowy w Pythonie? Pamiętasz with open()?"
Cel: Upewnić się, że uczeń pamięta open() z wcześniejszych ćwiczeń. Jeśli nie — szybkie przypomnienie.
3. Architektura
„Program ma 3 etapy: (1) wczytaj dane z pliku, (2) zbuduj okno z navbar i 3 widokami, (3) napisz funkcję logowania."
mainwindow.ui → pyuic6 mainwindow.ui -o mainwindow.py → from mainwindow import Ui_MainWindowzaloguj.ui → pyuic6 zaloguj.ui -o zaloguj.py → from zaloguj import Ui_Zalogujformularz.ui → pyuic6 formularz.ui -o formularz.py → from formularz import Ui_Formularzwyswietl.ui → pyuic6 wyswietl.ui -o wyswietl.py → from wyswietl import Ui_Wyswietldane.txt → open() → zmienne (imie, nazwisko, klasa)self.ui.btnMenu + QMenu — rozwijane menu nawigacji (navbar)QStackedWidget w kodzie → 3 widoki (Zaloguj, Logowanie, Profil)self.ui_formularz.btnZaloguj → porównanie → self.stacked.setCurrentIndex(2) lub QtWidgets.QMessageBox.critical()
4. Zasady prowadzenia
Kluczowa zasada: Uczeń pisze kod SAM. Ty pytasz, naprowadzasz, ale NIE dyktuj. Przy błędzie — pytaj: „Co ta linia robi? Czego tu brakuje?"
Tempo: To zadanie jest prostsze niż poprzednie (brak SQL/bazy). Jeśli uczeń jest zaawansowany, ukończy w 20 min. Jeśli początkujący — 45 min z wyjaśnieniami.
5. Częste blokady
| Problem | Podpowiedź |
|---|---|
| ImportError — brak PyQt6 | „Zainstaluj: pip install PyQt6. Zaimportuj potrzebne klasy." |
| QStackedWidget nie przełącza stron | „Czy podłączyłeś akcje menu do ui.stackedWidget.setCurrentIndex(n)?" |
| QLineEdit.text() zwraca pusty string | „Czy pole jest puste? Wpisz coś i kliknij przycisk." |
| Dane z pliku mają \n | „Użyj .strip() na każdej linii — usuwa \n." |
| Porównanie nie działa | „Sprawdź wielkość liter. Użyj .strip() po obu stronach." |
| Profil dostępny przed logowaniem | „Użyj akcja_profil.setEnabled(False) na starcie — zablokuje opcję w menu." |
6. Po zbudowaniu
„Co by się stało, gdyby plik dane.txt nie istniał?"
Odp: Program rzuci FileNotFoundError. Można to obsłużyć try/except.
„Czy przechowywanie hasła/danych w pliku .txt jest bezpieczne?"
Odp: Nie — to tylko ćwiczenie. W prawdziwej aplikacji dane trzyma się w bazie z hashowaniem haseł.
#05 — Wymagania — checklist
| Wymaganie | Detale | Status |
|---|---|---|
| Plik dane.txt | 3 linie: imię, nazwisko, klasa | ✅ |
| Wczytanie pliku | with open(), .strip() na liniach | ✅ |
| Okno PyQt6 | QMainWindow (class MainWindow), tytuł, rozmiar | ✅ |
| QPushButton + QMenu (navbar) | Rozwijane menu z 3 opcjami nawigacji | ✅ |
| QStackedWidget | Tworzony w kodzie, 3 widoki z osobnych .ui | ✅ |
| Widok 1 — napis | Label z tekstem powitalnym | ✅ |
| Widok 2 — QLineEdit × 2 | Pola: Imię, Nazwisko | ✅ |
| Widok 2 — QPushButton | Przycisk „Zaloguj" z clicked.connect() | ✅ |
| Logika logowania | Porównanie .text() z danymi z pliku | ✅ |
| Błędne logowanie | QMessageBox.critical() | ✅ |
| Widok 3 — profil | QLabel z imieniem, nazwiskiem, klasą | ✅ |
| Profil zablokowany w menu | akcja_profil.setEnabled(False) do momentu logowania | ✅ |
| Znaczące nazwy | entryImie, entryNazwisko, zaloguj, itp. | ✅ |
#05 — Budowa krok po kroku
„Najpierw tworzymy plik z danymi. Utwórz nowy plik dane.txt obok main.py, z trzema liniami: imię, nazwisko i klasa."
Zawartość dane.txt:
Jan
Kowalski
3TIP
„Teraz otwórz Qt Creator → File → New → Qt Designer Form → Widget. To będzie nasz formularz."
1. mainwindow.ui — File → New → Qt Designer Form → Main Window → Create
Przeciągnij na formularz:
QPushButton (objectName: btnMenu, tekst: „☰ Menu")Zaznacz centralWidget → prawy klik → Lay out Vertically. Zapisz jako
mainwindow.ui2. zaloguj.ui — File → New → Qt Designer Form → Widget → Create
Przeciągnij:
QLabel (objectName: lblPowitanie)Lay out Vertically. Zapisz jako
zaloguj.ui3. formularz.ui — File → New → Qt Designer Form → Widget → Create
Przeciągnij:
•
QLabel (lblImie) — tekst: „Imię:"•
QLineEdit (entryImie)•
QLabel (lblNazwisko) — tekst: „Nazwisko:"•
QLineEdit (entryNazwisko)•
QPushButton (btnZaloguj) — tekst: „Zaloguj"Lay out Vertically. Zapisz jako
formularz.ui4. wyswietl.ui — File → New → Qt Designer Form → Widget → Create
Przeciągnij:
QLabel (objectName: lblProfil)Lay out Vertically. Zapisz jako
wyswietl.ui
„Teraz konwertujemy każdy formularz na kod Pythona. W terminalu wpisz 4 komendy:"
pyuic6 mainwindow.ui -o mainwindow.py
pyuic6 zaloguj.ui -o zaloguj.py
pyuic6 formularz.ui -o formularz.py
pyuic6 wyswietl.ui -o wyswietl.py
„Utwórz plik main.py w VS Code ze szkieletem."
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
from zaloguj import Ui_Zaloguj
from formularz import Ui_Formularz
from wyswietl import Ui_Wyswietl
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.setWindowTitle("System logowania")
self.resize(400, 300)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
„Uruchom — widzisz okno z przyciskiem ☰ Menu na górze? Super, szkielet działa."
„Teraz wczytujemy dane z pliku dane.txt. Pamiętaj — każda linia to jedna wartość: imię, nazwisko, klasa."
# ===== 1. WCZYTANIE DANYCH Z PLIKU =====
with open("dane.txt", "r", encoding="utf-8") as plik:
linie = plik.readlines()
prawidlowe_imie = linie[0].strip()
prawidlowe_nazwisko = linie[1].strip()
klasa = linie[2].strip()
print(f"Dane z pliku: {prawidlowe_imie} {prawidlowe_nazwisko}, klasa: {klasa}")
„Dlaczego używamy .strip() na każdej linii?"
Odp: Każda linia z pliku kończy się znakiem nowej linii \n. strip() go usuwa. Bez tego porównanie „Jan\n" == „Jan" zwróci False.
Jeśli FileNotFoundError:
„Czy plik dane.txt jest w tym samym folderze co main.py? Sprawdź ścieżkę."
„Co robi readlines()?"
Odp: Zwraca listę wszystkich linii z pliku. linie[0] to pierwsza linia (imię), linie[1] to druga (nazwisko), linie[2] to trzecia (klasa).
„Przycisk menu i QStackedWidget zostały już zaprojektowane w Qt Creator. Zobaczmy jakie widżety mamy dostępne przez obiekt ui."
| Plik .ui | objectName | Typ | Opis |
|---|---|---|---|
| mainwindow.ui | btnMenu | QPushButton | Przycisk „☰ Menu" z QMenu |
| zaloguj.ui | lblPowitanie | QLabel | Tekst powitalny |
| formularz.ui | lblImie | QLabel | Etykieta „Imię:" |
| formularz.ui | entryImie | QLineEdit | Pole na imię |
| formularz.ui | lblNazwisko | QLabel | Etykieta „Nazwisko:" |
| formularz.ui | entryNazwisko | QLineEdit | Pole na nazwisko |
| formularz.ui | btnZaloguj | QPushButton | Przycisk „Zaloguj" |
| wyswietl.ui | lblProfil | QLabel | Dane profilu |
„Jak to działa? QPushButton na górze to nasz navbar — po kliknięciu rozwija się menu z opcjami. QStackedWidget przełącza widoki (strony)."
• Każdy widok (zaloguj, formularz, wyswietl) ma własny plik
.ui → .py•
QStackedWidget tworzymy w kodzie i dodajemy do centralwidget• Każdy widok:
QWidget() → Ui_Xxx() → setupUi(widget) → stacked.addWidget(widget)•
self.stacked.setCurrentIndex(n) — przełącza widok•
QMenu — tworzymy w kodzie i podpinamy: self.ui.btnMenu.setMenu(menu)
„Tworzymy rozwijane menu (navbar) i podpinamy je do przycisku ☰ Menu. Każda opcja w menu przełączy widok QStackedWidget."
# ===== 2. WIDOKI + QStackedWidget =====
self.stacked = QtWidgets.QStackedWidget()
layout = QtWidgets.QVBoxLayout(self.ui.centralwidget)
layout.addWidget(self.stacked)
# Widok 0 — Zaloguj
self.w_zaloguj = QtWidgets.QWidget()
self.ui_zaloguj = Ui_Zaloguj()
self.ui_zaloguj.setupUi(self.w_zaloguj)
self.stacked.addWidget(self.w_zaloguj)
# Widok 1 — Formularz logowania
self.w_formularz = QtWidgets.QWidget()
self.ui_formularz = Ui_Formularz()
self.ui_formularz.setupUi(self.w_formularz)
self.stacked.addWidget(self.w_formularz)
# Widok 2 — Profil
self.w_wyswietl = QtWidgets.QWidget()
self.ui_wyswietl = Ui_Wyswietl()
self.ui_wyswietl.setupUi(self.w_wyswietl)
self.stacked.addWidget(self.w_wyswietl)
# ===== 3. NAVBAR — ROZWIJANE MENU =====
self.menu = QtWidgets.QMenu(self)
self.akcja_zaloguj = self.menu.addAction("Zaloguj")
self.akcja_logowanie = self.menu.addAction("Logowanie")
self.akcja_profil = self.menu.addAction("Profil")
self.ui.btnMenu.setMenu(self.menu)
# Kliknięcie opcji → przełącz widok
self.akcja_zaloguj.triggered.connect(lambda: self.stacked.setCurrentIndex(0))
self.akcja_logowanie.triggered.connect(lambda: self.stacked.setCurrentIndex(1))
self.akcja_profil.triggered.connect(lambda: self.stacked.setCurrentIndex(2))
# Tekst powitalny na widoku 0
self.ui_zaloguj.lblPowitanie.setText("Witaj!\nKliknij ☰ Menu → 'Logowanie'\naby się zalogować.")
„Uruchom. Kliknij przycisk ☰ Menu — rozwija się lista 3 opcji? Klikasz 'Logowanie' — widzisz formularz?"
„Skąd biorą się widżety self.ui_zaloguj.lblPowitanie, self.ui_formularz.entryImie itd.?"
Odp: Z plików wygenerowanych przez pyuic6. Widżety z zaloguj.ui dostępne przez self.ui_zaloguj., z formularz.ui przez self.ui_formularz., z wyswietl.ui przez self.ui_wyswietl.
„Co robi self.ui.btnMenu.setMenu(self.menu)?"
Odp: Podpina QMenu do przycisku — po kliknięciu rozwija się lista opcji (dropdown). To nasz navbar.
„Teraz najważniejsza część — funkcja zaloguj(). Odczytuje dane z pól, porównuje z danymi z pliku i reaguje."
# ===== 3. FUNKCJA LOGOWANIA =====
def zaloguj(self):
wpisane_imie = self.ui_formularz.entryImie.text().strip()
wpisane_nazwisko = self.ui_formularz.entryNazwisko.text().strip()
if wpisane_imie == self.prawidlowe_imie and wpisane_nazwisko == self.prawidlowe_nazwisko:
# Logowanie poprawne — pokaż profil
self.ui_wyswietl.lblProfil.setText(
f"Zalogowano!\n\nImię: {self.prawidlowe_imie}\nNazwisko: {self.prawidlowe_nazwisko}\nKlasa: {self.klasa}"
)
self.akcja_profil.setEnabled(True) # odblokuj opcję Profil w menu
self.stacked.setCurrentIndex(2) # przełącz na widok Profil
else:
QtWidgets.QMessageBox.critical(
self,
"Błąd logowania",
"Nieprawidłowe imię lub nazwisko!\nSpróbuj ponownie."
)
„Co robi self.ui_formularz.entryImie.text()?"
Odp: Pobiera tekst wpisany w pole QLineEdit z widoku formularz. Zwraca string. Dlatego .strip() na wypadek spacji.
„Dlaczego self.akcja_profil.setEnabled(True)?"
Odp: Odblokowujemy opcję „Profil" w menu, która wcześniej była szara i nieaktywna. Dopiero po poprawnym logowaniu użytkownik może ją kliknąć.
„Co robi self.stacked.setCurrentIndex(2)?"
Odp: Programowo przełącza QStackedWidget na stronę 2 (Profil) — tak jakby użytkownik wybrał tę opcję z menu.
zaloguj(self) umieść przed linią self.ui_formularz.btnZaloguj.clicked.connect(self.zaloguj) — Python musi znać metodę zanim ją podłączysz do sygnału.
„Ostatni krok — widok Profil. Na starcie opcja 'Profil' w menu jest wyłączona (szara). Odblokuje się dopiero po poprawnym logowaniu."
# ===== 4. USTAWIENIA PROFILU + BLOKADA =====
self.ui_wyswietl.lblProfil.setText("Nie jesteś zalogowany.")
# Zablokuj opcję Profil w menu na starcie
self.akcja_profil.setEnabled(False)
# ===== 5. PODŁĄCZENIE SYGNAŁÓW =====
self.ui_formularz.btnZaloguj.clicked.connect(self.zaloguj)
„Uruchom. Kliknij ☰ Menu — opcja 'Profil' jest szara/nieaktywna? Nie da się jej kliknąć?"
„Teraz kliknij 'Logowanie' w menu, wpisz poprawne dane (Jan, Kowalski) i kliknij Zaloguj. Co widzisz?"
Bonus: Dodaj przycisk „Wyloguj" na widoku Profil, który wróci do widoku 0, wyczyści pola i zablokuje z powrotem opcję Profil w menu.
#05 — Gotowy kod
Pliki .ui — widżety w Qt Creator
| Plik .ui | objectName | Typ | Opis |
|---|---|---|---|
| mainwindow.ui | btnMenu | QPushButton | Przycisk „☰ Menu" z rozwijanym QMenu |
| zaloguj.ui | lblPowitanie | QLabel | Tekst powitalny |
| formularz.ui | lblImie | QLabel | Etykieta „Imię:" |
| formularz.ui | entryImie | QLineEdit | Pole na imię |
| formularz.ui | lblNazwisko | QLabel | Etykieta „Nazwisko:" |
| formularz.ui | entryNazwisko | QLineEdit | Pole na nazwisko |
| formularz.ui | btnZaloguj | QPushButton | Przycisk „Zaloguj" |
| wyswietl.ui | lblProfil | QLabel | Dane profilu |
Konwersja:
pyuic6 mainwindow.ui -o mainwindow.py
pyuic6 zaloguj.ui -o zaloguj.py
pyuic6 formularz.ui -o formularz.py
pyuic6 wyswietl.ui -o wyswietl.py
main.py
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
from zaloguj import Ui_Zaloguj
from formularz import Ui_Formularz
from wyswietl import Ui_Wyswietl
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.setWindowTitle("System logowania")
self.resize(400, 300)
# ===== 1. WCZYTANIE DANYCH Z PLIKU =====
with open("dane.txt", "r", encoding="utf-8") as plik:
linie = plik.readlines()
self.prawidlowe_imie = linie[0].strip()
self.prawidlowe_nazwisko = linie[1].strip()
self.klasa = linie[2].strip()
# ===== 2. WIDOKI + QStackedWidget =====
self.stacked = QtWidgets.QStackedWidget()
layout = QtWidgets.QVBoxLayout(self.ui.centralwidget)
layout.addWidget(self.stacked)
self.w_zaloguj = QtWidgets.QWidget()
self.ui_zaloguj = Ui_Zaloguj()
self.ui_zaloguj.setupUi(self.w_zaloguj)
self.stacked.addWidget(self.w_zaloguj)
self.w_formularz = QtWidgets.QWidget()
self.ui_formularz = Ui_Formularz()
self.ui_formularz.setupUi(self.w_formularz)
self.stacked.addWidget(self.w_formularz)
self.w_wyswietl = QtWidgets.QWidget()
self.ui_wyswietl = Ui_Wyswietl()
self.ui_wyswietl.setupUi(self.w_wyswietl)
self.stacked.addWidget(self.w_wyswietl)
# ===== 3. NAVBAR — ROZWIJANE MENU =====
self.menu = QtWidgets.QMenu(self)
self.akcja_zaloguj = self.menu.addAction("Zaloguj")
self.akcja_logowanie = self.menu.addAction("Logowanie")
self.akcja_profil = self.menu.addAction("Profil")
self.ui.btnMenu.setMenu(self.menu)
self.akcja_zaloguj.triggered.connect(lambda: self.stacked.setCurrentIndex(0))
self.akcja_logowanie.triggered.connect(lambda: self.stacked.setCurrentIndex(1))
self.akcja_profil.triggered.connect(lambda: self.stacked.setCurrentIndex(2))
# ===== 4. USTAWIENIA WIDŻETÓW =====
self.ui_zaloguj.lblPowitanie.setText(
"Witaj!\nKliknij ☰ Menu → 'Logowanie'\naby się zalogować."
)
self.ui_wyswietl.lblProfil.setText("Nie jesteś zalogowany.")
self.akcja_profil.setEnabled(False)
# ===== 5. PODŁĄCZENIE SYGNAŁÓW =====
self.ui_formularz.btnZaloguj.clicked.connect(self.zaloguj)
# ===== 6. METODA LOGOWANIA =====
def zaloguj(self):
wpisane_imie = self.ui_formularz.entryImie.text().strip()
wpisane_nazwisko = self.ui_formularz.entryNazwisko.text().strip()
if wpisane_imie == self.prawidlowe_imie and wpisane_nazwisko == self.prawidlowe_nazwisko:
self.ui_wyswietl.lblProfil.setText(
f"Zalogowano!\n\nImię: {self.prawidlowe_imie}\n"
f"Nazwisko: {self.prawidlowe_nazwisko}\nKlasa: {self.klasa}"
)
self.akcja_profil.setEnabled(True)
self.stacked.setCurrentIndex(2)
else:
QtWidgets.QMessageBox.critical(
self,
"Błąd logowania",
"Nieprawidłowe imię lub nazwisko!\nSpróbuj ponownie."
)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
dane.txt
Jan
Kowalski
3TIP
1. Zaprojektuj 4 formularze w Qt Creator → zapisz jako
mainwindow.ui, zaloguj.ui, formularz.ui, wyswietl.ui2. Konwertuj każdy:
pyuic6 mainwindow.ui -o mainwindow.py (i analogicznie pozostałe 3)3. Utwórz
main.py i dane.txt w tym samym folderze4. Uruchom:
python main.py5. Kliknij ☰ Menu → „Logowanie", wpisz Jan i Kowalski, kliknij „Zaloguj"
6. Widok przełączy się na „Profil" z danymi: Jan Kowalski, klasa 3TIP
📝 Sprawdzian — Logowanie z nawigacją menu
5 pytań · 5 minut · minimum 60% żeby zdać
entry.text()?readlines() należy użyć .strip() na każdej linii?Rozgrzewka — Konwerter walut
Szybkie pytania na początek lekcji — kliknij pytanie żeby zobaczyć odpowiedź.
1. Czym różni się input() od print()?
input() CZYTA tekst od użytkownika i go zwraca. print() WYPISUJE tekst na ekran. Pamiętaj: input() zawsze zwraca string — żeby dostać liczbę musisz użyć int() lub float().
2. Co to jest słownik (dict) w Pythonie?
Słownik to zbiór par klucz-wartość: {"USD": 4.05, "EUR": 4.32}. Dostęp przez klucz: kursy["USD"] daje 4.05. Idealny do tabeli kursów walut.
3. Kiedy użyć pętli while a kiedy for?
for — gdy znamy ile razy iterować (po liście, range). while — gdy iterujemy DOPÓKI warunek prawdziwy (np. menu programu działa do wyboru "0 - Wyjście").
4. Jak sformatować liczbę do 2 miejsc po przecinku?
F-string z formatowaniem: f"{kwota:.2f}". Np. f"{4.0567:.2f}" → "4.06". Dwie cyfry po kropce + zaokrąglanie.
5. Co robi try / except?
Łapie błędy. try próbuje wykonać kod, except ValueError łapie błąd jeśli się pojawi. Idealne gdy użytkownik wpisze tekst zamiast liczby — program nie crashuje, tylko pokazuje komunikat.
Konwerter walut — Instrukcja
Treść zadania
Napisz program konsolowy który:
- Wyświetla menu z 4 walutami: USD, EUR, GBP, CHF (kursy w PLN)
- Pyta użytkownika ile PLN chce przeliczyć
- Pyta na jaką walutę przeliczyć
- Wyświetla wynik z dokładnością do 2 miejsc po przecinku
- Po przeliczeniu pyta czy kontynuować — pętla działa do wyjścia
- Łapie błędy gdy użytkownik wpisze tekst zamiast liczby (try/except)
Czego się nauczysz
| Pojęcie | Co robi |
|---|---|
dict (słownik) | Tabela kursów walut: nazwa → kurs |
input() | Pobranie danych od użytkownika |
float() | Konwersja stringa na liczbę zmiennoprzecinkową |
while True | Pętla menu — działa do break |
try / except | Obsługa błędów (np. tekst zamiast liczby) |
f-string :.2f | Formatowanie liczby do 2 miejsc po przecinku |
def | Wydzielenie kodu do funkcji |
Przykładowe działanie
=== KONWERTER WALUT ===
Dostępne waluty:
USD - 4.05 PLN
EUR - 4.32 PLN
GBP - 5.18 PLN
CHF - 4.61 PLN
Ile PLN chcesz przeliczyć? 100
Na jaką walutę? (USD/EUR/GBP/CHF): EUR
100.00 PLN = 23.15 EUR
Kontynuować? (t/n): n
Do widzenia!
Konwerter walut — Budowa krok po kroku
Utwórz plik konwerter.py. Na górze pliku dodaj słownik kursów oraz funkcję wyświetlającą menu:
kursy = {
"USD": 4.05,
"EUR": 4.32,
"GBP": 5.18,
"CHF": 4.61
}
def pokaz_menu():
print("\n=== KONWERTER WALUT ===\n")
print("Dostępne waluty:")
for waluta, kurs in kursy.items():
print(f" {waluta} - {kurs} PLN")
waluta i kurs.
def przelicz(kwota_pln, waluta):
kurs = kursy[waluta]
wynik = kwota_pln / kurs
return wynik
Funkcja przyjmuje kwotę w PLN i symbol waluty, dzieli przez kurs i zwraca wynik.
100 / 4.05 ≈ 24.69 USD.
while True:
pokaz_menu()
# Pobierz kwotę
kwota_str = input("\nIle PLN chcesz przeliczyć? ")
kwota = float(kwota_str)
# Pobierz walutę
waluta = input("Na jaką walutę? (USD/EUR/GBP/CHF): ").upper()
# Przelicz
wynik = przelicz(kwota, waluta)
print(f"\n{kwota:.2f} PLN = {wynik:.2f} {waluta}")
# Czy kontynuować?
odp = input("\nKontynuować? (t/n): ").lower()
if odp == "n":
print("Do widzenia!")
break
.upper()— zamienia "eur" → "EUR" (klucze w słowniku są wielkimi literami):.2f— formatuje liczbę do 2 miejsc po przecinku (np. 23.149 → 23.15)while True ... break— pętla nieskończona aż dobreak
Co jeśli użytkownik wpisze "JPY"? Słownik nie zawiera tej waluty — program crashuje. Dodaj sprawdzenie:
waluta = input("Na jaką walutę? (USD/EUR/GBP/CHF): ").upper()
if waluta not in kursy:
print(f"Nieznana waluta: {waluta}")
continue # wraca na początek pętli
Co jeśli zamiast "100" użytkownik wpisze "sto"? float("sto") rzuca ValueError — program się wywala.
kwota_str = input("\nIle PLN chcesz przeliczyć? ")
try:
kwota = float(kwota_str)
except ValueError:
print(f"To nie jest liczba: {kwota_str}")
continue
try— "spróbuj wykonać ten kod"except ValueError— "jeśli wystąpi błąd ValueError, zrób to"- Bez try/except program by crashował przy złym wejściu
W terminalu uruchom: python konwerter.py
Sprawdź każdy scenariusz:
| Test | Oczekiwany wynik |
|---|---|
| Wpisz: 100, EUR | 23.15 EUR |
| Wpisz: 200, usd (małe litery) | 49.38 USD (działa dzięki .upper()) |
| Wpisz: 50, JPY | "Nieznana waluta: JPY", wraca do menu |
| Wpisz: sto, EUR | "To nie jest liczba: sto", wraca do menu |
| Po przeliczeniu wpisz: n | "Do widzenia!" — program kończy |
| Po przeliczeniu wpisz: t | Wraca do menu, można przeliczać dalej |
Konwerter walut — Gotowy kod
konwerter.py
kursy = {
"USD": 4.05,
"EUR": 4.32,
"GBP": 5.18,
"CHF": 4.61
}
def pokaz_menu():
print("\n=== KONWERTER WALUT ===\n")
print("Dostępne waluty:")
for waluta, kurs in kursy.items():
print(f" {waluta} - {kurs} PLN")
def przelicz(kwota_pln, waluta):
kurs = kursy[waluta]
wynik = kwota_pln / kurs
return wynik
while True:
pokaz_menu()
# Pobierz kwotę z obsługą błędu
kwota_str = input("\nIle PLN chcesz przeliczyć? ")
try:
kwota = float(kwota_str)
except ValueError:
print(f"To nie jest liczba: {kwota_str}")
continue
# Pobierz walutę i sprawdź czy istnieje
waluta = input("Na jaką walutę? (USD/EUR/GBP/CHF): ").upper()
if waluta not in kursy:
print(f"Nieznana waluta: {waluta}")
continue
# Przelicz i wyświetl wynik
wynik = przelicz(kwota, waluta)
print(f"\n{kwota:.2f} PLN = {wynik:.2f} {waluta}")
# Czy kontynuować?
odp = input("\nKontynuować? (t/n): ").lower()
if odp == "n":
print("Do widzenia!")
break
Co warto rozszerzyć (zadania domowe)
- Dodaj odwrotną konwersję (z USD na PLN)
- Wczytaj kursy z pliku
kursy.txtzamiast trzymać w kodzie - Zapisuj historię przeliczeń do pliku
historia.txt - Dodaj walutę z największym kursem (znajdź
max(kursy.values()))
Konwerter walut — Sprawdzian
5 pytań · 5 minut · minimum 60% żeby zdać
input()?continue w pętli?float("abc") bez try/except?kursy?📝 Ściąga Python
Zmienne & typy
x = 10 # int
y = 3.14 # float
s = "tekst" # str
b = True # bool
l = [1, 2, 3] # list
d = {"k": "v"} # dictf-string
f"Cześć {imie}"
f"{cena:.2f} zł"
f"{nr:05d}"if / elif / else
if x > 0:
print("dodatnia")
elif x == 0:
print("zero")
else:
print("ujemna")for + range
for i in range(5):
print(i)
for el in lista:
print(el)
for i, el in enumerate(l):
print(i, el)Funkcja
def suma(a, b=0):
return a + bLista — metody
l.append(x)
l.insert(0, x)
l.remove(x)
l.pop()
l.sort()
sorted(l, key=...)Słownik
d = {"imie": "Jan"}
d["imie"]
d.get("x", "?")
d.keys()
d.values()
d.items()Plik — czytanie
with open("f.txt", "r",
encoding="utf-8") as f:
for linia in f:
linia = linia.strip()
parts = linia.split(";")Plik — zapis
with open("f.txt", "w",
encoding="utf-8") as f:
f.write("tekst\n")SQLite — tworzenie
import sqlite3
conn = sqlite3.connect("db.db")
c = conn.cursor()
c.execute("""CREATE TABLE IF
NOT EXISTS t (id INTEGER PRIMARY KEY,
nazwa TEXT, cena REAL)""")
conn.commit()SQLite — INSERT
c.execute(
"INSERT INTO t VALUES (?,?,?)",
(1, "Laptop", 3500.0)
)
conn.commit()SQLite — SELECT
c.execute("SELECT * FROM t
WHERE kategoria = ?
ORDER BY cena DESC",
("elektronika",))
rows = c.fetchall()PyQt6 — Qt Creator
import sys
from PyQt6 import QtWidgets
from mainwindow import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())PyQt6 — self.ui. widżety
# pyuic6 mainwindow.ui -o mainwindow.py
self.ui.label.setText("X")
self.ui.lineEdit.text()
self.ui.pushButton.clicked.connect(self.fn)
self.ui.listWidget.addItem("el")
self.ui.comboBox.currentText()List comprehension
[x**2 for x in range(5)]
[x for x in l if x > 0]Konwersja typów
int("42")
float("3.14")
str(100)
bool(0) # False🐛 Typowe błędy Python
# ❌ Mieszanie tab i spacji
if True:
print("tab") # ← TAB
print("space") # ← 4 spacje → BŁĄD
# ✅ Zawsze 4 spacje
if True:
print("ok")# ❌
print(imie) # imie nie istnieje
# ✅
imie = "Maks"
print(imie)# ❌ input() zwraca string!
wiek = input("Wiek: ")
print(wiek + 1) # "18" + 1 → TypeError
# ✅
wiek = int(input("Wiek: "))
print(wiek + 1) # 19# ❌ Zły folder lub nazwa pliku
open("Dane.txt") # a plik to dane.txt (małe d!)
# ✅ Sprawdź: plik w tym samym folderze co .py
# ✅ Nazwy case-sensitive na Mac/Linux!# ❌
l = [1, 2, 3]
print(l[3]) # IndexError! (indeksy 0, 1, 2)
# ✅
print(l[2]) # 3
print(l[-1]) # 3 (ostatni)# ❌ Brak przecinka — to nie tuple!
cursor.execute("SELECT * WHERE k=?", ("elektronika")) # Error
# ✅ Przecinek tworzy tuple
cursor.execute("SELECT * WHERE k=?", ("elektronika",))# ❌ INSERT działa, ale dane nie zapisane na dysk
cursor.execute("INSERT INTO t VALUES (?,?)", (1, "X"))
# brak commit → dane znikną po zamknięciu
# ✅
cursor.execute("INSERT INTO t VALUES (?,?)", (1, "X"))
conn.commit() # ← ZAPISZ!# ❌ ImportError: No module named 'mainwindow'
from mainwindow import Ui_MainWindow # plik nie istnieje!
# ✅ Wygeneruj plik z formularza .ui:
# pyuic6 mainwindow.ui -o mainwindow.py
# Upewnij się, że mainwindow.ui jest zapisany w Qt Creator# ❌ app.exec() PRZED konfiguracją
window.show()
app.exec()
ui.btnFiltruj.clicked.connect(fn) # za późno!
# ✅ app.exec() na SAMYM KOŃCU
ui.btnFiltruj.clicked.connect(fn)
window.show()
app.exec()# ❌ .sort() zwraca None!
wynik = lista.sort() # wynik = None
# ✅ sorted() zwraca NOWĄ listę
wynik = sorted(lista) # wynik = posortowana kopia
# ✅ .sort() mutuje oryginalną
lista.sort() # lista jest teraz posortowana🎓 Matura z informatyki rozszerzonej — jak to działa
Zanim wejdziemy w zadania — zrozum egzamin jako całość. Matura rozszerzona z informatyki to przepustka na kierunki IT (informatyka, automatyka, AI) na uczelniach technicznych. Liczy się jako przedmiot dodatkowy — im wyższy wynik, tym więcej punktów rekrutacyjnych.
Format egzaminu (Formuła 2023)
| Parametr | Wartość |
|---|---|
| Czas | 210 minut (3,5 godziny) |
| Punkty | 50 |
| Liczba zadań | ~8 zadań głównych, podzielonych na podpunkty |
| Forma | Jeden arkusz: część „bez komputera" (papier) + część „przy komputerze" |
| Języki programowania | C++, Java, Python (wybierasz jeden) |
| Oprogramowanie na sali | Środowisko do programowania, arkusz kalkulacyjny (Excel/Calc), baza danych (SQL) |
| Internet | Brak — komputer odcięty od sieci |
Pięć działów — i ile są warte
Rozkład punktów jest dość stały rok do roku. Z arkusza maj 2026:
| Dział | Waga | Charakter |
|---|---|---|
| Programowanie (napisy, struktury, pliki) | ~34% | Kod czytający pliki z danymi (setki–tysiące wierszy) |
| Algorytmika i rekurencja | ~24% | Analiza algorytmu na papierze + pisanie |
| Bazy danych (SQL) | ~18% | Zapytania na 3 powiązanych tabelach |
| Arkusz kalkulacyjny + modelowanie | ~18% | Formuły, wykresy, symulacje |
| Teoria (systemy, sieci, logika) | ~6% | Krótkie zadania na wiedzę |
Jak wygląda dzień egzaminu
- Dostajesz arkusz papierowy + folder z plikami danych na komputerze (np.
pary.txt,klienci.txt) - Część zadań rozwiązujesz na papierze (algorytm, teoria, analiza), część przy komputerze (kod, SQL, arkusz)
- Pliki z rozwiązaniami (kod źródłowy, plik arkusza, zapytania SQL) zapisujesz w wskazanym folderze pod określoną nazwą
- Oceniany jest zapisany plik — pamiętaj zapisywać często (brak autozapisu = stracone punkty przy zawieszeniu)
Strategia na sali (210 minut)
- Najpierw przejrzyj cały arkusz (5 min) — zaznacz zadania które od razu umiesz.
- Zbierz pewne punkty — teoria (systemy, sieci) i proste podpunkty SQL/Excel. Szybkie, gwarantowane.
- Potem zadania programistyczne — najwięcej punktów, ale czasochłonne. Zrób podpunkty od najłatwiejszego (zwykle .1 przed .3).
- Podpunkty są niezależne — jeśli nie umiesz 3.3, zrób 3.1 i 3.2. Punkty się sumują.
- Zapisuj co 10 minut i nazywaj pliki dokładnie tak jak każe polecenie.
- Pilnuj czasu — orientacyjnie ~4 min na punkt. Nie utknij 40 min na jednym podpunkcie za 2p.
Co oznacza wynik (orientacyjnie)
| Wynik | Interpretacja |
|---|---|
| 30–40% | Próg na mniej oblężone kierunki |
| 50–60% | Solidny — większość kierunków IT |
| 70%+ | Mocny — czołowe uczelnie (PW, AGH, UJ) |
| 85%+ | Bardzo wysoki — pewne wejście wszędzie |
Następny krok
Przejdź do Arkusz CKE → „Maj 2026 — omówienie" — tam masz realny arkusz rozłożony zadanie po zadaniu, z pełnymi rozwiązaniami w Pythonie, SQL i arkuszu kalkulacyjnym. To pokazuje czego konkretnie się uczysz. Potem teoria działami (Systemy liczbowe → … → Teoria informacji) i ćwiczenia.
📄 Arkusz CKE — Maj 2026 (rozszerzona)
Pełne omówienie arkusza, z którego zdawali maturzyści w maju 2026 (Formuła 2023). Jeden arkusz, 210 minut, 50 punktów. Część zadań „bez komputera" (algorytm na papierze, teoria), część „przy komputerze" (kod, SQL, arkusz kalkulacyjny). To twoja mapa — z czego dokładnie będziesz zdawał.
Rozkład punktów — na czym się skupić
| Dział | Punkty | Waga |
|---|---|---|
| Programowanie (napisy, struktury, pliki) | 17 | 34% |
| Algorytmika i rekurencja | 12 | 24% |
| Bazy danych (SQL) | 9 | 18% |
| Arkusz kalkulacyjny + modelowanie | 9 | 18% |
| Teoria (systemy, sieci) | 3 | 6% |
Załączniki — pliki z danymi (folder Dane-NF-2605)
Na egzaminie dostajesz folder z plikami wejściowymi do zadań. Oto co realnie w nich jest:
| Plik | Wierszy | Struktura | Zadanie |
|---|---|---|---|
pary.txt | 500 | dwa słowa w wierszu (spacja), litery a–d, max 50 | 3 |
korpo.txt | 50 000 | liczba/wiersz = nr przełożonego pracownika i; 0 = prezes | 4 |
staw.txt | 365 dni | TAB: Data, Temp, Opady | 7 |
klienci.txt | 2045 | TAB: IdKlienta, Imie, Nazwisko, Plec (K/M) | 8 |
transakcje.txt | 1000 | TAB: IdTransakcji, DataTransakcji, IdKlienta, IdSklepu, IdSprzedawcy | 8 |
opis_transakcji.txt | 1466 | TAB: IdTransakcji, IdProduktu, Cena, Liczba | 8 |
- pary.txt — litery to a, b, c i d. Nie zakładaj alfabetu z głowy.
- staw.txt — temperatura z polskim przecinkiem (
9,8). W Pythoniefloat("9,8")wybucha — trzebafloat(x.replace(",", ".")). - transakcje.txt — 163 transakcje mają puste IdSprzedawcy = kasy samoobsługowe. To klucz do zadania 8.3! Po
split("\t")to"", nieNone. - opis_transakcji.txt — wiele wierszy na jedną transakcję (pozycje koszyka). Kwota = suma
Cena × Liczba. - korpo.txt — drzewo zapisane jako tablica rodziców. Pracownik
ijest w liniii(od 1). - Wszędzie separator TAB (
\t) pozapary.txt(spacja). Pliki SQL-owe mają nagłówek do pominięcia.
Zadanie 1 — Rekurencja [7 pkt, papier]
Dana funkcja: A(m,n) = m gdy n=1; A(2m, n/2) gdy n parzyste; 2·A(m, (n−1)/2) + m gdy n nieparzyste.
A(m, n) = m · n — ta funkcja to po prostu mnożenie metodą binarną. Dowód indukcyjny: baza A(m,1)=m=m·1; parzyste A(2m,n/2)=(2m)(n/2)=mn; nieparzyste 2·m·(n−1)/2 + m = m(n−1)+m = mn. Liczba wywołań = liczba kroków sprowadzających n do 1 ≈ ⌊log₂n⌋.
1.1 — tabela wywołań (krok po kroku)
„Wywołanie rekurencyjne" = każde wejście do funkcji A z wnętrza A. Wywołanie startowe (np. A(3,9)) nie liczy się — liczymy dopiero te, które ono uruchamia. Śledzimy drugi argument n i stosujemy reguły: parzyste → n/2, nieparzyste → (n−1)/2, aż dojdziemy do n=1 (baza).
m=2⁵=32, n=2⁵=32 — n parzyste cały czas, więc tylko dzielenie przez 2 (a m się podwaja):
A(32,32) → A(64,16) → A(128,8) → A(256,4) → A(512,2) → A(1024,1)
(1) (2) (3) (4) (5) = baza
Drugi argument: 16, 8, 4, 2, 1 → 5 wywołań.
m=10, n=15 — n nieparzyste, więc (n−1)/2; m się nie zmienia (w regule nieparzystej pierwszy argument zostaje):
15 → (15−1)/2 = 7 → (7−1)/2 = 3 → (3−1)/2 = 1
(1) (2) (3) = baza
Wywołania: A(10,7), A(10,3), A(10,1) → 3 wywołania.
m=1, n=2¹⁰⁰+1 — najpierw jeden krok nieparzysty: (2¹⁰⁰+1−1)/2 = 2¹⁰⁰/2 = 2⁹⁹. Potem 2⁹⁹ to czyste potęgi dwójki — dzielimy przez 2 aż do 2⁰=1. Drugie argumenty kolejnych wywołań: 2⁹⁹, 2⁹⁸, …, 2¹, 2⁰. Wykładniki od 99 do 0 to 100 liczb → 100 wywołań.
1.2 — wartości (dlaczego tak)
Tu nie liczymy wywołań ręcznie — używamy wzoru A(m,n)=m·n (udowodnionego wyżej). To po prostu mnożenie:
A(1, 777) = 1 · 777 = 777A(2·10⁶, 256·10⁶) = 2·10⁶ · 256·10⁶ = (2·256) · 10⁶⁺⁶ = 512·10¹²
Gdybyś NIE zauważył wzoru, musiałbyś dla 256·10⁶ wykonać ~28 wywołań ręcznie (256·10⁶ ≈ 2²⁸) — dlatego rozpoznanie „to jest mnożenie" jest tu kluczowe i oszczędza mnóstwo czasu.
1.3 — liczba wywołań i i-ty argument (uogólnienie 1.1)
To samo co w 1.1, tylko zapisane wzorem dla dowolnego k. Wzorzec z przykładu n=8: drugie argumenty to 4, 2, 1 = 8/2¹, 8/2², 8/2³, czyli i-ty argument = 8/2ⁱ = 2³⁻ⁱ, a wywołań jest 3.
n = 2ᵏ — same potęgi dwójki, dzielimy przez 2: drugie argumenty to 2ᵏ⁻¹, 2ᵏ⁻², …, 2⁰. To k wywołań, a i-ty argument = 2ᵏ⁻ⁱ (= 2ᵏ/2ⁱ). Sprawdź dla k=3 (n=8): 3 wywołania, 2³⁻ⁱ ✓.
n = 2ᵏ−1 — liczba nieparzysta, więc reguła (n−1)/2. Ciekawostka: (2ᵏ−1−1)/2 = (2ᵏ−2)/2 = 2ᵏ⁻¹−1 — znów liczba tej samej postaci, o jeden wykładnik mniej! Sekwencja: 2ᵏ⁻¹−1, 2ᵏ⁻²−1, …, 2¹−1=1 (baza). Wykładniki od k−1 do 1 → k−1 wywołań, i-ty argument = 2ᵏ⁻ⁱ−1. Sprawdź dla k=3 (n=7): 7→3→1 = 2 wywołania = k−1 ✓.
| n | liczba wywołań | i-ty drugi argument |
|---|---|---|
| 8 | 3 | 2³⁻ⁱ |
| 2ᵏ | k | 2ᵏ⁻ⁱ |
| 2ᵏ−1 | k−1 | 2ᵏ⁻ⁱ−1 |
Zadanie 2 — Liczenie przeniesień [5 pkt]
O co chodzi: gdy dodajesz dwie liczby „pisemnie" (jak w szkole — kolumna po kolumnie od prawej), czasem suma w kolumnie wynosi 10 lub więcej. Wtedy piszesz ostatnią cyfrę, a 1 „przenosisz" do następnej kolumny. Zadanie: policzyć ile razy nastąpi takie przeniesienie.
Najpierw zrozum na papierze (przykład z arkusza: 27732 + 72619)
Dodajemy kolumnami od prawej. „p" = powstaje przeniesienie:
pozycja: 5 4 3 2 1 (od prawej)
a: 2 7 7 3 2
b: 7 2 6 1 9
─────────────────────────────
jedności: 2+9 = 11 → piszę 1, przenoszę 1 ← przeniesienie #1
dziesiątki:3+1 +1(p)= 5 → bez przeniesienia
setki: 7+6 = 13 → przeniesienie ← #2
tysiące: 7+2 +1(p)= 10 → przeniesienie ← #3
dzies.tys.:2+7 +1(p)= 10 → przeniesienie ← #4
─────────────────────────────
RAZEM: 4 przeniesienia ✓
Zwróć uwagę: przeniesienie z poprzedniej kolumny dolicza się do następnej (dlatego 3+1+1=5, a 7+2+1=10).
2.1 — odpowiedzi (rozpisane kolumnami)
88765 + 11111: każda kolumna: 5+1=6, 6+1=7, 7+1=8, 8+1=9, 8+1=9 — żadna suma nie sięga 10. → 0 przeniesień (wynik 99876).
456789 + 222222:
9+2=11 → przeniesienie #1
8+2+1=11 → #2
7+2+1=10 → #3
6+2+1=9 → bez
5+2=7 → bez
4+2=6 → bez
→ 3 przeniesienia (wynik 679011)
2.2 — algorytm (bez tablic i stringów, tylko arytmetyka)
Klucz: jak „wyciągnąć" cyfry liczby bez zamiany na tekst? Dwoma operatorami:
n % 10→ ostatnia cyfra (reszta z dzielenia przez 10). Np.27732 % 10 = 2.n // 10→ liczba bez ostatniej cyfry (dzielenie całkowite). Np.27732 // 10 = 2773.
Powtarzając te dwie operacje, „zjadamy" liczbę cyfra po cyfrze od prawej — dokładnie tak jak dodawanie pisemne.
def przeniesienia(a, b):
carry = 0 # czy z poprzedniej kolumny przyszło przeniesienie (0/1)
count = 0 # licznik przeniesień
while a > 0 or b > 0: # dopóki w którejkolwiek liczbie zostały cyfry
s = a % 10 + b % 10 + carry # suma cyfr w tej kolumnie + ew. przeniesienie
if s >= 10:
carry = 1 # jest przeniesienie do następnej kolumny
count += 1 # zliczamy je
else:
carry = 0 # nie ma przeniesienia
a //= 10 # obcinamy ostatnią cyfrę a
b //= 10 # obcinamy ostatnią cyfrę b
return count
Linia po linii: w każdym obrocie pętli bierzemy ostatnią cyfrę a (a%10) i b (b%10), dodajemy do nich przeniesienie z poprzedniej kolumny. Jeśli suma ≥10 — ustawiamy carry=1 i zliczamy. Potem //10 przesuwa nas o kolumnę w lewo. Pętla działa while a>0 OR b>0 (nie AND!), bo liczby mogą mieć różną długość — kończymy gdy obie się „wyczerpią".
% i //), a nie „obejść" zadanie zamieniając liczbę na napis i czytając znaki.Zadanie 3 — Pary słów [9 pkt, pary.txt]
O co chodzi: w pliku pary.txt jest 500 wierszy, w każdym dwa słowa (litery a–z, oddzielone spacją). Dla każdego podpunktu przechodzimy wszystkie pary i czegoś szukamy.
Wczytanie pliku — czytamy linia po linii, dzielimy po spacji na dwa słowa:
pary = []
with open("pary.txt") as f:
for linia in f:
a, b = linia.split() # "bcba babb" → a="bcba", b="babb"
pary.append((a, b))
3.1 — największa różnica sum kodów ASCII
Każda litera ma swój kod ASCII (numer w tablicy znaków): a=97, b=98, c=99, …, z=122. W Pythonie ord('a') daje 97. f(s) = suma kodów wszystkich liter słowa.
Przykład z arkusza: dla słów oko i pies:
f("oko") = ord('o')+ord('k')+ord('o') = 111+107+111 = 329
f("pies") = ord('p')+ord('i')+ord('e')+ord('s') = 112+105+101+115 = 433
|329 − 433| = 104
Szukamy pary, dla której ta różnica (wartość bezwzględna) jest największa. Lecimy przez wszystkie 500 par i zapamiętujemy maksimum:
def suma_ascii(s):
return sum(ord(z) for z in s) # suma kodów liter słowa
maks = 0
wynik = None
for a, b in pary:
roznica = abs(suma_ascii(a) - suma_ascii(b))
if roznica > maks:
maks = roznica
wynik = (a, b) # zapamiętujemy też PARĘ, nie tylko liczbę
print(wynik, maks)
3.2 — suma wspólnych wystąpień liter
Dla każdej litery liczymy ile razy występuje w obu słowach i bierzemy mniejszą z tych dwóch liczb (to „wspólna" liczba wystąpień). Sumujemy po wszystkich literach.
Przykład z arkusza: para adabbcdde / aadabbbccdc:
litera a: w 1. słowie 2 razy, w 2. słowie 3 razy → min(2,3)=2
litera b: 2 i 3 → min=2
litera c: 1 i 3 → min=1
litera d: 3 i 2 → min=2
litera e: 1 i 0 → min=0
suma wspólnych = 2+2+1+2+0 = 7
Counter(s) z biblioteki collections automatycznie zlicza litery (Counter("aab") → {'a':2,'b':1}). set(a) & set(b) to litery wspólne dla obu słów:
from collections import Counter
maks = 0
wynik = None
for a, b in pary:
ca, cb = Counter(a), Counter(b)
wspolne = sum(min(ca[z], cb[z]) for z in set(a) & set(b))
if wspolne > maks:
maks = wspolne
wynik = (a, b)
print(wynik, maks)
3.3 — najdłuższy prefiksosufiks ≥ 5
Prefiksosufiks pary to słowo, które jest początkiem (prefiksem) jednego słowa i jednocześnie końcem (sufiksem) drugiego — i to w obie strony.
Przykład z arkusza: para abbaabaa / baabaabba:
"abba" — początek 1. słowa (abbaabaa) i koniec 2. słowa (...aabba) → długość 4
"baabaa" — początek 2. słowa (baabaabba) i koniec 1. słowa (...baabaa) → długość 6
najdłuższy ma długość 6 → para spełnia warunek (≥5)
Sprawdzamy oba kierunki (prefiks a = sufiks b ORAZ prefiks b = sufiks a), dla każdej długości k. a[:k] = pierwsze k liter, b[-k:] = ostatnie k liter:
def pref_suf(a, b):
best = 0
for k in range(1, min(len(a), len(b)) + 1):
if a[:k] == b[-k:]: # początek a = koniec b
best = max(best, k)
if b[:k] == a[-k:]: # początek b = koniec a
best = max(best, k)
return best
for a, b in pary:
if pref_suf(a, b) >= 5:
print(a, b, pref_suf(a, b))
- 3.1 → para
gpeeazeugmvsbzwsrxfplqdbakoxxe/lhpbmoirdm, różnica 2206 - 3.2 → para
aacbcccaacacbcabac/cccccaaaacaccbabcba, suma 18 - 3.3 → 7 par z prefiksosufiksem ≥5 (m.in.
aababbbababbbbbbaab / bbbbaabbababababa → 7,aaaababaaaabbbb / aabbbbbabbbaaaa → 6,caabbccabccc / cabccccabbaac → 6; reszta po 5)
Zadanie 4 — Korporacja / drzewo [8 pkt, korpo.txt]
O co chodzi: plik korpo.txt ma 50 000 wierszy. Wiersz numer i zawiera numer przełożonego pracownika i. Wartość 0 oznacza prezesa (nie ma nad sobą nikogo). To jest drzewo — tylko zapisane nietypowo: każdy „wskazuje w górę" na swojego szefa.
Przykład (mała firma, 6 osób):
wiersz 1: 0 ← pracownik 1 to prezes (szef = 0)
wiersz 2: 1 ← pracownik 2 podlega pod 1
wiersz 3: 1 ← pracownik 3 podlega pod 1
wiersz 4: 3 ← pracownik 4 podlega pod 3
wiersz 5: 3 ← pracownik 5 podlega pod 3
wiersz 6: 3 ← pracownik 6 podlega pod 3
1 (prezes)
/ \
2 3
/|\
4 5 6
Wczytanie — chcemy by szef[i] = przełożony pracownika i. Pracownicy numerowani od 1, więc na indeks 0 wkładamy „zaślepkę":
szef = [0] # indeks 0 nieużywany (pracownicy od 1)
with open("korpo.txt") as f:
for linia in f:
szef.append(int(linia))
n = len(szef) - 1 # liczba pracowników
4.2 — liczba „liści" (szeregowych)
Liść = pracownik, który nie jest niczyim szefem — czyli jego numer nigdy nie pojawia się jako wartość w pliku. W przykładzie: 2, 4, 5, 6 (nikt pod nimi nie podlega) = 4 liście.
Counter(szef[1:]) zlicza ile razy każdy numer występuje jako szef = ilu ma bezpośrednich podwładnych. Kto ma 0 → jest liściem:
from collections import Counter
ile_podwladnych = Counter(szef[1:]) # ile razy każdy jest czyimś szefem
liscie = sum(1 for i in range(1, n+1) if ile_podwladnych[i] == 0)
print(liscie)
4.3 — kto ma najwięcej bezpośrednich podwładnych
To po prostu numer, który najczęściej pojawia się jako wartość w pliku (najczęstszy szef). Korzystamy z tego samego Counter — szukamy klucza o największej wartości (pomijając 0, bo to „brak przełożonego", nie pracownik):
del ile_podwladnych[0] # 0 to nie pracownik, tylko znacznik prezesa
najlepszy = max(ile_podwladnych, key=ile_podwladnych.get)
print(najlepszy, ile_podwladnych[najlepszy])
4.4 — maksymalna liczba przełożonych (głębokość drzewa)
Dla pracownika liczymy, ilu ma nad sobą przełożonych: jego szef, szef jego szefa, … aż do prezesa. To „głębokość" w drzewie. Szukamy największej takiej liczby w całej firmie.
Naturalnie robi to rekurencja „w górę": głębokość(i) = 1 + głębokość(szefa i), a prezes (szef=0) ma głębokość 0. ALE przy 50 000 pracowników liczenie tego osobno dla każdego byłoby wolne (te same ścieżki liczone wielokrotnie). Dlatego memoizacja — raz policzoną głębokość zapamiętujemy w tablicy glebokosc i nie liczymy drugi raz:
glebokosc = [0] * (n + 1) # pamięć policzonych wyników
def policz(i):
if szef[i] == 0: # prezes — nie ma przełożonych
return 0
if glebokosc[i] == 0: # jeszcze nie liczone → policz i zapamiętaj
glebokosc[i] = 1 + policz(szef[i])
return glebokosc[i]
print(max(policz(i) for i in range(1, n+1)))
Zadanie 5 — Systemy liczbowe [2 pkt, papier]
Równanie z arkusza: 1440₅ + …₅ = 427₁₀ = …₃ − 110002₃ (dolny indeks = podstawa systemu). Trzeba wpisać dwie liczby w miejsca kropek.
1440₅ czytamy jako1·5³ + 4·5² + 4·5¹ + 0·5⁰ = 1·125 + 4·25 + 4·5 + 0 = 245 (w dziesiętnym).To jak w dziesiętnym, gdzie 1440 = 1·1000+4·100+4·10+0 — tylko zamiast dziesiątek mamy piątki.
Strategia: wszystko przeliczamy na zwykły system dziesiętny, rozwiązujemy jak równanie, a wynik konwertujemy z powrotem na żądany system.
Lewa równość 1440₅ + X₅ = 427₁₀ — szukamy X w systemie piątkowym:
1) zamień 1440₅ na dziesiętny: 1·125 + 4·25 + 4·5 + 0 = 245
2) z równania: 245 + X = 427 → X = 182 (dziesiętnie)
3) zamień 182 z powrotem na piątkowy (dziel przez 5, spisuj reszty):
182 : 5 = 36 r 2
36 : 5 = 7 r 1
7 : 5 = 1 r 2
1 : 5 = 0 r 1
reszty od dołu: 1 2 1 2 → X = 1212₅
sprawdzenie: 1·125+2·25+1·5+2 = 182 ✓
Prawa równość 427₁₀ = Y₃ − 110002₃ — szukamy Y w systemie trójkowym:
1) zamień 110002₃ na dziesiętny: 1·243 + 1·81 + 0 + 0 + 0 + 2 = 326
2) z równania: 427 = Y − 326 → Y = 753 (dziesiętnie)
3) zamień 753 na trójkowy (dziel przez 3, spisuj reszty):
753 : 3 = 251 r 0
251 : 3 = 83 r 2
83 : 3 = 27 r 2
27 : 3 = 9 r 0
9 : 3 = 3 r 0
3 : 3 = 1 r 0
1 : 3 = 0 r 1
reszty od dołu: 1 0 0 0 2 2 0 → Y = 1000220₃
sprawdzenie: 1·729 + 2·9 + 2·3 = 753 ✓
Zadanie 6 — Sieci [1 pkt, papier]
Adres IP to „numer" urządzenia w sieci. Są dwie wersje:
- IPv4 = 32 bity (4 bajty, np.
192.168.0.1— 4 liczby po 8 bitów). Daje ~4 mld adresów — to się skończyło. - IPv6 = 128 bitów (16 bajtów, np.
2001:0db8::1). Stworzony bo IPv4 się wyczerpał — daje praktycznie nieskończenie wiele adresów.
To czysta wiedza do zapamiętania — 32 i 128 bitów. Pewny punkt za 0 wysiłku obliczeniowego.
Zadanie 7 — Staw / rzęsa wodna [9 pkt, staw.txt, arkusz kalkulacyjny]
Staw 10 000 m². Plik staw.txt = dane pogodowe całego 2022 (Data, Temp z przecinkiem, Opady).
- 7.1 — średnie temperatury miesięczne + wykres kolumnowy. Kolumna pomocnicza z miesiącem (
=MIESIĄC(A2)), potemŚREDNIA.JEŻELIlub tabela przestawna. - 7.2 — najdłuższy ciąg kolejnych dni bez opadów w każdym miesiącu. Kolumna licznika:
=JEŻELI(Opady=0; poprzedni+1; 0), potemMAXper miesiąc.
Model do 7.3–7.4 (od 1 marca 2023, 184 dni, stałe warunki): wzrost 1,75%/noc, start 2000 m² (20%), 80 amurów × 0,25 m²/dzień = 20 m²/dzień zjadane, dodatkowo co piątek −60 m². Pomiar rano, wzrost w nocy. Rekurencja na każdy kolejny ranek:
P_jutro = (P_dziś − amury·0,25 − (piątek? 60 : 0)) × 1,0175
- 7.3 — pierwszy dzień (licząc od 1 marca), gdy rzęsa > 75% (7500 m²).
- 7.4 — najmniejsza liczba amurów, by w całym okresie rzęsa zajmowała maks. 50% (5000 m²).
ZASTĄP.Zadanie 8 — Sieć sklepów [9 pkt, SQL]
O co chodzi: trzy powiązane tabele. klienci (kto), transakcje (kto, gdzie, kiedy, który sprzedawca), opis_transakcji (co kupiono w danej transakcji — produkt, cena, ilość). Łączą się przez wspólne Id (IdKlienta, IdTransakcji).
SELECT … FROM tabela— wybierz kolumny z tabeliWHERE warunek— tylko wiersze spełniające warunekGROUP BY X— zgrupuj wiersze o tym samym X i licz na grupachCOUNT(*)= ile wierszy,SUM(…)= suma,COUNT(DISTINCT X)= ile różnych XORDER BY … DESC+LIMIT 1— posortuj malejąco i weź pierwszy (czyli „największy")JOIN B ON …— dołącz dane z drugiej tabeli po wspólnym kluczu
8.1 — klient z największą liczbą transakcji (pyta o imię i nazwisko, więc JOIN do klienci):
SELECT k.Imie, k.Nazwisko, COUNT(*) AS ile
FROM transakcje t
JOIN klienci k ON k.IdKlienta = t.IdKlienta
GROUP BY t.IdKlienta
ORDER BY ile DESC
LIMIT 1;
Czytanie: grupujemy transakcje po kliencie → COUNT(*) liczy ile transakcji ma każdy → sortujemy malejąco → bierzemy pierwszego. JOIN dokleja imię i nazwisko z tabeli klienci.
8.2 — ile kobiet i mężczyzn NIC nie kupiło:
SELECT Plec, COUNT(*) AS ilu
FROM klienci
WHERE IdKlienta NOT IN (SELECT DISTINCT IdKlienta FROM transakcje)
GROUP BY Plec;
Czytanie: wewnętrzne SELECT (podzapytanie) daje listę klientów którzy COŚ kupili. NOT IN wybiera tych, których na tej liście NIE ma (nic nie kupili). GROUP BY Plec rozbija wynik na K i M.
8.3 — ile RÓŻNYCH sklepów z kasą samoobsługową + łączna kwota (kasa samoobsługowa = puste IdSprzedawcy):
-- liczba różnych sklepów
SELECT COUNT(DISTINCT IdSklepu)
FROM transakcje WHERE IdSprzedawcy = '';
-- łączna kwota = suma (cena × ilość) z pozycji tych transakcji
SELECT SUM(o.Cena * o.Liczba)
FROM transakcje t
JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
WHERE t.IdSprzedawcy = '';
Czytanie: kwoty są w opis_transakcji (cena i ilość każdej pozycji), a info „która kasa" w transakcje — dlatego JOIN. COUNT(DISTINCT IdSklepu) liczy różne sklepy (jeden sklep może mieć wiele transakcji, ale liczymy go raz).
8.4 — sprzedawca w największej liczbie różnych sklepów W JEDNYM MIESIĄCU + ten miesiąc. Data ma format dd.mm.rrrr, więc miesiąc to znaki 4–5 (SUBSTR(data,4,2)):
SELECT IdSprzedawcy, SUBSTR(DataTransakcji, 4, 2) AS miesiac,
COUNT(DISTINCT IdSklepu) AS sklepy
FROM transakcje
WHERE IdSprzedawcy <> ''
GROUP BY IdSprzedawcy, miesiac
ORDER BY sklepy DESC
LIMIT 1;
Czytanie: grupujemy po dwóch rzeczach naraz (sprzedawca + miesiąc), bo pytanie brzmi „w jednym miesiącu". Dla każdej takiej pary liczymy ile różnych sklepów obsłużył, sortujemy i bierzemy najlepszego. <> '' odrzuca kasy samoobsługowe (brak sprzedawcy).
8.5 — produkty z kategorii „spożywcze" z opisem „do ekspresu kolbowego", które ktoś kupił (JOIN trzech tabel):
SELECT DISTINCT p.IdProduktu, p.Nazwa
FROM Produkty p
JOIN Kategorie k ON k.IdKategorii = p.IdKategorii
JOIN opis_transakcji o ON o.IdProduktu = p.IdProduktu
WHERE k.NazwaKategorii = 'spozywcze'
AND p.Opis LIKE '%do ekspresu kolbowego%';
Czytanie: łączymy produkt z jego kategorią (żeby filtrować „spożywcze") i z opisem zakupów (żeby wziąć tylko kupione). LIKE '%…%' szuka fragmentu w tekście (procenty = „cokolwiek przed i po"). DISTINCT usuwa duplikaty (produkt mógł być kupiony wiele razy).
- 8.1 → Marcelino Kruk (8 transakcji)
- 8.2 → 689 kobiet, 651 mężczyzn bez zakupów
- 8.3 → 28 różnych sklepów, łącznie 19 443,29 zł
- 8.4 → sprzedawca 14, miesiąc styczeń (01) — 3 różne sklepy
NOT IN (8.2), COUNT(DISTINCT) + SUM (8.3), grupowanie po dwóch kolumnach (8.4), JOIN 3 tabel + LIKE (8.5).Szablony wczytywania danych (Python)
# Plik TAB z nagłówkiem (klienci, transakcje, opis_transakcji, staw)
with open("klienci.txt", encoding="utf-8") as f:
naglowek = f.readline() # pomiń nagłówek
for linia in f:
pola = linia.rstrip("\n").split("\t")
# Temp z przecinkiem dziesiętnym (staw)
temp = float(pola[1].replace(",", "."))
# Puste pole (transakcje — IdSprzedawcy = kasa samoobsługowa)
if pola[4] == "":
...
# Plik spacjowy (pary)
a, b = linia.split()
# Plik jednokolumnowy (korpo)
szef.append(int(linia))
🐍 Maj 2026 — gotowy kod Python
Kompletne, uruchamialne rozwiązania zadań programistycznych z arkusza maj 2026. Każdy program czyta odpowiedni plik z folderu z danymi (Dane-NF-2605) i wypisuje odpowiedź. Wszystkie wyniki są zweryfikowane na realnych załącznikach.
.py w tym samym folderze, w którym leżą pliki danych (np. pary.txt), i odpal w terminalu: python zad3_pary.py. Zadania teoretyczne (1, 5, 6) i SQL (8) mają rozwiązania w zakładce „Maj 2026 — omówienie".
Zadanie 2 — przeniesienia (zad2.py)
Operuje wyłącznie na liczbach całkowitych (%, //), bez tablic i napisów — zgodnie z zakazem w treści.
# Zadanie 2 — liczba przeniesień przy dodawaniu pisemnym a + b
def przeniesienia(a, b):
carry = 0 # przeniesienie z poprzedniej kolumny (0 lub 1)
count = 0 # licznik przeniesień
while a > 0 or b > 0:
s = a % 10 + b % 10 + carry # suma cyfr w kolumnie + przeniesienie
if s >= 10:
carry = 1
count += 1
else:
carry = 0
a //= 10 # obetnij ostatnią cyfrę
b //= 10
return count
# przykład z arkusza
print("27732 + 72619 =>", przeniesienia(27732, 72619)) # 4
# Zadanie 2.1
print("37932 + 12528 =>", przeniesienia(37932, 12528)) # 3
print("88765 + 11111 =>", przeniesienia(88765, 11111)) # 0
print("456789 + 222222 =>", przeniesienia(456789, 222222)) # 3
Zadanie 3 — pary słów (zad3.py)
Czyta pary.txt, zapisuje odpowiedzi do wyniki3.txt (tak jak wymaga arkusz) i pokazuje je na ekranie.
# Zadanie 3 — pary słów (pary.txt)
from collections import Counter
pary = []
with open("pary.txt", encoding="utf-8") as f:
for linia in f:
a, b = linia.split()
pary.append((a, b))
# 3.1 — maks. |suma_ASCII(s1) - suma_ASCII(s2)|
def suma_ascii(s):
return sum(ord(z) for z in s)
best31 = (-1, None)
for a, b in pary:
roznica = abs(suma_ascii(a) - suma_ascii(b))
if roznica > best31[0]:
best31 = (roznica, (a, b))
# 3.2 — maks. suma wspólnych wystąpień liter: sum( min(d(x,s1), d(x,s2)) )
best32 = (-1, None)
for a, b in pary:
ca, cb = Counter(a), Counter(b)
wspolne = sum(min(ca[x], cb[x]) for x in set(a) | set(b))
if wspolne > best32[0]:
best32 = (wspolne, (a, b))
# 3.3 — pary, których najdłuższy prefiksosufiks ma >= 5 liter (obie strony)
def pref_suf(a, b):
best = 0
for k in range(1, min(len(a), len(b)) + 1):
if a[:k] == b[-k:]: # prefiks a = sufiks b
best = max(best, k)
if b[:k] == a[-k:]: # prefiks b = sufiks a
best = max(best, k)
return best
wyniki33 = [(a, b, pref_suf(a, b)) for a, b in pary if pref_suf(a, b) >= 5]
# zapis do wyniki3.txt
with open("wyniki3.txt", "w", encoding="utf-8") as out:
out.write("3.1 {} {} {}\n".format(best31[1][0], best31[1][1], best31[0]))
out.write("3.2 {} {} {}\n".format(best32[1][0], best32[1][1], best32[0]))
out.write("3.3\n")
for a, b, d in wyniki33:
out.write(" {} {} {}\n".format(a, b, d))
print("3.1:", best31[1], "->", best31[0])
print("3.2:", best32[1], "->", best32[0])
print("3.3:", len(wyniki33), "par")
Zadanie 4 — korporacja / drzewo (zad4.py)
Czyta korpo.txt (drzewo jako tablica rodziców). Memoizacja, bo 50 000 pracowników.
# Zadanie 4 — korporacja (korpo.txt)
import sys
from collections import Counter
szef = [0] # szef[i] = przełożony pracownika i (indeks 0 nieużywany)
with open("korpo.txt", encoding="utf-8") as f:
for linia in f:
szef.append(int(linia))
n = len(szef) - 1
ile_podwladnych = Counter(szef[1:]) # ile razy każdy jest czyimś szefem
# 4.2 — liście (nikt ich nie wskazuje jako szefa)
liscie = sum(1 for i in range(1, n + 1) if ile_podwladnych[i] == 0)
# 4.3 — najwięcej bezpośrednich podwładnych
ile = dict(ile_podwladnych)
ile.pop(0, None) # 0 = znacznik prezesa, pomijamy
najlepszy = max(ile, key=ile.get)
# 4.4 — maksymalna liczba przełożonych (głębokość) z memoizacją
sys.setrecursionlimit(100000)
glebokosc = [0] * (n + 1)
def policz(i):
if szef[i] == 0:
return 0
if glebokosc[i] == 0:
glebokosc[i] = 1 + policz(szef[i])
return glebokosc[i]
print("4.2 liście:", liscie)
print("4.3 najwięcej podwładnych: pracownik", najlepszy, "(", ile[najlepszy], ")")
print("4.4 maks. głębokość:", max(policz(i) for i in range(1, n + 1)))
Zadanie 7.3 / 7.4 — staw (zad7.py)
To zadanie z arkusza kalkulacyjnego, ale ten sam model w Pythonie jest świetny do weryfikacji. Skrypt sam sprawdza checkpoint 30 kwietnia (powinno wyjść 25,79% — wtedy model jest poprawny).
# Zadanie 7.3 i 7.4 — symulacja stawu
from datetime import date, timedelta
TOTAL, START, GROWTH, EAT, FRIDAY, DAYS = 10000, 2000.0, 0.0175, 0.25, 60, 184
def symulacja(amury):
P = START
d = date(2023, 3, 1)
log = []
for dzien in range(1, DAYS + 1):
log.append((dzien, d, P)) # pomiar rano
rem = amury * EAT + (FRIDAY if d.weekday() == 4 else 0) # 4 = piątek
P = (P - rem) * (1 + GROWTH) # wzrost w nocy
if P < 0:
P = 0
d += timedelta(days=1)
return log
log = symulacja(80)
po_30iv = next(P for (_, dt, P) in log if dt == date(2023, 4, 30))
print("Walidacja 30 IV:", round(po_30iv / TOTAL * 100, 2), "% (arkusz: 25,79%)")
# 7.3 — pierwszy dzień > 75%
for dzien, dt, P in log:
if P > 0.75 * TOTAL:
print("7.3: dzień", dzien, "(", dt, ",", round(P / TOTAL * 100, 2), "% )")
break
# 7.4 — najmniejsza liczba amurów, by zarost <= 50% przez cały okres
def max_zarost(amury):
return max(P for (_, _, P) in symulacja(amury))
a = 80
while max_zarost(a) > 0.50 * TOTAL:
a += 1
print("7.4: min amurów =", a, "( max", round(max_zarost(a) / TOTAL * 100, 2), "% )")
Zadanie 8 — SQL (gotowe zapytania)
Zadanie 8 robi się w bazie (SQLite/MySQL), nie w Pythonie. Gotowe zapytania:
-- 8.1: klient z największą liczbą transakcji (imię i nazwisko)
SELECT k.Imie, k.Nazwisko, COUNT(*) AS ile
FROM transakcje t JOIN klienci k ON k.IdKlienta = t.IdKlienta
GROUP BY t.IdKlienta ORDER BY ile DESC LIMIT 1;
-- 8.2: ile K i M nic nie kupiło
SELECT Plec, COUNT(*) FROM klienci
WHERE IdKlienta NOT IN (SELECT DISTINCT IdKlienta FROM transakcje)
GROUP BY Plec;
-- 8.3: ile różnych sklepów z kasą samoobsługową + łączna kwota
SELECT COUNT(DISTINCT IdSklepu) FROM transakcje WHERE IdSprzedawcy = '';
SELECT SUM(o.Cena * o.Liczba)
FROM transakcje t JOIN opis_transakcji o ON o.IdTransakcji = t.IdTransakcji
WHERE t.IdSprzedawcy = '';
-- 8.4: sprzedawca w najwięcej różnych sklepach w jednym miesiącu
SELECT IdSprzedawcy, SUBSTR(DataTransakcji,4,2) AS miesiac,
COUNT(DISTINCT IdSklepu) AS sklepy
FROM transakcje WHERE IdSprzedawcy <> ''
GROUP BY IdSprzedawcy, miesiac ORDER BY sklepy DESC LIMIT 1;
-- 8.5: produkty spożywcze "do ekspresu kolbowego", które kupiono
SELECT DISTINCT p.IdProduktu, p.Nazwa
FROM Produkty p
JOIN Kategorie k ON k.IdKategorii = p.IdKategorii
JOIN opis_transakcji o ON o.IdProduktu = p.IdProduktu
WHERE k.NazwaKategorii = 'spozywcze'
AND p.Opis LIKE '%do ekspresu kolbowego%';
📘 Maj 2026 — Zadanie 2 (dodawanie): pełny przewodnik
Wyczerpujący przewodnik do Zadania 2 z arkusza maj 2026 — od intuicji „co to jest przeniesienie" po wyprowadzenie algorytmu CKE i ćwiczenia z odpowiedziami. Materiał do przejścia z uczniem od deski do deski.
Wprowadzenie
Zadanie 2 z arkusza maja 2026 jest "papierowe" tylko z pozoru. CKE testuje, czy potrafisz rozłożyć szkolną mechanikę dodawania na cyfry i zapisać ją jako pętlę. Pod spodem to ten sam mechanizm, który procesor wykonuje miliardy razy na sekundę w pełnym sumatorze bitowym. Jeśli zrozumiesz przeniesienie w systemie dziesiętnym, to samo zrozumiesz w binarnym, szesnastkowym i każdym innym.
Dodawanie pisemne w trzech zdaniach
Ustawiasz liczby jedna pod drugą tak, żeby cyfry jedności były w tej samej kolumnie. Dodajesz kolumna po kolumnie od prawej do lewej. Jeżeli suma cyfr w danej kolumnie wynosi 10 lub więcej, zapisujesz pod kreską tylko jej ostatnią cyfrę (czyli suma mod 10), a 1 przekazujesz w lewo do następnej kolumny. Ta przekazana jedynka to właśnie przeniesienie (ang. carry).
Mikroprzykład 1: 47 + 8
Przeniesienie: 1
Liczba a: 4 7
Liczba b: + 8
-----
Wynik: 5 5
Kolumna jedności: 7 + 8 = 15. Pod kreską piszemy 5, a 1 wędruje w lewo. Kolumna dziesiątek: 4 + 0 + 1 (przeniesienie) = 5. Mamy 1 przeniesienie.
Mikroprzykład 2: 56 + 44
Przeniesienie: 1 1
Liczba a: 5 6
Liczba b: + 4 4
-----
Wynik: 1 0 0
Jedności: 6 + 4 = 10, piszemy 0, przeniesienie 1. Dziesiątki: 5 + 4 + 1 = 10, piszemy 0, przeniesienie 1. Trafia w pustą setkę, gdzie nie ma już żadnych cyfr do dodania, więc 1 ląduje jako nowa cyfra wyniku. Mamy 2 przeniesienia.
Dlaczego CKE w ogóle to liczy
Dodawanie pisemne to najprostsza wersja pełnego sumatora, który na poziomie sprzętu składa się z bramek logicznych. Przeniesienie w dziesiętnym odpowiada carry bit w binarnym i to ono jest źródłem integer overflow, gdy suma dwóch int32 przekracza pojemność rejestru. Każdy, kto pisał kiedyś kryptografię, arytmetykę dużych liczb albo debugował overflow w C, robił to samo co Ty w tym zadaniu, tylko na bitach.
123 + 456), może być równa liczbie cyfr (gdy każda kolumna przekracza, np. 999 + 999), a w skrajnym przypadku może być o jeden większa niż liczba cyfr każdego ze składników, jeśli ostatnie przeniesienie wytworzy nową, dodatkową cyfrę wyniku (jak 56 + 44 = 100). Dla pary a, b o tej samej liczbie cyfr n, liczba przeniesień zawsze mieści się w przedziale 0..n.
1 do przekazania w lewo, a nie ile cyfr ma końcowy wynik, nie ile jedynek widać na samej górze ilustracji, nie suma wartości przeniesień. Liczymy zdarzenia, nie cyfry.
Przyklad krok po kroku: 27732 + 72619
Dodawanie pisemne wykonujemy kolumna po kolumnie, idąc od prawej do lewej. W każdej kolumnie liczymy sumę: cyfra z liczby a, cyfra z liczby b oraz przeniesienie wniesione z kolumny poprzedniej (carry in). Jeśli suma w kolumnie jest większa lub równa 10, generujemy przeniesienie do kolumny następnej (carry out = 1) i zapisujemy cyfrę wynikową suma mod 10. W przeciwnym wypadku carry out = 0, a cyfrą wynikową jest cała suma.
Numerujemy kolumny od prawej: kolumna 1 to jedności, kolumna 2 to dziesiątki, i tak dalej. Litera P w ostatniej kolumnie tabeli oznacza, że w tej kolumnie powstało przeniesienie do kolumny następnej.
kol | a | b | c_in | suma | cyfra | c_out | P?
-----+-----+-----+------+------+-------+-------+----
1 | 2 | 9 | 0 | 11 | 1 | 1 | P
2 | 3 | 1 | 1 | 5 | 5 | 0 | -
3 | 7 | 6 | 0 | 13 | 3 | 1 | P
4 | 7 | 2 | 1 | 10 | 0 | 1 | P
5 | 2 | 7 | 1 | 10 | 0 | 1 | P
-----+-----+-----+------+------+-------+-------+----
razem P = 4
Sklejając cyfry wynikowe od lewej (z dopisanym ostatnim carry out jako najstarszą cyfrą wyniku) dostajemy: 1 0 0 3 5 1, czyli 100351. Zgadza się z arytmetyką: 27732 + 72619 = 100351.
Liczymy wiersze oznaczone literą P: kolumna 1, 3, 4 oraz 5. Daje to dokładnie 4 przeniesienia, zgodnie z odpowiedzią z arkusza.
i > 1 zachodzi tożsamość:
carry_in(i) = carry_out(i - 1)
Przeniesienie wyjściowe z kolumny poprzedniej staje się przeniesieniem wejściowym do kolumny bieżącej. Dla kolumny pierwszej (jedności) zawsze carry_in(1) = 0. Ta zależność jest fundamentem algorytmu z zadania 2.2: w pętli wystarczy pamiętać jedną zmienną c (bieżące przeniesienie), nadpisywać ją w każdym kroku wartością carry_out i zliczać, ile razy przyjmie wartość 1 jako wyjście z kolumny.
Zadanie 2.1 - pelne rozwiazania
Przeniesienie powstaje w kolumnie wtedy, gdy suma cyfr tej kolumny powiększona o przeniesienie z kolumny poprzedniej jest większa lub równa 10. Sprawdzamy każdą kolumnę osobno, idąc od prawej do lewej.
s = a[i] + b[i] + carry_in. Jeśli s >= 10, to carry_out = 1 (i zwiększamy licznik przeniesień), w przeciwnym razie carry_out = 0.
Wiersz 1: 37932 + 12528 (przykład z arkusza)
Kolumna: 5 4 3 2 1
Przeniesienie: 1 1 1
a: 3 7 9 3 2
b: + 1 2 5 2 8
-----------------
Wynik: 5 0 4 6 0
Kolumna 1 (jednosci): 2 + 8 + 0 = 10 -> wynik 0, carry 1 [PRZENIESIENIE]
Kolumna 2 (dziesiatki): 3 + 2 + 1 = 6 -> wynik 6, carry 0
Kolumna 3 (setki): 9 + 5 + 0 = 14 -> wynik 4, carry 1 [PRZENIESIENIE]
Kolumna 4 (tysiace): 7 + 2 + 1 = 10 -> wynik 0, carry 1 [PRZENIESIENIE]
Kolumna 5 (dz. tys.): 3 + 1 + 1 = 5 -> wynik 5, carry 0
Wiersz 2: 88765 + 11111
Kolumna: 5 4 3 2 1
Przeniesienie: - - - -
a: 8 8 7 6 5
b: + 1 1 1 1 1
-----------------
Wynik: 9 9 8 7 6
Kolumna 1 (jednosci): 5 + 1 + 0 = 6 -> wynik 6, carry 0
Kolumna 2 (dziesiatki): 6 + 1 + 0 = 7 -> wynik 7, carry 0
Kolumna 3 (setki): 7 + 1 + 0 = 8 -> wynik 8, carry 0
Kolumna 4 (tysiace): 8 + 1 + 0 = 9 -> wynik 9, carry 0
Kolumna 5 (dz. tys.): 8 + 1 + 0 = 9 -> wynik 9, carry 0
Cyfry liczby b to same jedynki, a cyfry liczby a w odpowiadających kolumnach wynoszą maksymalnie 8, więc żadna suma kolumnowa nie osiąga 10.
Wiersz 3: 456789 + 222222
Kolumna: 6 5 4 3 2 1
Przeniesienie: 1 1 1
a: 4 5 6 7 8 9
b: + 2 2 2 2 2 2
---------------------
Wynik: 6 7 9 0 1 1
Kolumna 1 (jednosci): 9 + 2 + 0 = 11 -> wynik 1, carry 1 [PRZENIESIENIE]
Kolumna 2 (dziesiatki): 8 + 2 + 1 = 11 -> wynik 1, carry 1 [PRZENIESIENIE]
Kolumna 3 (setki): 7 + 2 + 1 = 10 -> wynik 0, carry 1 [PRZENIESIENIE]
Kolumna 4 (tysiace): 6 + 2 + 1 = 9 -> wynik 9, carry 0
Kolumna 5 (dz. tys.): 5 + 2 + 0 = 7 -> wynik 7, carry 0
Kolumna 6 (setki tys.): 4 + 2 + 0 = 6 -> wynik 6, carry 0
Trzy najmłodsze kolumny generują przeniesienie kaskadowo: jednostki dają 11, dziesiątki z carry dają 11, setki z carry dają 10. Kolumna tysięcy (6 + 2 + 1 = 9) już nie przekracza progu, więc kaskada się urywa.
Tabela zbiorcza odpowiedzi
| a | b | a + b | Liczba przeniesień |
|---|---|---|---|
| 37932 | 12528 | 50460 | 3 |
| 88765 | 11111 | 99876 | 0 |
| 456789 | 222222 | 679011 | 3 |
7 + 2 = 9) nie dałaby przeniesienia, daje je dopiero carry wpadające z dziesiątek (7 + 2 + 1 = 10). Zawsze licz od prawej do lewej i przekazuj carry w górę.
a wynosi 8, a wszystkie cyfry liczby b to 1, to maksymalna suma kolumnowa wynosi 8 + 1 = 9. Bez wejściowego carry nigdy nie powstanie nowe carry, stąd 0 przeniesień bez konieczności rozpisywania każdej kolumny. Tę intuicję warto mieć w głowie jako szybką weryfikację wyniku.
Zadanie 2.2 - wyprowadzenie algorytmu
Zadanie wymaga napisania algorytmu, który dla dwóch dodatnich liczb całkowitych a i b o tej samej liczbie cyfr policzy, ile razy przy dodawaniu pisemnym pojawi się przeniesienie. Wynik dla 27732 + 72619 to 4. Zanim napiszemy choćby linię kodu, musimy precyzyjnie wiedzieć, czego nam wolno użyć, bo CKE w 2026 mocno zacieśnił dozwolony zestaw operacji.
1. Zakazy CKE
Wolno użyć wyłącznie: operatorów arytmetycznych +, -, *, /, div, mod, porównań, operatorów logicznych, instrukcji sterujących (if, while, for), przypisania oraz własnych funkcji pomocniczych.
Nie wolno użyć: tablic, list, słowników, łańcuchów znaków, funkcji konwersji str() / int(), indeksowania n[i], bibliotek, operatorów bitowych. Każda próba "zamienię liczbę na string i polecę po znakach" daje 0 punktów.
2. Dozwolony zestaw - szybka sciaga
| Kategoria | Co wolno |
|---|---|
| Arytmetyka | + - * /, div (czyli //), mod (czyli %) |
| Porównania | = < > <= >= != |
| Logiczne | and, or, not |
| Sterujące | if, else, while, for |
| Inne | przypisanie <- / =, własne funkcje pomocnicze |
3. Klucz calego zadania - jak wyciagnac cyfre arytmetycznie
Skoro nie wolno traktować liczby jako napisu, jedyna droga do pojedynczej cyfry prowadzi przez dwie operacje:
n mod 10- zwraca ostatnią cyfrę liczby (np.27732 mod 10 = 2)n div 10- obcina ostatnią cyfrę (np.27732 div 10 = 2773)
Łącząc te dwa kroki w pętli, "zjadamy" liczbę od prawej do lewej, dokładnie tak, jak przebiega dodawanie pisemne. Demo na n = 27732:
krok | n | n mod 10 | n div 10
-----+-------+----------+---------
1 | 27732 | 2 | 2773
2 | 2773 | 3 | 277
3 | 277 | 7 | 27
4 | 27 | 7 | 2
5 | 2 | 2 | 0 <- koniec, n stalo sie 0
Pięć kroków, pięć cyfr. Zgadza się. Warunek stopu: pętla działa, dopóki n > 0.
4. Pseudokod naturalny - szkic logiki
Inwariant: w każdej iteracji bierzemy ostatnią cyfrę a, ostatnią cyfrę b, dodajemy do nich aktualne przeniesienie c (0 albo 1). Jeśli suma >= 10, generujemy nowe przeniesienie i zwiększamy licznik. Następnie obcinamy obie liczby przez div 10 i lecimy z następną kolumną.
zaczynamy z carry = 0 i count = 0
dopoki sa jeszcze cyfry do przerobienia:
suma = ostatnia cyfra a + ostatnia cyfra b + carry
jezeli suma >= 10:
nowy carry = 1
count = count + 1
w przeciwnym razie:
nowy carry = 0
obetnij ostatnia cyfre a
obetnij ostatnia cyfre b
zwroc count
5. Pelna implementacja w Pythonie (wersja while)
def przeniesienia(a, b):
carry = 0 # aktualne przeniesienie (0 albo 1)
count = 0 # licznik przeniesien
while a > 0 or b > 0: # patrz nizej: dlaczego "or"
s = a % 10 + b % 10 + carry # suma w aktualnej kolumnie
if s >= 10:
carry = 1 # generujemy przeniesienie
count = count + 1
else:
carry = 0
a = a // 10 # obcinamy ostatnia cyfre a
b = b // 10 # obcinamy ostatnia cyfre b
return count
# Testy
print(przeniesienia(27732, 72619)) # 4
print(przeniesienia(88765, 11111)) # 0
print(przeniesienia(456789, 222222)) # 3
Dlaczego a > 0 or b > 0, a nie and? Zadanie 2.2 gwarantuje równą liczbę cyfr, więc or i and dadzą tu ten sam efekt. Ale or jest bezpieczniejsze i uniwersalniejsze, działa też wtedy, gdy z powodu przeniesień jedna liczba "skończy się" wcześniej niż druga (czego de facto nie ma w 2.2, ale jest np. gdy a = 5, b = 95). and zatrzymałoby pętlę zbyt wcześnie. Egzaminator za or nie odejmie punktu, za and przy nierównych długościach odejmie.
6. Alternatywa - wersja for wykorzystujaca zalozenie "ta sama liczba cyfr"
Skoro arkusz gwarantuje równą długość, możemy najpierw policzyć cyfry i przelecieć pętlę for ustaloną liczbę razy. Wygląda elegancko i jasno zdradza egzaminatorowi, że rozumiemy specyfikację.
def liczba_cyfr(n):
k = 0
while n > 0:
k = k + 1
n = n // 10
return k
def przeniesienia_for(a, b):
k = liczba_cyfr(a) # = liczba_cyfr(b) z zalozenia zadania
carry = 0
count = 0
for _ in range(k):
s = a % 10 + b % 10 + carry
if s >= 10:
carry = 1
count = count + 1
else:
carry = 0
a = a // 10
b = b // 10
return count
print(przeniesienia_for(27732, 72619)) # 4
Obie wersje są poprawne i zdobywają komplet 4 punktów. Wersja while jest krótsza i bardziej uniwersalna. Polecam ją na maturze, jeśli nie czujesz się pewnie w pisaniu funkcji pomocniczej liczba_cyfr.
7. Pseudokod CKE - wersja jezyk-niezalezna
Jeśli wolisz oddać pseudokod (CKE akceptuje), zapisz tak:
funkcja przeniesienia(a, b):
c <- 0
p <- 0
dopoki a > 0:
s <- a mod 10 + b mod 10 + c
jezeli s >= 10:
c <- 1
p <- p + 1
w przeciwnym razie:
c <- 0
a <- a div 10
b <- b div 10
zwroc p
Warunek dopoki a > 0 wystarcza, bo z założenia a i b mają tę samą liczbę cyfr. Gdy a stanie się zerem, b też. Na egzaminie z 2.2 to jest formalnie poprawne.
Zlozonosc i kontrola
| Parametr | Wartość |
|---|---|
| Liczba iteracji | k - liczba cyfr (np. dla 5-cyfrowej liczby: 5 obrotów pętli) |
| Złożoność czasowa | O(k) = O(log10 a) |
| Złożoność pamięciowa | O(1) - tylko trzy zmienne: carry, count, s |
| Brak struktur | żadnej tablicy, żadnego napisu - zgodne z zakazem CKE |
Punktacja CKE (4 pkt): 1 pkt za poprawne ekstraktowanie cyfry przez mod 10 i div 10, 1 pkt za prawidłową obsługę przeniesienia carry, 1 pkt za warunek s >= 10 i inkrementację licznika, 1 pkt za poprawny warunek stopu pętli. Jakakolwiek tablica, str() albo indeksowanie - minus wszystkie 4 punkty.
Pulapki i blednowanie CKE
Zadanie 2.2 wyglada na trywialne: pobierasz cyfry, sumujesz, sledzisz carry. W praktyce CKE rok w rok punktuje ten typ zadania ponizej oczekiwan, bo zdajacy potykaja sie o szczegoly mechaniki dodawania pisemnego, a nie o sam algorytm. Ponizej siedem konkretnych pulapek z kodem ZLY/POPRAWNY i komentarzem do czego prowadza.
27732+72619=4, a polegnie na 99999+11111, tracisz 2-3 z 4 punktow.
1. Carry niezerowane w iteracji bez przeniesienia
Klasyk. Zdajacy ustawia carry := 1 przy przeniesieniu, ale zapomina ustawic carry := 0, gdy go nie ma. W efekcie raz wlaczony carry zostaje na zawsze i licznik p rosnie w kazdej kolejnej iteracji.
# ZLY - carry nie wraca do 0
carry = 0
while a > 0 or b > 0:
s = a % 10 + b % 10 + carry
if s >= 10:
carry = 1
p = p + 1
# brak else: carry = 0 -- BLAD
a = a // 10
b = b // 10
# POPRAWNY
carry = 0
while a > 0 or b > 0:
s = a % 10 + b % 10 + carry
if s >= 10:
carry = 1
p = p + 1
else:
carry = 0
a = a // 10
b = b // 10
Koszt: -2 pkt (algorytm dziala dla losowych liczb, ale dla 12+10 zwroci wiecej przeniesien niz jest).
2. Warunek petli and zamiast or
Specyfikacja mowi "o tej samej liczbie cyfr", wiec teoretycznie and wystarczy. Ale wystarczy, ze zdajacy testuje 99+1 (rozna liczba cyfr) i widzi, ze algorytm sie urywa zanim policzy ostatnie przeniesienie. or jest nawykiem bezpieczniejszym i CKE w kluczu odpowiedzi zwykle akceptuje oba, jednak preferuje or.
# Ryzykowne (formalnie OK dla zalozenia, ale lamie sie poza nim)
while a > 0 and b > 0:
...
# Bezpieczne
while a > 0 or b > 0:
...
3. if s > 10 zamiast if s >= 10
Najczestszy off-by-one w tym zadaniu. Suma 5+5=10 to JUZ przeniesienie w dodawaniu pisemnym: zapisujesz 0, w pamieci masz 1. Jezeli warunkiem jest s > 10, wszystkie pary cyfr sumujace sie dokladnie do 10 sa pomijane.
Test: 55 + 55
1 1 <- powinno byc carry z obu kolumn
5 5
+ 5 5
-----
1 1 0
przeniesienia = 2
Algorytm z s > 10 zwroci 0, a poprawnie 2. Koszt: -2 pkt.
4. Niedoliczenie carry IN do sumy biezacej kolumny
Klasyczny przypadek 9+0=9, ale jezeli z poprzedniej kolumny przyszedl carry=1, to faktyczna suma to 10 i jest kolejne przeniesienie. Zdajacy, ktory liczy tylko a%10 + b%10 bez doliczania carry, gubi calego lancucha przeniesien.
# ZLY - carry ignorowany w warunku
s = a % 10 + b % 10
if s >= 10:
p = p + 1
carry = 1
else:
carry = 0
# POPRAWNY
s = a % 10 + b % 10 + carry
if s >= 10:
p = p + 1
carry = 1
else:
carry = 0
Przyklad demaskujacy blad: 199+101. Bez carry IN algorytm zwroci 1, poprawnie 2.
5. Zla kolejnosc operacji na cyfrze
Jezeli najpierw a := a div 10, a dopiero potem a mod 10, czytasz nie te cyfre. Klasyczny blad copy-paste.
# ZLY
a = a // 10 # ucinasz najmlodsza cyfre
cyfra_a = a % 10 # czytasz JUZ druga od konca, nie ostatnia
# POPRAWNY
cyfra_a = a % 10 # najpierw odczytaj
a = a // 10 # potem skroc
Efekt: algorytm w ogole nie patrzy na ostatnia kolumne, zaczyna od przedostatniej. Koszt: typowo -3 pkt, bo wynik jest losowo zly.
6. Zakazane konstrukcje: str, len, listy
Specyfikacja explicit zabrania konwersji napis-liczba, tablic, list i funkcji wlasnych poza dozwolonymi operatorami. Rozwiazanie typu:
# ZAKAZANE - 0 pkt mimo poprawnego wyniku
sa = str(a)
sb = str(b)
n = len(sa)
for i in range(n-1, -1, -1):
...
CKE traktuje to jako niespelnienie warunkow zadania i zeruje 2.2 nawet jezeli wynik dla przykladu jest poprawny. To NIE jest negocjowalne, egzaminator nie ma marginesu interpretacyjnego.
7. Carry na koncu, gdy nie ma juz cyfr do dodania
Przyklad: 9+1. Ostatnia (i jedyna) kolumna: 9+1=10, p=1. Po tej iteracji a=0, b=0, carry=1. Niektorzy zdajacy mysla "to przeniesienie wystaje poza liczbe, nie liczy sie" - to blad interpretacyjny. CKE jasno definiuje: kazde wystapienie sytuacji "suma >= 10 w danej kolumnie" to jedno przeniesienie, niezaleznie czy ostatecznie wystaje poza liczbe wynikowa.
1
9
+ 1
---
1 0 <- carry koncowy istnieje, ale p liczy SIE +1 za 9+1
Algorytm nie musi specjalnie obslugiwac "carry koncowego", wystarczy ze warunek s >= 10 zliczy go w iteracji ostatniej cyfry. Pulapka pojawia sie wtedy, gdy zdajacy dorzuca dodatkowy if carry == 1 then p := p - 1 "zeby skompensowac wystajacy carry". To czysty blad logiczny i -1 pkt.
Jak CKE punktuje 2.2 (4 pkt)
Klucz odpowiedzi CKE rozbija 4 punkty zwykle nastepujaco: 1 pkt za poprawna inicjalizacje (p=0, carry=0) i zgodnosc ze specyfikacja Dane/Wynik; 1 pkt za poprawna petle iterujaca po cyfrach z uzyciem div i mod; 1 pkt za poprawne wyliczenie sumy z uwzglednieniem carry IN i warunek >= 10; 1 pkt za poprawna aktualizacje carry (zarowno ustawienie na 1, jak i powrot do 0) oraz dzialanie algorytmu dla przykladu 27732+72619=4. Uzycie zakazanej konstrukcji (str, len, lista, tablica, wlasna funkcja, operator inny niz dozwolone) zeruje cale zadanie 2.2 niezaleznie od poprawnosci logiki - egzaminator nie zaglada w wynik, tylko skresla. Drobny blad logiczny (np. > zamiast >=) kosztuje 1-2 pkt zaleznie od tego, czy algorytm dziala dla podanego przykladu z trescia zadania.
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.
cyfra_a + cyfra_b + przeniesienie_z_poprzedniej_kolumny >= 10. Liczymy od najmlodszej cyfry (prawej) do najstarszej (lewej).
5749 + 3451. Narysuj slup z przeniesieniami.
Pokaz odpowiedz
Odpowiedz: 3 przeniesienia.
Przeniesienie: 1 1 1
Liczba a: 5 7 4 9
Liczba b: + 3 4 5 1
---------
Wynik: 9 2 0 0
Kolumna jednosci: 9 + 1 + 0 = 10 -> wpis 0, carry 1 [PRZENIESIENIE]
Kolumna dziesiatek: 4 + 5 + 1 = 10 -> wpis 0, carry 1 [PRZENIESIENIE]
Kolumna setek: 7 + 4 + 1 = 12 -> wpis 2, carry 1 [PRZENIESIENIE]
Kolumna tysiecy: 5 + 3 + 1 = 9 -> wpis 9, carry 0
Razem przeniesien: 3
Wynik koncowy: 9200
Uwaga: w arkuszu maturalnym istotne sa przeniesienia ktore powstaja podczas dodawania, nie to czy na koniec dostaniesz dodatkowa cyfre z lewej. Tu carry z tysiecy wynosi 0, wiec wynik ma 4 cyfry - liczymy 3 przeniesienia w trzech najmlodszych kolumnach.
19999 + 80001. To klasyczna "lawina carry": zwroc uwage na to, jak jedno przeniesienie z najmlodszej kolumny propaguje sie przez cala liczbe.
Pokaz odpowiedz
Odpowiedz: 5 przeniesien.
Przeniesienie: 1 1 1 1 1
Liczba a: 1 9 9 9 9
Liczba b: + 8 0 0 0 1
-----------
Wynik: 1 0 0 0 0 0
Kolumna 1 (jednosci): 9 + 1 + 0 = 10 -> carry 1
Kolumna 2 (dziesiatki): 9 + 0 + 1 = 10 -> carry 1
Kolumna 3 (setki): 9 + 0 + 1 = 10 -> carry 1
Kolumna 4 (tysiace): 9 + 0 + 1 = 10 -> carry 1
Kolumna 5 (dziesiatki tys): 1 + 8 + 1 = 10 -> carry 1
Razem: 5 przeniesien
Wynik: 100000 (szesciocyfrowy, bo ostatnie carry "wyszlo" w nowa kolumne)
9999) i dodaj 1: przeniesienie idzie przez wszystkie pozycje. Tutaj 19999 + 80001 robi to samo zjawisko, tylko z innymi cyframi startowymi.
Pokaz odpowiedz
Regula jest prosta: w kazdej kolumnie suma cyfr musi byc <= 9. Wtedy zaden carry nie powstaje na zadnej pozycji.
Przyklad poprawny: 1234 + 5432.
Przeniesienie: 0 0 0 0
Liczba a: 1 2 3 4
Liczba b: + 5 4 3 2
---------
Wynik: 6 6 6 6
Kolumna jednosci: 4 + 2 = 6 (<= 9, brak carry)
Kolumna dziesiatek: 3 + 3 = 6 (<= 9, brak carry)
Kolumna setek: 2 + 4 = 6 (<= 9, brak carry)
Kolumna tysiecy: 1 + 5 = 6 (<= 9, brak carry)
Przeniesien: 0
Inne poprawne odpowiedzi: 1111 + 1111 = 2222, 2222 + 3333 = 5555, 4321 + 5432 = 9753 (kolumna jednosci 1+2=3, dziesiatek 2+3=5, setek 3+4=7, tysiecy 4+5=9 - wszystko <= 9).
Anty-przyklad: 1234 + 5678 - kolumna jednosci 4+8=12, juz carry. NIE spelnia.
Pokaz odpowiedz
Trzy przeniesienia w liczbach 3-cyfrowych oznaczaja, ze carry powstaje na kazdej z trzech pozycji (jednosci, dziesiatki, setki). To wymaga zeby suma w kazdej kolumnie (z uwzglednieniem przychodzacego carry) byla >= 10.
Najprostsza droga: zbudowac "pelna lawine" jak 555 + 555 lub 999 + 111.
Przyklad A: 555 + 555
Przeniesienie: 1 1 1
Liczba a: 5 5 5
Liczba b: + 5 5 5
---------
Wynik: 1 1 1 0
Kolumna jednosci: 5 + 5 + 0 = 10 -> carry 1
Kolumna dziesiatek: 5 + 5 + 1 = 11 -> carry 1
Kolumna setek: 5 + 5 + 1 = 11 -> carry 1
Razem: 3 przeniesienia, wynik 1110
Przyklad B: 999 + 111
Przeniesienie: 1 1 1
Liczba a: 9 9 9
Liczba b: + 1 1 1
---------
Wynik: 1 1 1 0
Kolumna jednosci: 9 + 1 + 0 = 10 -> carry 1
Kolumna dziesiatek: 9 + 1 + 1 = 11 -> carry 1
Kolumna setek: 9 + 1 + 1 = 11 -> carry 1
Razem: 3 przeniesienia, wynik 1110
Zwroc uwage: po 3 przeniesieniach w liczbach 3-cyfrowych wynik ZAWSZE ma 4 cyfry (bo carry z najstarszej kolumny tworzy nowa pozycje z lewej).
a + b wiekszej niz w zadaniu 4 (gdzie bylo 555+555 = 1110, suma cyfr 5+5+5+5+5+5 = 30, albo 999+111 = 1110, suma cyfr 9+9+9+1+1+1 = 30). Cel: liczba bedzie wieksza od 1110, ale ZADNEJ kolumny nie przepelnia.
Pokaz odpowiedz
Pomysl: chcemy uniknac carry (kazda kolumna <= 9), ale chcemy uzyc cyfr ktore w sumie daja duzo. Maksymalna suma cyfr w jednej kolumnie bez carry to 9 (np. 4+5, 9+0, 3+6). Wiec dla 4-cyfrowych liczb maksymalna suma cyfr a+b bez zadnego carry wynosi 9 * 4 = 36.
Przyklad ktory tego dosiega:
Liczba a: 9 0 9 0
Liczba b: 0 9 0 9
---------
Wynik: 9 9 9 9
Kolumna jednosci: 0 + 9 = 9 (brak carry)
Kolumna dziesiatek: 9 + 0 = 9 (brak carry)
Kolumna setek: 0 + 9 = 9 (brak carry)
Kolumna tysiecy: 9 + 0 = 9 (brak carry)
Przeniesien: 0
Suma cyfr a + b = 9+0+9+0 + 0+9+0+9 = 36
Wynik liczbowy: 9999 (znacznie wiecej niz 1110 z zadania 4)
9999 (wynik 9090+0909) jest 9x wieksze od 1110 (555+555), a powstalo bez ani jednego carry. Przeniesienia opisuja strukture cyfr, nie wielkosc.
Pokaz odpowiedz
Idea: w systemie dziesietnym carry powstaje, gdy suma w kolumnie >= 10. W systemie binarnym (podstawa 2) carry powstaje, gdy suma w kolumnie >= 2. Wynik kolumny to suma mod 2, a carry to suma div 2 (zawsze 0 albo 1, bo max suma to 1 + 1 + 1 = 3).
Wystarczy w algorytmie zamienic kazde wystapienie 10 na 2:
def liczba_przeniesien_bin(a, b):
p = 0 # licznik przeniesien
c = 0 # carry z poprzedniej pozycji
while a > 0 or b > 0 or c > 0:
bit_a = a % 2 # cyfra binarna a
bit_b = b % 2 # cyfra binarna b
s = bit_a + bit_b + c # suma w kolumnie
if s >= 2:
c = 1
p = p + 1
else:
c = 0
a = a // 2
b = b // 2
return p
# Test: 0b1011 + 0b0111 = 11 + 7 = 18 = 0b10010
# Kolumny od prawej:
# 1 + 1 + 0 = 2 -> bit 0, carry 1 (1. przeniesienie)
# 1 + 1 + 1 = 3 -> bit 1, carry 1 (2. przeniesienie)
# 0 + 1 + 1 = 2 -> bit 0, carry 1 (3. przeniesienie)
# 1 + 0 + 1 = 2 -> bit 0, carry 1 (4. przeniesienie)
# 0 + 0 + 1 = 1 -> bit 1, carry 0
# Razem: 4 przeniesienia
print(liczba_przeniesien_bin(0b1011, 0b0111)) # -> 4
W pseudokodzie maturalnym (z dozwolonym mod i div):
funkcja LiczbaPrzeniesienBin(a, b):
p <- 0
c <- 0
dopoki a > 0 lub b > 0 lub c > 0:
bit_a <- a mod 2
bit_b <- b mod 2
s <- bit_a + bit_b + c
jezeli s >= 2:
c <- 1
p <- p + 1
w przeciwnym razie:
c <- 0
a <- a div 2
b <- b div 2
zwroc p
B >= 2: wystarczy zamienic kazde 10 (w wersji dziesietnej) lub 2 (w wersji binarnej) na B, a mod 10 / div 10 na mod B / div B. To pokazuje, ze "przeniesienie" jest cecha pozycyjnego systemu liczbowego, nie konkretnej podstawy.
a > 0 lub b > 0 lub c > 0 automatycznie obsluguje rozne dlugosci). To rozszerzenie poza wymagania arkusza, pokazuje, ze rozumiesz problem glebiej niz tylko skopiowanie wzorca.
Podsumowanie
Z tego zadania uczen wynosi trzy rzeczy operacyjne: ekstrakcja cyfr przez mod 10 i div 10 (jedyna dozwolona droga przy zakazie str()), nawyk przekazywania carry jako jednej zmiennej skalarnejnadpisywanej w petli oraz dyscypline warunku s >= 10 z obowiazkowym else: carry = 0. Te trzy elementy ukladaja sie w wzorzec "per-pozycja w systemie pozycyjnym", ktory na maturze wraca w kazdym zadaniu o liczbach: konwersjach miedzy systemami (Z2/Z10/Z16), liczeniu sumy cyfr, palindromach liczbowych, kontrolnym NIP/PESEL, mnozeniu pisemnym. Umiejetnosc "iteruj cyfra po cyfrze bez zamiany na napis" jest transferowalna 1:1 i odroznia 4 punkty od 0 punktow nie tylko w zadaniu 2.
1. Systemy liczbowe
Na maturze z informatyki systemy liczbowe pojawiają się prawie co rok — konwersje, arytmetyka, porównania. Musisz umieć szybko przeliczać między BIN, OCT, DEC i HEX.
Czym jest system liczbowy?
DEC (10) → 0-9 · BIN (2) → 0-1 · OCT (8) → 0-7 · HEX (16) → 0-9, A-F
| System | Podstawa | Cyfry | Przykład |
|---|---|---|---|
| Binarny (BIN) | 2 | 0, 1 | 1011₂ = 11₁₀ |
| Ósemkowy (OCT) | 8 | 0–7 | 13₈ = 11₁₀ |
| Dziesiętny (DEC) | 10 | 0–9 | 11₁₀ |
| Szesnastkowy (HEX) | 16 | 0–9, A–F | B₁₆ = 11₁₀ |
Konwersja DEC → BIN (dzielenie przez 2)
156₁₀ → BIN:
156 ÷ 2 = 78 reszta 0
78 ÷ 2 = 39 reszta 0
39 ÷ 2 = 19 reszta 1
19 ÷ 2 = 9 reszta 1
9 ÷ 2 = 4 reszta 1
4 ÷ 2 = 2 reszta 0
2 ÷ 2 = 1 reszta 0
1 ÷ 2 = 0 reszta 1
Reszty od dołu: 10011100₂ ✓
Konwersja BIN → DEC (wagi pozycji)
10011100₂ → DEC:
1×2⁷ + 0×2⁶ + 0×2⁵ + 1×2⁴ + 1×2³ + 1×2² + 0×2¹ + 0×2⁰
= 128 + 0 + 0 + 16 + 8 + 4 + 0 + 0
= 156₁₀ ✓
Szybka konwersja BIN ↔ HEX (grupy po 4 bity)
10011100₂ → HEX:
1001 1100
9 C
= 9C₁₆ ✓
Tablica:
0000=0 0100=4 1000=8 1100=C
0001=1 0101=5 1001=9 1101=D
0010=2 0110=6 1010=A 1110=E
0011=3 0111=7 1011=B 1111=F
Szybka konwersja BIN ↔ OCT (grupy po 3 bity)
10011100₂ → OCT:
10 011 100
2 3 4
= 234₈ ✓
Arytmetyka binarna
Dodawanie BIN:
1011 (11₁₀)
+ 0110 ( 6₁₀)
------
10001 (17₁₀) ✓
Zasady: 0+0=0, 0+1=1, 1+0=1, 1+1=10 (0 i przeniesienie 1)
Konwersje w Pythonie
# DEC → inne systemy
bin(156) # '0b10011100'
oct(156) # '0o234'
hex(156) # '0x9c'
# Inne systemy → DEC
int('10011100', 2) # 156
int('234', 8) # 156
int('9C', 16) # 156
# Usuwanie prefiksu
bin(156)[2:] # '10011100'
📝 Zadanie praktyczne
Zamień liczbę 173₁₀ na system binarny, ósemkowy i szesnastkowy.
173₁₀ → BIN (dzielenie przez 2):
173 ÷ 2 = 86 r 1
86 ÷ 2 = 43 r 0
43 ÷ 2 = 21 r 1
21 ÷ 2 = 10 r 1
10 ÷ 2 = 5 r 0
5 ÷ 2 = 2 r 1
2 ÷ 2 = 1 r 0
1 ÷ 2 = 0 r 1
173₁₀ = 10101101₂
BIN → OCT (grupy po 3): 010 101 101 = 255₈
BIN → HEX (grupy po 4): 1010 1101 = AD₁₆
2. Logika i algebra Boole'a
Na maturze pytania o logikę to tablice prawdy, upraszczanie wyrażeń, bramki logiczne. Musisz znać operacje AND, OR, NOT, XOR i prawa De Morgana.
Operatory logiczne
| Operator | Symbol | Python | Opis |
|---|---|---|---|
| AND (koniunkcja) | ∧ / · | and | Prawda gdy OBA prawdziwe |
| OR (alternatywa) | ∨ / + | or | Prawda gdy CHOĆ JEDEN prawdziwy |
| NOT (negacja) | ¬ / ! | not | Odwraca wartość |
| XOR (różnica sym.) | ⊕ | ^ | Prawda gdy DOKŁADNIE JEDEN prawdziwy |
Tablice prawdy
A B │ A∧B A∨B A⊕B ¬A
──────┼────────────────────
0 0 │ 0 0 0 1
0 1 │ 0 1 1 1
1 0 │ 0 1 1 0
1 1 │ 1 1 0 0
Prawa De Morgana
¬(A ∧ B) = ¬A ∨ ¬B
¬(A ∨ B) = ¬A ∧ ¬B
Negacja koniunkcji → alternatywa negacji. Negacja alternatywy → koniunkcja negacji.
Inne prawa algebry Boole'a
| Prawo | AND | OR |
|---|---|---|
| Tożsamości | A ∧ 1 = A | A ∨ 0 = A |
| Dominacji | A ∧ 0 = 0 | A ∨ 1 = 1 |
| Idempotentność | A ∧ A = A | A ∨ A = A |
| Dopełnienie | A ∧ ¬A = 0 | A ∨ ¬A = 1 |
| Podwójna negacja | ¬(¬A) = A | |
Bramki logiczne
AND — wyjście 1 tylko gdy oba wejścia = 1
OR — wyjście 1 gdy choć jedno wejście = 1
NOT — odwraca sygnał (1→0, 0→1)
XOR — wyjście 1 gdy wejścia różne
NAND — NOT + AND (negacja AND)
NOR — NOT + OR (negacja OR)
Upraszczanie wyrażeń — przykład
Uprość: (A ∧ B) ∨ (A ∧ ¬B)
= A ∧ (B ∨ ¬B) ← wyciągnięcie A (rozdzielność)
= A ∧ 1 ← B ∨ ¬B = 1 (prawo dopełnienia)
= A ← A ∧ 1 = A (prawo tożsamości)
Operacje bitowe w Pythonie
a = 0b1100 # 12
b = 0b1010 # 10
a & b # AND → 0b1000 = 8
a | b # OR → 0b1110 = 14
a ^ b # XOR → 0b0110 = 6
~a # NOT → -13 (dopełnienie do 2)
a << 2 # Przesunięcie w lewo: 0b110000 = 48
a >> 1 # Przesunięcie w prawo: 0b0110 = 6
📝 Zadanie praktyczne
Uprość wyrażenie logiczne: (A ∧ B) ∨ (A ∧ ¬B) ∨ (¬A ∧ B)
Krok 1: Grupujemy pierwsze dwa składniki
(A ∧ B) ∨ (A ∧ ¬B) = A ∧ (B ∨ ¬B) = A ∧ 1 = A
Krok 2: Podstawiamy
A ∨ (¬A ∧ B)
Krok 3: Stosujemy prawo absorpcji
A ∨ (¬A ∧ B) = A ∨ B
Odpowiedź: A ∨ B
3. Algorytmy — analiza i pseudokod
Analiza pseudokodu to kluczowa umiejętność na maturze — część 1 (arkusz papierowy). Musisz umieć śledzić zmienne krok po kroku i przewidzieć wynik.
Pseudokod — konwencja CKE
• Przypisanie:
x ← 5• Warunek:
jeżeli x > 0 to ... w przeciwnym razie ...• Pętla:
dopóki x > 0 wykonuj ... lub dla i = 1, 2, ..., n wykonuj•
div — dzielenie całkowite, mod — reszta z dzielenia• Tablice:
T[i] (indeksowanie od 1!)Śledzenie algorytmu — technika tabelki
Algorytm:
x ← 156
wynik ← ""
dopóki x > 0 wykonuj:
r ← x mod 2
wynik ← r + wynik (dopisz r na POCZĄTEK)
x ← x div 2
zwróć wynik
Śledzenie (tabelka):
Krok │ x │ r │ wynik
0 │ 156 │ │ ""
1 │ 78 │ 0 │ "0"
2 │ 39 │ 0 │ "00"
3 │ 19 │ 1 │ "100"
4 │ 9 │ 1 │ "1100"
5 │ 4 │ 1 │ "11100"
6 │ 2 │ 0 │ "011100"
7 │ 1 │ 0 │ "0011100"
8 │ 0 │ 1 │ "10011100"
Wynik: "10011100" — to konwersja DEC→BIN!
Złożoność obliczeniowa
| Złożoność | Nazwa | Przykład |
|---|---|---|
| O(1) | Stała | Dostęp do elementu tablicy T[i] |
| O(log n) | Logarytmiczna | Wyszukiwanie binarne |
| O(n) | Liniowa | Przejście po tablicy, szukanie max |
| O(n log n) | Liniowo-log. | Merge sort, Quick sort (avg) |
| O(n²) | Kwadratowa | Bubble sort, Selection sort |
| O(2ⁿ) | Wykładnicza | Generowanie podzbiorów |
Operatory div i mod
div = dzielenie całkowite (zaokrąglenie w dół)
mod = reszta z dzielenia
17 div 5 = 3 17 mod 5 = 2
10 div 3 = 3 10 mod 3 = 1
7 div 2 = 3 7 mod 2 = 1
Zastosowania mod:
• Sprawdzenie parzystości: x mod 2 = 0
• Wyciągnięcie ostatniej cyfry: x mod 10
• Wyciągnięcie cyfr: x mod 10, (x div 10) mod 10, ...
Schemat blokowy
Symbole:
┌─────────────┐
│ START/STOP │ ← Owal
└─────────────┘
┌─────────────┐
│ Operacja │ ← Prostokąt
└─────────────┘
╱ ╲
╱ Warunek ╲ ← Romb (TAK/NIE)
╲ ╱
╲ ╱
┌─────────────┐
│ Wejście/ │
│ Wyjście │ ← Równoległobok
└─────────────┘
Specyfikacja algorytmu
• Dane wejściowe — co otrzymuje algorytm
• Wynik — co zwraca
• Warunek stopu — kiedy pętla się kończy
• Niezmiennik pętli — co jest prawdziwe w każdej iteracji
📝 Zadanie praktyczne
Prześledź algorytm dla T = [3, 7, 2, 5]. Co robi i jaki zwraca wynik?
Algorytm X(T, n):
max ← T[1]
dla i = 2, 3, ..., n wykonuj:
jeżeli T[i] > max to:
max ← T[i]
zwróć max
Śledzenie:
i=1: max ← 3
i=2: T[2]=7 > 3 → max ← 7
i=3: T[3]=2 < 7 → bez zmian
i=4: T[4]=5 < 7 → bez zmian
Zwraca: 7
Opis: Algorytm znajduje MAKSIMUM w tablicy.
Złożoność: O(n) — jedno przejście przez tablicę.
4. Sortowanie i wyszukiwanie
Algorytmy sortowania i wyszukiwania to stały element matury. Musisz znać ich działanie, złożoność i umieć śledzić krok po kroku.
Sortowanie bąbelkowe (Bubble Sort)
# 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)
# 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)
# 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)
# 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
| Algorytm | Najlepszy | Średni | Najgorszy | Pamięć |
|---|---|---|---|---|
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) |
| Szukanie liniowe | O(1) | O(n) | O(n) | O(1) |
| Szukanie binarne | O(1) | O(log n) | O(log n) | O(1) |
📝 Zadanie praktyczne
Wykonaj sortowanie bąbelkowe dla tablicy T = [5, 2, 8, 1, 9]. Pokaż tablicę po każdym przejściu.
T = [5, 2, 8, 1, 9]
Przejście 1: porównujemy sąsiadów, zamieniamy gdy lewy > prawy
[5,2,8,1,9] → [2,5,8,1,9] → [2,5,8,1,9] → [2,5,1,8,9] → [2,5,1,8,9]
Wynik: [2, 5, 1, 8, 9] — 9 "wypłynęło" na koniec
Przejście 2:
[2,5,1,8,9] → [2,5,1,8,9] → [2,1,5,8,9] → bez zmian
Wynik: [2, 1, 5, 8, 9] — 8 na miejscu
Przejście 3:
[2,1,5,8,9] → [1,2,5,8,9] → bez zmian
Wynik: [1, 2, 5, 8, 9] — POSORTOWANE ✓
Porównań: ~n² = 25, ale kończymy wcześniej.
5. Rekurencja
Rekurencja to technika algorytmiczna, w której funkcja wywołuje samą siebie. Na maturze pojawiają się zadania z analizą funkcji rekurencyjnych.
Budowa funkcji rekurencyjnej
1. Warunek bazowy (stopu) — kiedy przestać
2. Krok rekurencyjny — wywołanie siebie z mniejszym problemem
Silnia — n!
def silnia(n):
if n <= 1: # warunek bazowy
return 1
return n * silnia(n - 1) # krok rekurencyjny
# silnia(5) = 5 * silnia(4) = 5 * 4 * silnia(3) = ... = 5*4*3*2*1 = 120
Drzewo wywołań silnia(4):
silnia(4)
→ 4 * silnia(3)
→ 3 * silnia(2)
→ 2 * silnia(1)
→ return 1
→ return 2*1 = 2
→ return 3*2 = 6
→ return 4*6 = 24
Fibonacci
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
# fib(0)=0, fib(1)=1, fib(2)=1, fib(3)=2, fib(4)=3, fib(5)=5, fib(6)=8
Drzewo wywołań fib(5):
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
f(2) f(1) f(1) f(0) f(1) f(0)
/ \
f(1) f(0)
Uwaga: fib(2) liczone 3 razy! Złożoność O(2ⁿ) — WOLNE!
NWD — algorytm Euklidesa
# Wersja rekurencyjna
def nwd(a, b):
if b == 0:
return a
return nwd(b, a % b)
# nwd(48, 18) → nwd(18, 12) → nwd(12, 6) → nwd(6, 0) → 6
Śledzenie nwd(48, 18):
Krok │ a │ b │ a%b
1 │ 48 │ 18 │ 12
2 │ 18 │ 12 │ 6
3 │ 12 │ 6 │ 0
4 │ 6 │ 0 │ — → return 6
Potęgowanie szybkie
def potega(a, n):
if n == 0:
return 1
if n % 2 == 0: # n parzyste
p = potega(a, n // 2)
return p * p
else: # n nieparzyste
return a * potega(a, n - 1)
# potega(2, 10) — tylko ~4 mnożenia zamiast 10!
Wieże Hanoi
def hanoi(n, zrodlo, cel, pomocniczy):
if n == 1:
print(f"Przenieś dysk 1 z {zrodlo} na {cel}")
return
hanoi(n - 1, zrodlo, pomocniczy, cel)
print(f"Przenieś dysk {n} z {zrodlo} na {cel}")
hanoi(n - 1, pomocniczy, cel, zrodlo)
# hanoi(3, 'A', 'C', 'B') — 7 ruchów (2ⁿ - 1)
Rekurencja vs iteracja
| Cecha | Rekurencja | Iteracja |
|---|---|---|
| Czytelność | Często lepsza | Może być mniej czytelna |
| Pamięć | Stos wywołań (ryzyko przepełnienia) | Stała ilość pamięci |
| Wydajność | Może być wolna (fib!) | Zwykle szybsza |
| Na maturze | Śledzenie, drzewa, NWD | Sortowanie, zliczanie |
📝 Zadanie praktyczne
Prześledź wywołania funkcji fib(5). Ile razy wywoływana jest funkcja? Jaki jest wynik?
fib(n):
jeżeli n ≤ 1 to:
zwróć n
zwróć fib(n-1) + fib(n-2)
fib(5)
├── fib(4)
│ ├── fib(3)
│ │ ├── fib(2) → fib(1)=1 + fib(0)=0 = 1
│ │ └── fib(1) = 1
│ │ → 2
│ └── fib(2) → 1
│ → 3
└── fib(3) → 2
→ 5
Wynik: fib(5) = 5
Liczba wywołań: 15 (eksplozja!)
fib(3) obliczany 2 razy, fib(2) — 3 razy!
Złożoność: O(2ⁿ) — BARDZO wolne!
6. Python — pliki i dane
Na maturze zadanie programistyczne polega zwykle na przetworzeniu pliku tekstowego z danymi. Typowe operacje: wczytaj → przetwórz → oblicz → zapisz wynik.
Czytanie pliku tekstowego
# Podstawowy wzorzec — ZAWSZE z with!
with open("dane.txt", "r", encoding="utf-8") as plik:
for linia in plik:
linia = linia.strip() # usuń \n
print(linia)
# Wczytaj wszystko do listy
with open("dane.txt", "r", encoding="utf-8") as plik:
dane = plik.readlines() # lista stringów z \n
dane = [l.strip() for l in dane] # bez \n
Parsowanie danych — split()
# Plik: "Jan;Kowalski;85;72;93"
with open("uczniowie.txt", "r", encoding="utf-8") as f:
for linia in f:
parts = linia.strip().split(";")
imie = parts[0]
nazwisko = parts[1]
oceny = [int(x) for x in parts[2:]]
srednia = sum(oceny) / len(oceny)
print(f"{imie} {nazwisko}: {srednia:.2f}")
Zapisywanie do pliku
# Zapisz wyniki do nowego pliku
with open("wyniki.txt", "w", encoding="utf-8") as f:
f.write("Raport wyników\n")
f.write("=" * 30 + "\n")
for uczeń, wynik in wyniki:
f.write(f"{uczeń}: {wynik}\n")
# "w" nadpisuje plik, "a" dopisuje na końcu
Typowe operacje na danych
# Wczytaj listę liczb z pliku (jedna na linię)
with open("liczby.txt") as f:
liczby = [int(l.strip()) for l in f if l.strip()]
# Minimum i maksimum
print(f"Min: {min(liczby)}, Max: {max(liczby)}")
# Suma i średnia
print(f"Suma: {sum(liczby)}, Średnia: {sum(liczby)/len(liczby):.2f}")
# Ile parzystych
parzyste = [x for x in liczby if x % 2 == 0]
print(f"Parzyste: {len(parzyste)}")
# Ile w zakresie [10, 50]
w_zakresie = [x for x in liczby if 10 <= x <= 50]
# Sortowanie malejąco
posortowane = sorted(liczby, reverse=True)
# Unikalne wartości
unikalne = sorted(set(liczby))
Przetwarzanie tekstów
# Zliczanie słów
tekst = "ala ma kota a kot ma alę"
slowa = tekst.split()
print(len(slowa)) # 7
# Zliczanie wystąpień litery
print(tekst.count("a")) # 5
# Zamiana liter
print(tekst.upper()) # "ALA MA KOTA..."
print(tekst.title()) # "Ala Ma Kota..."
# Sprawdzanie
"abc".isalpha() # True — tylko litery
"123".isdigit() # True — tylko cyfry
"abc".islower() # True — małe litery
# Zliczanie liter i cyfr w tekście
litery = sum(1 for c in tekst if c.isalpha())
cyfry = sum(1 for c in tekst if c.isdigit())
Wzorzec maturalnego zadania programistycznego
# Typowy szkielet rozwiązania:
# 1. Wczytaj dane
with open("dane.txt", "r", encoding="utf-8") as f:
dane = [linia.strip().split(";") for linia in f if linia.strip()]
# 2. Przetwórz (np. wyciągnij kolumny, konwertuj typy)
rekordy = []
for row in dane:
rekordy.append({
"nazwa": row[0],
"wartosc": int(row[1]),
"kategoria": row[2]
})
# 3. Oblicz (odpowiedzi na podpunkty)
# a) Ile rekordów spełnia warunek?
wynik_a = len([r for r in rekordy if r["wartosc"] > 100])
# b) Który rekord ma max wartość?
wynik_b = max(rekordy, key=lambda r: r["wartosc"])
# c) Suma per kategoria
from collections import Counter
sumy = {}
for r in rekordy:
sumy[r["kategoria"]] = sumy.get(r["kategoria"], 0) + r["wartosc"]
# 4. Zapisz wyniki
with open("wyniki.txt", "w", encoding="utf-8") as f:
f.write(f"a) {wynik_a}\n")
f.write(f"b) {wynik_b['nazwa']}\n")
for kat, s in sorted(sumy.items()):
f.write(f"c) {kat}: {s}\n")
📝 Zadanie praktyczne
Plik oceny.txt ma format: imie;mat;inf;ang. Napisz kod, który znajdzie ucznia z najwyższą średnią.
with open("oceny.txt", "r", encoding="utf-8") as f:
uczniowie = []
for linia in f:
p = linia.strip().split(";")
uczniowie.append({
"imie": p[0],
"srednia": (int(p[1]) + int(p[2]) + int(p[3])) / 3
})
najlepszy = max(uczniowie, key=lambda u: u["srednia"])
print(f"{najlepszy['imie']}: {najlepszy['srednia']:.2f}")
7. Arkusz kalkulacyjny
Na maturze zadanie z arkusza kalkulacyjnego wymaga formuł, funkcji warunkowych, wyszukiwania i wykresu. Dane importujesz z pliku .txt/.csv.
Import danych
Kluczowe funkcje
| Funkcja | Opis | Przykład |
|---|---|---|
| SUMA | Suma zakresu | =SUMA(B2:B100) |
| ŚREDNIA | Średnia arytmetyczna | =ŚREDNIA(C2:C100) |
| MIN / MAX | Minimum / Maksimum | =MAX(D2:D100) |
| ILE.LICZB | Ile komórek z liczbami | =ILE.LICZB(A:A) |
| DŁUGOŚĆ | Ile znaków w tekście | =DŁUGOŚĆ(A2) |
| LEWY / PRAWY | N znaków od lewej/prawej | =LEWY(A2;3) |
| FRAGMENT.TEKSTU | Wycinek tekstu | =FRAGMENT.TEKSTU(A2;2;4) |
Funkcje warunkowe
JEŻELI — warunek z dwoma wynikami:
=JEŻELI(C2>=50;"Zdał";"Nie zdał")
Zagnieżdżone:
=JEŻELI(C2>=90;"Celująca";JEŻELI(C2>=75;"Dobra";JEŻELI(C2>=50;"Dostateczna";"Niedostateczna")))
LICZ.JEŻELI — ile spełnia warunek:
=LICZ.JEŻELI(B2:B100;"elektronika")
=LICZ.JEŻELI(C2:C100;">=50")
SUMA.JEŻELI — suma dla warunku:
=SUMA.JEŻELI(C2:C100;">=50";D2:D100)
LICZ.WARUNKI — wiele warunków:
=LICZ.WARUNKI(B2:B100;"elektronika";C2:C100;">=100")
SUMA.WARUNKÓW:
=SUMA.WARUNKÓW(D2:D100;B2:B100;"odzież";C2:C100;">=50")
WYSZUKAJ.PIONOWO (VLOOKUP)
Składnia:
=WYSZUKAJ.PIONOWO(szukana_wartość; tabela; nr_kolumny; [przybliżone])
Przykład — szukaj oceny dla ucznia "Jan":
=WYSZUKAJ.PIONOWO("Jan";A2:D100;3;0)
↑ ↑ ↑ ↑
szukana wartość tabela kol dokładne (0)
WAŻNE: 0 = dokładne dopasowanie (prawie ZAWSZE chcesz 0!)
1 = przybliżone (tablica MUSI być posortowana)
Adresowanie
| Typ | Zapis | Zachowanie przy kopiowaniu |
|---|---|---|
| Względne | A1 | Zmienia się (przesunięcie) |
| Bezwzględne | $A$1 | NIE zmienia się |
| Mieszane | $A1 lub A$1 | Jedna współrzędna stała |
=B2/$B$101Wykresy
Najczęściej wymagane na maturze:
1. Kolumnowy — porównanie wartości per kategoria
2. Kołowy — udział procentowy każdej kategorii
3. Liniowy — trendy w czasie
Obowiązkowe elementy:
• Tytuł wykresu
• Opisy osi (X i Y)
• Legenda (gdy wiele serii)
• Etykiety danych (opcjonalnie)
📝 Zadanie praktyczne
Dane w kolumnach: A (produkt), B (cena), C (ilość). Napisz formuły dla: sumy wartości, średniej ceny, produktu z najwyższą ceną.
Suma wartości (cena × ilość):
=SUMA.ILOCZYNU(B2:B100;C2:C100)
lub: =SUMA(B2:B100*C2:C100) [Ctrl+Shift+Enter w starszych wersjach]
Średnia cena:
=ŚREDNIA(B2:B100)
Produkt z najwyższą ceną:
=INDEKS(A2:A100;PODAJ.POZYCJĘ(MAX(B2:B100);B2:B100;0))
lub: =WYSZUKAJ.PIONOWO(MAX(B:B);B:A;2;0) [dane posortowane]
8. Bazy danych i SQL
Na maturze zadanie bazodanowe wymaga napisania zapytań SQL. Od 2023 roku SQL trzeba też napisać na papierze (Część 1). W Części 2 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
| Operator | Przykład |
|---|---|
=, !=, <, >, <=, >= | WHERE cena > 100 |
AND, OR, NOT | WHERE cena > 100 AND kategoria = 'odzież' |
BETWEEN | WHERE wiek BETWEEN 18 AND 25 |
IN | WHERE miasto IN ('Kraków', 'Warszawa') |
LIKE | WHERE nazwisko LIKE 'Kow%' |
IS NULL | WHERE email IS NULL |
LIKE — wzorce
% — dowolny ciąg znaków (0 lub więcej)
_ — dokładnie jeden znak
WHERE imie LIKE 'A%' -- zaczyna się na A
WHERE imie LIKE '%ski' -- kończy się na "ski"
WHERE imie LIKE '_a%' -- druga litera to "a"
WHERE kod LIKE '3_-___' -- wzorzec: 3X-XXX
Funkcje agregujące
COUNT(*) — ilość wierszy
COUNT(kolumna) — ilość niepustych wartości
SUM(kolumna) — suma
AVG(kolumna) — średnia
MIN(kolumna) — minimum
MAX(kolumna) — maksimum
-- Przykład
SELECT COUNT(*) AS ile_produktow,
AVG(cena) AS srednia_cena,
MAX(cena) AS najdrozsza
FROM produkty;
GROUP BY + HAVING
-- Ile produktów per kategoria
SELECT kategoria, COUNT(*) AS ile
FROM produkty
GROUP BY kategoria;
-- Tylko kategorie z > 5 produktami
SELECT kategoria, COUNT(*) AS ile
FROM produkty
GROUP BY kategoria
HAVING COUNT(*) > 5;
-- Kolejność klauzul: SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
WHERE filtruje PRZED grupowaniem (wiersze)
HAVING filtruje PO grupowaniu (grupy)
JOIN — łączenie tabel
-- INNER JOIN — tylko pasujące wiersze z obu tabel
SELECT u.imie, u.nazwisko, o.przedmiot, o.ocena
FROM uczniowie u
INNER JOIN oceny o ON u.id = o.uczen_id;
-- LEFT JOIN — wszyscy z lewej + pasujący z prawej (NULL jeśli brak)
SELECT u.imie, COUNT(o.id) AS ile_ocen
FROM uczniowie u
LEFT JOIN oceny o ON u.id = o.uczen_id
GROUP BY u.imie;
Podzapytania
-- Uczniowie z oceną wyższą od średniej
SELECT imie, ocena
FROM uczniowie
WHERE ocena > (SELECT AVG(ocena) FROM uczniowie);
-- Produkty z kategorii z najwyższą średnią ceną
SELECT *
FROM produkty
WHERE kategoria = (
SELECT kategoria
FROM produkty
GROUP BY kategoria
ORDER BY AVG(cena) DESC
LIMIT 1
);
Modyfikacja danych
-- INSERT
INSERT INTO uczniowie (imie, nazwisko, klasa)
VALUES ('Jan', 'Kowalski', '4TI');
-- UPDATE
UPDATE produkty
SET cena = cena * 1.1
WHERE kategoria = 'elektronika';
-- DELETE
DELETE FROM uczniowie
WHERE klasa = '4TI' AND ocena < 2;
-- CREATE TABLE
CREATE TABLE zamowienia (
id INTEGER PRIMARY KEY AUTOINCREMENT,
produkt_id INTEGER,
ilosc INTEGER DEFAULT 1,
data TEXT,
FOREIGN KEY (produkt_id) REFERENCES produkty(id)
);
CREATE VIEW
-- Widok (wirtualna tabela)
CREATE VIEW raport_kategorie AS
SELECT kategoria,
COUNT(*) AS ile,
AVG(cena) AS srednia_cena,
SUM(cena * ilosc) AS wartosc
FROM produkty
GROUP BY kategoria;
-- Korzystanie z widoku
SELECT * FROM raport_kategorie WHERE wartosc > 10000;
📝 Zadanie praktyczne
Masz tabelę zamowienia (klient, produkt, cena, data). Napisz zapytanie: ile zamówień złożył każdy klient i ile wydał łącznie, tylko klienci z >3 zamówieniami.
SELECT
klient,
COUNT(*) AS ile_zamowien,
SUM(cena) AS suma_wydatkow
FROM zamowienia
GROUP BY klient
HAVING COUNT(*) > 3
ORDER BY suma_wydatkow DESC;
9. Sieci komputerowe
Na maturze pytania o sieci dotyczą adresacji IP, masek podsieci, obliczania adresów sieci i rozgłoszeniowych. Czasem pojawiają się pytania o protokoły.
Adres IP (IPv4)
Przykład:
192.168.1.100 = 11000000.10101000.00000001.01100100Klasy adresów IP
| Klasa | Zakres | Maska domyślna | Przeznaczenie |
|---|---|---|---|
| A | 1.0.0.0 – 126.x.x.x | 255.0.0.0 (/8) | Duże sieci |
| B | 128.0.0.0 – 191.255.x.x | 255.255.0.0 (/16) | Średnie sieci |
| C | 192.0.0.0 – 223.255.255.x | 255.255.255.0 (/24) | Małe sieci |
Adresy prywatne (RFC 1918)
10.0.0.0 – 10.255.255.255 (klasa A — 1 sieć)
172.16.0.0 – 172.31.255.255 (klasa B — 16 sieci)
192.168.0.0 – 192.168.255.255 (klasa C — 256 sieci)
Adres 127.0.0.1 = localhost (pętla zwrotna)
Maska podsieci
Maska określa: które bity = sieć, które = host.
255.255.255.0 = /24
11111111.11111111.11111111.00000000
└────── sieć (24 bity) ───┘└ hosty ┘
Adres sieci: IP AND maska (bitowy AND)
Adres broadcast: IP OR NOT(maska)
Przykład: IP = 192.168.1.100, maska = /24
Adres sieci: 192.168.1.0
Adres broadcast: 192.168.1.255
Hosty: 192.168.1.1 – 192.168.1.254 (254 hosty)
Obliczanie — subnetting (maska /26)
IP: 192.168.1.130, maska: /26 (255.255.255.192)
Krok 1: /26 → 26 bitów sieci, 6 bitów hosta
Krok 2: 2⁶ = 64 adresów na podsieć
Krok 3: Podsieci: .0, .64, .128, .192
Krok 4: 130 należy do podsieci .128
Adres sieci: 192.168.1.128
Pierwszy host: 192.168.1.129
Ostatni host: 192.168.1.190
Broadcast: 192.168.1.191
Ile hostów: 2⁶ - 2 = 62
Tabela masek
| CIDR | Maska | Hostów | Adresów |
|---|---|---|---|
| /24 | 255.255.255.0 | 254 | 256 |
| /25 | 255.255.255.128 | 126 | 128 |
| /26 | 255.255.255.192 | 62 | 64 |
| /27 | 255.255.255.224 | 30 | 32 |
| /28 | 255.255.255.240 | 14 | 16 |
| /30 | 255.255.255.252 | 2 | 4 |
Protokoły
| Protokół | Port | Opis |
|---|---|---|
| HTTP | 80 | Strony WWW (nieszyfrowane) |
| HTTPS | 443 | Strony WWW (szyfrowane TLS) |
| FTP | 21 | Transfer plików |
| SSH | 22 | Zdalny terminal (szyfrowany) |
| SMTP | 25 | Wysyłanie e-maili |
| DNS | 53 | Tłumaczenie domen na IP |
| DHCP | 67/68 | Automatyczne przydzielanie IP |
Model OSI / TCP-IP
TCP/IP (4 warstwy):
4. Aplikacji — HTTP, FTP, DNS, SMTP
3. Transportowa — TCP (niezawodny), UDP (szybki)
2. Internetowa — IP, ICMP, ARP
1. Dostępu — Ethernet, Wi-Fi, kable
TCP vs UDP:
TCP — potwierdza odbiór, gwarantuje kolejność (WWW, e-mail)
UDP — bez potwierdzeń, szybszy (streaming, gry, VoIP)
📝 Zadanie praktyczne
Komputer ma IP 192.168.10.45 i maskę /26. Oblicz adres sieci i adres rozgłoszeniowy.
/26 = 255.255.255.192
192 = 11000000 → ostatni oktet ma 64 adresy (2^6)
192.168.10.45:
- 45 / 64 = 0 (zaokrąglenie w dół) → sieć zaczyna od .0
- 45 mieści się w zakresie 0-63
Adres sieci: 192.168.10.0
Broadcast: 192.168.10.63 (0 + 64 - 1)
Zakres hostów: 192.168.10.1 - 192.168.10.62
Sprawdzenie: 62 hosty = 2^6 - 2 = 64 - 2 ✓
10. Teoria informacji
Na maturze pojawiają się zadania z kodowania znaków (ASCII, UTF-8), miar ilości informacji i kompresji danych.
Jednostki informacji
| Jednostka | Wartość |
|---|---|
| 1 bit (b) | 0 lub 1 |
| 1 bajt (B) | 8 bitów |
| 1 KB (kilobajt) | 1024 B = 2¹⁰ B |
| 1 MB (megabajt) | 1024 KB = 2²⁰ B |
| 1 GB (gigabajt) | 1024 MB = 2³⁰ B |
| 1 TB (terabajt) | 1024 GB = 2⁴⁰ B |
Ilość informacji
Przykłady:
• 2 stany → 1 bit
• 8 stanów → 3 bity
• 256 stanów → 8 bitów = 1 bajt
• 1000 stanów → 10 bitów (2¹⁰ = 1024 ≥ 1000)
Kodowanie znaków
| Kodowanie | Bajty/znak | Zakres |
|---|---|---|
| ASCII | 1 (7 bitów) | 128 znaków (0-127): litery ang., cyfry, znaki specjalne |
| ASCII rozszerzony | 1 (8 bitów) | 256 znaków — dodaje znaki narodowe (np. Windows-1250) |
| UTF-8 | 1-4 | Cały Unicode; ASCII-compatible (znaki ang. = 1 bajt) |
| UTF-16 | 2-4 | Cały Unicode; minimum 2 bajty na znak |
Kody ASCII (musisz pamiętać!):
'0' = 48 'A' = 65 'a' = 97
'1' = 49 'B' = 66 'b' = 98
... ... ...
'9' = 57 'Z' = 90 'z' = 122
Różnice: 'a' - 'A' = 32, '1' - '0' = 1 (konwersja cyfra→liczba)
Kompresja
Kompresja bezstratna — odtworzysz 100% oryginału:
• ZIP, RAR, 7z (pliki)
• PNG (obrazy)
• FLAC (dźwięk)
Metody: kodowanie Huffmana, RLE, LZW
Kompresja stratna — traci część jakości:
• JPEG (obrazy)
• MP3 (dźwięk)
• MP4/H.264 (wideo)
Stopień kompresji = rozmiar_oryginału / rozmiar_skompresowany
Np. 10 MB → 2 MB = stopień 5:1 (80% redukcji)
Kodowanie Huffmana
Idea: Częste znaki → krótkie kody, rzadkie → dłuższe
Tekst: "AABBBCCCC"
Częstość: C=4, B=3, A=2
Drzewo Huffmana:
(9)
/ \
(5) C:4 → kod: 1
/ \
A:2 B:3
kod:00 kod:01
Zakodowany: 00 00 01 01 01 1 1 1 1
Oryginalnie: 9 znaków × 8 bitów = 72 bity
Huffman: 2+2+2+2+2+1+1+1+1 = 14 bitów → 80% mniej!
Grafika — rozdzielczość i głębia koloru
Rozmiar obrazu = szerokość × wysokość × głębia_koloru / 8
Przykład: Full HD, 24-bit kolor:
1920 × 1080 × 24 / 8 = 6 220 800 B ≈ 5.93 MB
Głębia koloru:
1 bit → 2 kolory (czarno-biały)
8 bit → 256 kolorów (odcienie szarości)
24 bit → 16 777 216 kolorów (True Color: 8R + 8G + 8B)
32 bit → 24 bit + 8 bit kanał alfa (przezroczystość)
Dźwięk — próbkowanie
Rozmiar = częstotliwość × głębia × kanały × czas_s / 8
Przykład: Jakość CD, 10 sekund:
44100 Hz × 16 bit × 2 kanały × 10 s / 8 = 1 764 000 B ≈ 1.68 MB
Częstotliwość próbkowania: ile próbek/s (CD = 44100 Hz)
Głębia bitowa: precyzja każdej próbki (CD = 16 bit)
Kanały: mono (1), stereo (2)
📝 Zadanie praktyczne
Film trwa 90 minut, ma 30 klatek/s, rozdzielczość 1280×720, głębia 24 bit. Oblicz rozmiar nieskompresowanego filmu.
Dane:
- Czas: 90 min = 5400 s
- FPS: 30 klatek/s
- Rozdzielczość: 1280 × 720 pikseli
- Głębia: 24 bit na piksel
Obliczenia:
1. Liczba klatek: 5400 s × 30 = 162 000 klatek
2. Rozmiar klatki: 1280 × 720 × 24 / 8 = 2 764 800 B ≈ 2.64 MB
3. Rozmiar filmu: 162 000 × 2 764 800 = 447 897 600 000 B
Przeliczenie:
447 897 600 000 / 1024 / 1024 / 1024 ≈ 417 GB
Odpowiedź: ~417 GB (dlatego kompresja jest konieczna!)
🔥 Rozgrzewka — Analiza algorytmu
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
T[i] mod 2 w pseudokodzie?mod oznacza resztę z dzielenia (modulo). T[i] mod 2 zwraca 0 dla liczb parzystych i 1 dla liczb nieparzystych. W Pythonie używamy %: T[i] % 2.wynik ← 0 lub i ← i + 1. NIE używamy = do przypisania! = w pseudokodzie to porównanie (równość).#01 — Analiza algorytmu (pseudokod)
Typ zadania: Część 1 — arkusz papierowy · Czas: ~30 min · Pseudokod CKE
Kontekst
Na arkuszu maturalnym podano pseudokod algorytmu operującego na tablicy liczb całkowitych T[1..n]. Przeanalizuj algorytm i odpowiedz na pytania.
Pseudokod
Dane: T[1..8] = [4, 7, 2, 9, 1, 5, 8, 3], n = 8
Algorytm TAJEMNICA(T, n):
wynik ← 0
i ← 1
dopóki i ≤ n wykonuj:
jeżeli T[i] mod 2 = 1 to:
wynik ← wynik + T[i]
i ← i + 1
zwróć wynik
Polecenia
- Podpunkt a) Prześledź algorytm krok po kroku. Podaj wartość zmiennej
wynikpo każdej iteracji pętli. - Podpunkt b) Co oblicza ten algorytm? Opisz jednym zdaniem.
- Podpunkt c) Zmodyfikuj algorytm tak, aby obliczał sumę liczb parzystych.
- Podpunkt d) Jaka jest złożoność czasowa tego algorytmu? Uzasadnij.
- Podpunkt e) Napisz ten algorytm w Pythonie.
Na lekcji: Niech uczeń sam narysuje tabelkę śledzenia. Ty NIE podawaj wyniku — pytaj: „Co jest w wynik po 3 iteracji?"
#01 — Rozwiązanie krok po kroku
„Zanim zaczniesz liczyć — przeczytaj kod. Co robi T[i] mod 2 = 1?"
„Kiedy reszta z dzielenia przez 2 wynosi 1?"
Odp: Gdy liczba jest nieparzysta.
„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)
„Opisz jednym zdaniem co robi algorytm."
Odp: Algorytm oblicza sumę wszystkich elementów nieparzystych
w tablicy T o n elementach.
„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 ✓
„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.
„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ń.
1. Co zwróci algorytm, który ma warunek T[i] mod 3 = 0 i sumuje T[i]?
2. Jaka jest złożoność czasowa algorytmu, który przegląda tablicę n-elementową raz?
3. W pseudokodzie CKE, jak zapisujemy przypisanie?
4. Dla T = [5, 2, 8, 3], ile wynosi suma elementów spełniających T[i] mod 2 = 1?
5. Co oznacza zwróć wynik w pseudokodzie?
🔥 Rozgrzewka — Programowanie Python
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
with open("plik.txt", "r", encoding="utf-8") as f: — użyj encoding="utf-8" żeby polskie znaki się wyświetlały poprawnie!parts = linia.strip().split(";") — najpierw strip() usuwa \n, potem split(";") dzieli po średniku. Wynik: ["2024-01-01", "-5.2", "0.0"]float() od int()?int() zamienia na liczbę całkowitą (np. "42" → 42). float() zamienia na ułamek (np. "-5.2" → -5.2). Temperatura i opady to float!max(lista) i min(lista). Dla obiektów używaj key: max(dane, key=lambda x: x["temp"]). Pamiętaj: zwraca element, nie indeks!data.split("-")[1] zwróci "01". Albo data[5:7] (slice). Możesz też użyć modułu datetime dla bardziej zaawansowanych operacji.#02 — Programowanie Python — przetwarzanie pliku
Typ zadania: Część 2 — komputer · Czas: ~45 min · Python
Kontekst
Plik dane_pogoda.txt zawiera dane pogodowe z 365 dni (po jednym wierszu na dzień). Format: data;temperatura;opady;cisnienie
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
„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
„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"])
„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")
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}%")
„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}")
else.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)}")
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ń.
1. Która konstrukcja PRAWIDŁOWO wczytuje plik z polskimi znakami?
2. Co zwróci "a;b;c".split(";")?
3. Jak poprawnie zamienić "-5.2" na liczbę?
4. Która funkcja znajdzie element z NAJMNIEJSZĄ wartością?
5. Jak wyciągnąć "01" z daty "2024-01-15"?
🔥 Rozgrzewka — Arkusz kalkulacyjny
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
A1 to adres względny — zmienia się przy kopiowaniu formuły. $A$1 to adres bezwzględny — NIE zmienia się. $A1 i A$1 to adresy mieszane.=LICZ.JEŻELI(B2:B100;">50") — warunek w cudzysłowie! Dla wielu warunków: =LICZ.WARUNKI(...).=WYSZUKAJ.PIONOWO(co;gdzie;która_kolumna;0). 0 = dokładne dopasowanie!=SUMA.JEŻELI(zakres_kryteriów;kryterium;zakres_sumy). Przykład: =SUMA.JEŻELI(A:A;"jabłka";B:B) — sumuje wartości z B, gdzie w A jest "jabłka".#03 — Arkusz kalkulacyjny
Typ zadania: Część 2 — komputer · Czas: ~30 min · Excel / LibreOffice Calc
Kontekst
Plik wyniki_egzamin.txt zawiera dane 200 uczniów z egzaminu. Format: imie;nazwisko;klasa;matematyka;informatyka;angielski
Anna;Kowalska;4A;72;85;68
Jan;Nowak;4B;55;92;71
Maria;Wiśniewska;4A;88;67;90
...
Punkty z każdego przedmiotu: 0–100.
Polecenia
- a) Zaimportuj dane do arkusza. W kolumnie G oblicz sumę punktów dla każdego ucznia. W kolumnie H oblicz średnią (2 m. po przecinku).
- b) Ile osób z klasy 4A zdało informatykę (≥50 pkt)? Użyj funkcji LICZ.WARUNKI.
- c) Znajdź najwyższy wynik z matematyki i użyj WYSZUKAJ.PIONOWO, aby znaleźć imię i nazwisko tej osoby.
- d) Oblicz średnią z informatyki osobno dla klasy 4A i 4B. Użyj ŚREDNIA.JEŻELI.
- e) Utwórz wykres kolumnowy porównujący średnie wyniki z 3 przedmiotów dla klas 4A i 4B.
Na lekcji: Zacznij od importu! Pokaż jak działa kreator tekstu → kolumny (separator: średnik).
#03 — Rozwiązanie krok po kroku
„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
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.
„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ę.
">=50", nie bez cudzysłowu!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.
Ś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)
„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
📝 Sprawdzian — Arkusz kalkulacyjny
Sprawdź swoją wiedzę z arkuszy kalkulacyjnych. Masz 10 minut na 5 pytań.
1. Jak zapisać adres bezwzględny dla komórki B5?
2. Która formuła zlicza komórki z wartością większą niż 100?
3. W WYSZUKAJ.PIONOWO, co oznacza ostatni argument "0"?
4. Co MUSI zawierać poprawny wykres na maturze?
5. Która funkcja oblicza średnią dla komórek spełniających warunek?
🔥 Rozgrzewka — Bazy danych SQL
Szybkie pytania na początek lekcji — kliknij pytanie, żeby zobaczyć odpowiedź.
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT. Pamiętaj: WHERE przed GROUP BY, HAVING po GROUP BY!WHERE filtruje wiersze PRZED grupowaniem. HAVING filtruje grupy PO grupowaniu. Z HAVING używasz funkcji agregujących (COUNT, SUM, AVG).INNER JOIN — tylko pasujące wiersze z obu tabel. LEFT JOIN — wszystko z lewej + pasujące z prawej. Na maturze najczęściej INNER JOIN.ORDER BY cena DESC. Domyślnie jest ASC (rosnąco). Możesz sortować po wielu kolumnach: ORDER BY kategoria ASC, cena DESC.SELECT kategoria, COUNT(*) FROM produkty GROUP BY kategoria. COUNT(*) zlicza wszystkie wiersze, COUNT(kolumna) — tylko niepuste.#04 — Bazy danych SQL
Typ zadania: Część 1 (SQL na papierze) + Część 2 (komputer) · Czas: ~40 min
Kontekst
Baza danych biblioteki zawiera 3 tabele:
-- Tabela: ksiazki
-- id INTEGER PRIMARY KEY
-- tytul TEXT
-- autor_id INTEGER (FK → autorzy.id)
-- rok_wydania INTEGER
-- gatunek TEXT
-- rating REAL
-- Tabela: autorzy
-- id INTEGER PRIMARY KEY
-- imie TEXT
-- nazwisko TEXT
-- kraj TEXT
-- Tabela: wypozyczenia
-- id INTEGER PRIMARY KEY
-- ksiazka_id INTEGER (FK → ksiazki.id)
-- data_wyp TEXT
-- data_zwrotu TEXT (NULL = niezwrócona)
Polecenia
- a) Wyświetl tytuły książek i nazwiska autorów, posortowane alfabetycznie po nazwisku.
- b) Ile książek napisał każdy autor? Wyświetl imię, nazwisko i liczbę książek. Tylko autorzy z ≥ 3 książkami.
- c) Wyświetl tytuły książek, które nigdy nie zostały wypożyczone.
- d) Ile książek z gatunku 'kryminał' zostało wypożyczonych w 2024 roku?
- e) Wyświetl tytuł i autora książki z najwyższym ratingiem.
- f) Stwórz widok (VIEW) pokazujący dla każdego gatunku: liczbę książek, średni rating i liczbę wypożyczeń.
#04 — Rozwiązanie krok po kroku
„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
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.
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;
WHERE filtruje WIERSZE, HAVING filtruje GRUPY!
„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.
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'
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.
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;
📝 Sprawdzian — Bazy danych SQL
Sprawdź swoją wiedzę z SQL. Masz 10 minut na 5 pytań.
1. Jaka jest prawidłowa kolejność klauzul w SELECT?
2. Kiedy używamy HAVING zamiast WHERE?
3. Co zwróci COUNT(*) dla tabeli z 10 wierszami (w tym 2 z NULL w kolumnie X)?
4. Jak posortować wyniki od najwyższej ceny?
5. Co robi INNER JOIN?
📝 Ściąga — Matura z informatyki
Kluczowe wzory, formuły i szablony na maturę z informatyki. Wydrukuj i przejrzyj przed egzaminem!
🔢 Systemy liczbowe
DEC→BIN: dziel przez 2, reszty od dołu
BIN→DEC: suma wag (128,64,32,16,8,4,2,1)
BIN→HEX: grupy po 4 bity
BIN→OCT: grupy po 3 bity
Python: bin(), oct(), hex(), int(s,base)
🧮 Logika Boole'a
De Morgan: ¬(A∧B)=¬A∨¬B
¬(A∨B)=¬A∧¬B
A∧1=A A∧0=0 A∧A=A A∧¬A=0
A∨0=A A∨1=1 A∨A=A A∨¬A=1
Python: and, or, not, &, |, ^
📊 Złożoność
O(1) — stała (dostęp T[i])
O(log n) — binary search
O(n) — liniowe szukanie
O(n log n)— merge sort
O(n²) — bubble/selection sort
O(2ⁿ) — podzbiory
🐍 Python — pliki
with open("f.txt","r",encoding="utf-8") as f:
for l in f:
parts = l.strip().split(";")
# Zapis:
with open("w.txt","w",encoding="utf-8") as f:
f.write(f"wynik: {x}\n")
🐍 Python — listy
min(L), max(L), sum(L), len(L)
sorted(L), sorted(L, reverse=True)
[x for x in L if warunek]
sum(1 for x in L if warunek)
set(L) # unikalne
🐍 Python — stringi
.split(sep) .strip() .upper()
.lower() .count(c) .replace(a,b)
.startswith(s) .endswith(s)
.isdigit() .isalpha() .islower()
f"text {zmienna:.2f}"
📋 Excel — kluczowe
=JEŻELI(war;"tak";"nie")
=LICZ.JEŻELI(zakr;war)
=LICZ.WARUNKI(z1;w1;z2;w2)
=SUMA.JEŻELI(z_war;war;z_sum)
=ŚREDNIA.JEŻELI(z_war;war;z_śr)
=WYSZUKAJ.PIONOWO(szu;tab;kol;0)
📋 Excel — adresy
A1 → względny (przesuwa się)
$A$1 → bezwzgl. (stały)
$A1 → kol stała, wiersz ruchomy
A$1 → kol ruchoma, wiersz stały
Skrót: F4 przełącza tryby
🗃️ SQL — SELECT
SELECT col FROM tab
WHERE war
GROUP BY col HAVING war
ORDER BY col ASC|DESC
LIMIT n;
🗃️ SQL — JOIN
INNER JOIN t2 ON t1.id=t2.fk
LEFT JOIN t2 ON t1.id=t2.fk
-- INNER: tylko pasujące
-- LEFT: wszyscy z lewej+NULL
🗃️ SQL — agregaty
COUNT(*), COUNT(col)
SUM(col), AVG(col)
MIN(col), MAX(col)
ROUND(val, 2)
DISTINCT col
🌐 Sieci — IP
Hosty = 2^(32-maska) - 2
/24→254h /25→126h /26→62h /27→30h
Adres sieci: IP AND maska
Broadcast: IP OR NOT(maska)
Prywatne: 10.x, 172.16-31.x, 192.168.x
💾 Informacja
N wartości → ⌈log₂(N)⌉ bitów
1KB=1024B 1MB=1024KB 1GB=1024MB
Obraz = szer×wys×głębia/8 bajtów
Dźwięk = freq×bity×kanały×czas/8
ASCII: '0'=48 'A'=65 'a'=97
⏱️ Strategia egzaminu
210 min — podział:
Cz.1 papier: ~50-60 min
Cz.2 program: ~50 min
Cz.2 Excel: ~30 min
Cz.2 SQL: ~40 min
Rezerwa: ~30 min
ZAWSZE zacznij od łatwiejszego!
🐛 Typowe błędy na maturze z informatyki
10 najczęstszych błędów, które kosztują punkty. Przejrzyj przed egzaminem!
❌ BŁĄD: Reszty z dzielenia czytane od GÓRY zamiast od DOŁU
156 ÷ 2 → reszta 0 ← to NIE jest pierwsza cyfra wyniku!
✅ POPRAWNIE: Reszty czytamy od DOŁU do GÓRY
Ostatnia reszta = najbardziej znaczący bit
❌ Pseudokod CKE: T[1..n] — indeksy od 1!
Python: T[0..n-1] — indeksy od 0!
Częsty błąd: Przepisanie pseudokodu 1:1 do Pythona
Pseudokod: T[1] → Python: T[0]
Pseudokod: T[n] → Python: T[n-1]
Pseudokod: i=1..n → Python: range(n) lub range(0,n)
# Pseudokod CKE:
# x div y = dzielenie całkowite
# x mod y = reszta
# Python:
x // y # div (nie / !)
x % y # mod
# ❌ BŁĄD: 17 / 5 = 3.4 (float!)
# ✅ POPRAWNIE: 17 // 5 = 3 (int)
# ❌ BŁĄD:
for linia in plik:
parts = linia.split(";")
# parts[-1] zawiera "wartość\n" — z nową linią!
# ✅ POPRAWNIE:
for linia in plik:
parts = linia.strip().split(";")
# czyste wartości
-- ❌ BŁĄD:
SELECT kategoria, COUNT(*)
FROM produkty
WHERE COUNT(*) > 5 -- NIE DZIAŁA!
GROUP BY kategoria;
-- ✅ POPRAWNIE:
SELECT kategoria, COUNT(*)
FROM produkty
GROUP BY kategoria
HAVING COUNT(*) > 5; -- HAVING po GROUP BY
❌ BŁĄD: Kopiujesz formułę w dół i tabela się przesuwa:
=WYSZUKAJ.PIONOWO(A2;G2:H100;2;0)
→ po skopiowaniu: =WYSZUKAJ.PIONOWO(A3;G3:H101;2;0) ← ZŁE!
✅ POPRAWNIE: Zablokuj tabelę dolarem:
=WYSZUKAJ.PIONOWO(A2;$G$2:$H$100;2;0)
→ A2 się przesuwa (dobrze), tabela stała
# Szukanie najdłuższej serii (np. bez opadów)
# ❌ BŁĄD — brak sprawdzenia po pętli:
for d in dane:
if warunek:
biezaca += 1
else:
if biezaca > max: max = biezaca
biezaca = 0
# Jeśli seria jest na KOŃCU — nigdy nie wejdzie w else!
# ✅ POPRAWNIE — sprawdź po pętli:
for d in dane:
...
# dodaj TO po pętli:
if biezaca > max: max = biezaca
# ❌ BŁĄD (Windows domyślnie cp1250):
with open("dane.txt") as f: # może nie odczytać ąęół
# ✅ POPRAWNIE:
with open("dane.txt", encoding="utf-8") as f:
# lub:
with open("dane.txt", encoding="cp1250") as f:
# Sprawdź kodowanie pliku w notatniku!
-- ❌ Ile RÓŻNYCH gatunków?
SELECT COUNT(gatunek) FROM ksiazki;
-- Liczy WSZYSTKIE wiersze (z powtórzeniami!)
-- ✅ POPRAWNIE:
SELECT COUNT(DISTINCT gatunek) FROM ksiazki;
-- ❌ Ile uczniów wypożyczyło książkę?
SELECT COUNT(*) FROM wypozyczenia; -- liczy WYPOŻYCZENIA, nie uczniów!
-- ✅ POPRAWNIE:
SELECT COUNT(DISTINCT uczen_id) FROM wypozyczenia;
❌ BŁĄD: 1 KB = 1000 B
✅ POPRAWNIE: 1 KB = 1024 B = 2¹⁰ B
❌ BŁĄD: Obraz 1920×1080 × 24 bit = 49 766 400 bajtów
✅ POPRAWNIE: 1920 × 1080 × 24 / 8 = 6 220 800 B (÷8!)
❌ BŁĄD: Zapomnienie o odejmowaniu 2 hostów
/24 → 256 hostów — NIE! → 256 - 2 = 254 hostów
(adres sieci + broadcast nie mogą być hostami)