Tworzenie aplikacji WPF z bazą SQLite

Czyli jak połączyć okno, logikę programu i prawdziwą bazę danych.


1. Dlaczego w aplikacjach potrzebna jest baza danych?

W praktycznie każdej aplikacji trudno obyć się bez przechowywania danych.
Na przykład:

  • lista produktów,
  • lista klientów,
  • zamówienia, ceny, stany magazynowe,
  • historia działań.

Gdyby program trzymał wszystko w pamięci RAM, po zamknięciu aplikacji wszystko by znikało.
Dlatego używa się baz danych trwałych zapisów na dysku.

Na egzaminie INF.04 świetnie sprawdza się SQLite, bo:

  • działa bez internetu,
  • nie trzeba instalować serwera,
  • cała baza to jeden plik .db,
  • WPF i WinForms współpracują z nim bez problemu.

Dzisiaj zobaczysz pełną aplikację: dodawanie, edycję, usuwanie, wyświetlanie i wyszukiwanie danych.


2. Zanim zaczniemy pisać — jak to w ogóle działa?

Najpierw warto zrozumieć cały „łańcuch”, bo wtedy kod stanie się prosty:

[UI – XAML] → [Logika okna – XAML.cs] → [Logika bazy – BazaDanych.cs] → [plik sklep.db]

Każda część ma swoją funkcję:

XAML

Tu jest wygląd aplikacji: pola tekstowe, przyciski, tabela.
Nic nie liczy — tylko pokazuje.

XAML.cs

Tu jest logika zachowania przycisków:

  • klik „Dodaj” → wyślij nowe dane do bazy
  • klik „Usuń” → usuń rekord
  • odśwież widok po każdej operacji

XAML.cs nie zna SQL – tylko wywołuje metody z klasy BazaDanych.

BazaDanych.cs

Tu jest prawdziwa praca:

  • połączenie z plikiem .db,
  • tworzenie tabeli, jeśli nie istnieje,
  • wykonywanie SQL-a: SELECT, INSERT, UPDATE, DELETE.

sklep.db

Po prostu plik na dysku, który przechowuje dane.


3. Podstawowe pojęcia (tak, żeby rozumieć kod SQL)

PojęcieWyjaśnienie
Baza danychPlik, w którym zapisane są informacje.
TabelaPodział danych na grupy (Produkty, Klienci…).
RekordJeden wiersz tabeli, np. jeden produkt.
Pole (kolumna)nazwa, cena, id…
PRIMARY KEYunikalne ID każdego rekordu.
AUTOINCREMENTsamodzielne nadawanie ID.

4. Najważniejsze polecenia SQL — te trzeba znać

1. SELECT – pobieranie danych
SELECT * FROM Produkty;
SELECT nazwa, cena FROM Produkty;
SELECT * FROM Produkty WHERE cena > 5;
SELECT * FROM Produkty WHERE cena < 10;
SELECT * FROM Produkty WHERE cena BETWEEN 4 AND 8;

-- wyszukiwanie po tekście
SELECT * FROM Produkty WHERE nazwa LIKE '%cola%';  
SELECT * FROM Produkty WHERE nazwa LIKE 'C%';      -- zaczyna się na C
SELECT * FROM Produkty WHERE nazwa LIKE '%a';      -- kończy się na a
SELECT * FROM Produkty WHERE nazwa LIKE '%or%';    -- zawiera „or”

-- sortowanie
SELECT * FROM Produkty ORDER BY cena ASC;
SELECT * FROM Produkty ORDER BY cena DESC;
SELECT * FROM Produkty ORDER BY cena ASC, nazwa ASC;

-- warunki z AND/OR
SELECT * FROM Produkty WHERE cena > 5 AND nazwa LIKE 'C%';
SELECT * FROM Produkty WHERE cena = 4 OR cena = 6;

-- statystyki
SELECT COUNT(*) FROM Produkty;
SELECT AVG(cena) FROM Produkty;
SELECT MIN(cena) FROM Produkty;
SELECT MAX(cena) FROM Produkty;
SELECT SUM(cena) FROM Produkty;

-- dodatkowe
SELECT ROUND(cena, 2) FROM Produkty;
SELECT UPPER(nazwa) FROM Produkty;
SELECT LOWER(nazwa) FROM Produkty;

-- informacje o tabeli
PRAGMA table_info(Produkty);
SELECT name FROM sqlite_master WHERE type='table';

2. INSERT – dodawanie danych
INSERT INTO Produkty(nazwa, cena) VALUES ('Chleb', 4.50);

3. UPDATE – edycja danych
UPDATE Produkty SET cena = 3.00 WHERE id = 1;
UPDATE Produkty SET cena = cena + 1;
UPDATE Produkty SET cena = cena - 0.5 WHERE id = 3;

4. DELETE – usuwanie danych
DELETE FROM Produkty WHERE id = 5;
DELETE FROM Produkty WHERE cena < 1;

5. Tworzenie projektu WPF

  1. Otwórz Visual Studio
  2. Nowy projekt → WPF App (.NET)
  3. Nazwa projektu: BazaDanychWPF

6. Dodanie pliku bazy danych sklep.db

Na egzaminie nie można ściągać nic z internetu, więc baza musi być dodana ręcznie.

  1. Pobierz plik sklep.db lub utwórz wcześniej swoją wersję
  2. Wklej go do projektu:
    Dodaj → Istniejący element → sklep.db
  3. Ustaw właściwości pliku:
  • Build Action → None
  • Copy to Output Directory → Copy always

Dzięki temu plik trafi do folderu aplikacji i program go znajdzie.

Dodajemy paczkę obsługi SQLite z paczka pakietów NuGet->
System.Data.SQLite.Core


7. Budujemy interfejs — MainWindow.xaml

Najpierw wytłumaczenie:

  • Pierwszy panel to pola tekstowe (nazwa, cena, id) → dane wejściowe.
  • Drugi panel to wyszukiwarka → filtruje tabelę.
  • Na dole jest DataGrid — automatyczna tabela WPF, która sama wyświetli dane z DataTable.
  • Przycisk ma np. Click="btnDodaj_Click" — czyli metoda w XAML.cs.

PEŁNY KOD MainWindow.xaml

<Window x:Class="BazaDanychWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Baza Danych WPF - Produkty" Height="500" Width="750">

    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- Górny panel -->
        <StackPanel Orientation="Horizontal" Margin="0,0,0,10">

            <!-- Panel danych -->
            <StackPanel Width="220">
                <TextBlock Text="Nazwa:"/>
                <TextBox x:Name="txtNazwa"/>

                <TextBlock Text="Cena:" Margin="0,10,0,0"/>
                <TextBox x:Name="txtCena"/>

                <TextBlock Text="ID (edycja/usuwanie):" Margin="0,10,0,0"/>
                <TextBox x:Name="txtID"/>

                <Button Content="Dodaj" Margin="0,10,0,0" Click="btnDodaj_Click"/>
                <Button Content="Zapisz zmiany" Click="btnZapisz_Click"/>
                <Button Content="Usuń" Click="btnUsun_Click"/>
            </StackPanel>

            <!-- Panel wyszukiwarki -->
            <StackPanel Margin="20,0,0,0" Width="200">
                <TextBlock Text="Szukaj nazwy:"/>
                <TextBox x:Name="txtSzukaj"/>

                <Button Content="Szukaj" Margin="0,10,0,0" Click="btnSzukaj_Click"/>
                <Button Content="Odśwież" Click="btnOdswiez_Click"/>
            </StackPanel>
        </StackPanel>

        <!-- Tabela -->
        <DataGrid x:Name="gridProdukty"
                  Grid.Row="1"
                  AutoGenerateColumns="True"
                  SelectionChanged="gridProdukty_SelectionChanged"/>
    </Grid>
</Window>

8. Logika okna — MainWindow.xaml.cs


Wytłumaczenie przed kodem

  • Tworzymy obiekt BazaDanych, żeby mieć dostęp do SQL.
  • WyswietlProdukty() ładuje dane do DataGrid.
  • Przyciski wywołują odpowiednie metody SQL (Dodaj, Edytuj, Usuń).
  • gridProdukty_SelectionChanged pozwala kliknąć rekord i wypełnić pola edycji.

PEŁNY KOD MainWindow.xaml.cs

using System;
using System.Data;
using System.Windows;
using System.Windows.Controls;

namespace BazaDanychWPF
{
    public partial class MainWindow : Window
    {
        // Obiekt odpowiedzialny za pracę z bazą danych
        BazaDanych baza = new BazaDanych();

        public MainWindow()
        {
            InitializeComponent();

            // Po uruchomieniu okna pobieramy dane z bazy
            WyswietlProdukty();
        }

        // Metoda wczytuje dane z bazy i ustawia je jako źródło dla tabeli
        private void WyswietlProdukty(string filtr = "")
        {
            gridProdukty.ItemsSource = baza.PobierzProdukty(filtr).DefaultView;
        }

        // Przycisk DODAJ – dodaje nowy rekord do bazy
        private void btnDodaj_Click(object sender, RoutedEventArgs e)
        {
            if (string.IsNullOrWhiteSpace(txtNazwa.Text))
            {
                MessageBox.Show("Podaj nazwę!");
                return;
            }

            // Sprawdzamy, czy cena jest poprawną liczbą
            if (!double.TryParse(txtCena.Text, out double cena))
            {
                MessageBox.Show("Cena musi być liczbą.");
                return;
            }

            // Wywołanie metody z klasy BazaDanych
            baza.DodajProdukt(txtNazwa.Text, cena);

            // Odświeżenie tabeli
            WyswietlProdukty();
        }

        // Przycisk USUŃ – usuwa rekord na podstawie ID
        private void btnUsun_Click(object sender, RoutedEventArgs e)
        {
            if (!int.TryParse(txtID.Text, out int id))
            {
                MessageBox.Show("Podaj poprawne ID!");
                return;
            }

            baza.UsunProdukt(id);
            WyswietlProdukty();
        }

        // Przycisk ZAPISZ – zapisuje nowe wartości do wybranego rekordu
        private void btnZapisz_Click(object sender, RoutedEventArgs e)
        {
            if (!int.TryParse(txtID.Text, out int id))
            {
                MessageBox.Show("Podaj ID!");
                return;
            }

            if (!double.TryParse(txtCena.Text, out double cena))
            {
                MessageBox.Show("Cena musi być liczbą.");
                return;
            }

            baza.EdytujProdukt(id, txtNazwa.Text, cena);
            WyswietlProdukty();
        }

        // Po kliknięciu rekordu w tabeli wypełniamy pola edycji
        private void gridProdukty_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (gridProdukty.SelectedItem is DataRowView row)
            {
                txtID.Text = row["id"].ToString();
                txtNazwa.Text = row["nazwa"].ToString();
                txtCena.Text = row["cena"].ToString();
            }
        }

        // Wyszukiwanie po fragmencie nazwy
        private void btnSzukaj_Click(object sender, RoutedEventArgs e)
        {
            WyswietlProdukty(txtSzukaj.Text);
        }

        // Przywrócenie pełnej listy produktów
        private void btnOdswiez_Click(object sender, RoutedEventArgs e)
        {
            WyswietlProdukty();
        }
    }
}

9. Logika bazy — BazaDanych.cs

Tutaj dzieją się trzy ważne rzeczy:
1. Tworzenie połączenia

Każda operacja na bazie musi mieć:

using var con = new SQLiteConnection(sciezka);
con.Open();

using sprawia, że połączenie zamknie się automatycznie.


2. Tworzenie tabeli na starcie

Program sam tworzy tabelę, jeśli jej nie ma — idealne rozwiązanie egzaminacyjne.

3. Wykonywanie SQL

INSERT / DELETE / UPDATE używają:

cmd.ExecuteNonQuery();

SELECT używa:

cmd.ExecuteReader();

a dane są ładowane do DataTable.


PEŁNY KOD BazaDanych.cs

using System;
using System.Data;
using System.Data.SQLite;

namespace BazaDanychWPF
{
    public class BazaDanych
    {
        // Ścieżka do pliku bazy danych (w katalogu aplikacji)
        private string sciezka =
            $"Data Source={AppDomain.CurrentDomain.BaseDirectory}sklep.db;Version=3;"; 

//***

        public BazaDanych()
        {
            // Przy starcie programu tworzymy tabelę, jeśli jej nie ma
            UtworzTabeleJesliNieIstnieje();
        }

        // Tworzenie tabeli w bazie – tylko jeśli nie istnieje
        private void UtworzTabeleJesliNieIstnieje()
        {
            using var con = new SQLiteConnection(sciezka);
            con.Open();

            string sql =
                "CREATE TABLE IF NOT EXISTS Produkty (" +
                "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                "nazwa TEXT NOT NULL, " +
                "cena REAL NOT NULL);";

            using var cmd = new SQLiteCommand(sql, con);
            cmd.ExecuteNonQuery(); // wykonanie polecenia SQL
        }

        // Pobieranie danych z bazy (SELECT)
        public DataTable PobierzProdukty(string filtr = "")
        {
            using var con = new SQLiteConnection(sciezka);
            con.Open();

            string sql = "SELECT * FROM Produkty";

            // Filtr wyszukiwania
            if (!string.IsNullOrWhiteSpace(filtr))
                sql += " WHERE nazwa LIKE @f";

            using var cmd = new SQLiteCommand(sql, con);

            if (!string.IsNullOrWhiteSpace(filtr))
                cmd.Parameters.AddWithValue("@f", "%" + filtr + "%");

            using SQLiteDataReader r = cmd.ExecuteReader();

            // Wczytanie wyników SELECT do tabeli w pamięci
            DataTable dt = new DataTable();
            dt.Load(r);
            return dt;
        }

        // Dodawanie rekordu
        public void DodajProdukt(string nazwa, double cena)
        {
            using var con = new SQLiteConnection(sciezka);
            con.Open();

            string sql = "INSERT INTO Produkty(nazwa, cena) VALUES (@n,@c)";
            using var cmd = new SQLiteCommand(sql, con);

            cmd.Parameters.AddWithValue("@n", nazwa);
            cmd.Parameters.AddWithValue("@c", cena);

            cmd.ExecuteNonQuery();
        }

        // Usuwanie rekordu po ID
        public void UsunProdukt(int id)
        {
            using var con = new SQLiteConnection(sciezka);
            con.Open();

            string sql = "DELETE FROM Produkty WHERE id=@id";
            using var cmd = new SQLiteCommand(sql, con);

            cmd.Parameters.AddWithValue("@id", id);

            cmd.ExecuteNonQuery();
        }

        // Edycja rekordu
        public void EdytujProdukt(int id, string nazwa, double cena)
        {
            using var con = new SQLiteConnection(sciezka);
            con.Open();

            string sql = "UPDATE Produkty SET nazwa=@n, cena=@c WHERE id=@id";
            using var cmd = new SQLiteCommand(sql, con);

            cmd.Parameters.AddWithValue("@n", nazwa);
            cmd.Parameters.AddWithValue("@c", cena);
            cmd.Parameters.AddWithValue("@id", id);

            cmd.ExecuteNonQuery();
        }
    }
}

*** „Na egzaminie zamiast własnego sklep.db ustaw w connection stringu ścieżkę do pliku bazy podanej w treści zadania (np. Data Source=C:\\Egzamin\\baza\\egzamin.db).”


10. Najpotrzebniejsze komendy C# i SQL

CelKomenda
Połączenie z baząnew SQLiteConnection(...)
Otwieraniecon.Open()
Wykonywanie SELECTcmd.ExecuteReader()
Wykonywanie INSERT/DELETE/UPDATEcmd.ExecuteNonQuery()
Parametry SQLcmd.Parameters.AddWithValue("@x", val)
Ładowanie wynikówdt.Load(reader)
Autoużycie połączeńusing var con = ...

Hmmmmmm……

11. Rozbudowa bazy danych – co by było, gdybyśmy chcieli dodać koszyk zamówień?

Do tej pory aplikacja obsługiwała jedną tabelę – Produkty.
Pozwalała ona dodawać, edytować, usuwać i wyszukiwać produkty w bazie danych.

Co by było jednak, gdybyśmy chcieli stworzyć prosty koszyk zamówień, podobny do tego, który znamy ze sklepów internetowych?

W takiej sytuacji jedna tabela już nie wystarczy.


11.1. Nowa koncepcja – druga tabela w bazie danych

Aby stworzyć koszyk, potrzebujemy informacji:

  • jaki produkt został dodany,
  • w jakiej ilości.

Nie chcemy jednak powielać danych (nazwy i ceny produktów), bo:

  • dane już istnieją w tabeli Produkty,
  • zmiana ceny produktu powinna być widoczna wszędzie.

👉 Dlatego tworzymy drugą tabelę, która odwołuje się do produktów przez ich ID.


11.2. Struktura nowej tabeli – Koszyk

Dodajemy tabelę Koszyk o następującej strukturze:

KolumnaOpis
idunikalny identyfikator wpisu w koszyku
produkt_idID produktu z tabeli Produkty
iloscilość danego produktu

📌 Tabela Koszyk nie przechowuje nazwy ani ceny produktu.
📌 Przechowuje tylko informację, który produkt i w jakiej ilości znajduje się w koszyku.

To jest najprostszy i poprawny model relacji danych na poziomie INF.04.


11.3. Rozbudowa bazy – tworzenie drugiej tabeli

Rozbudowę zaczynamy od warstwy bazy danych, czyli pliku BazaDanych.cs.

📄 Plik: BazaDanych.cs

W metodzie UtworzTabeleJesliNieIstnieje() dopiszemy tworzenie drugiej tabeli:

// Tworzenie tabeli koszyka – tylko jeśli nie istnieje
string sqlKoszyk =
    "CREATE TABLE IF NOT EXISTS Koszyk (" +
    "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
    "produkt_id INTEGER NOT NULL, " +
    "ilosc INTEGER NOT NULL);";

using var cmd2 = new SQLiteCommand(sqlKoszyk, con);
cmd2.ExecuteNonQuery();

Wyjaśnienie:

  • CREATE TABLE IF NOT EXISTS – tabela zostanie utworzona tylko raz,
  • produkt_id – przechowuje ID produktu z tabeli Produkty,
  • brak kluczy obcych – relację realizujemy w kodzie aplikacji.

11.4. Dodawanie produktu do koszyka

Następnie dodajemy metodę umożliwiającą zapis produktu do koszyka.

📄 Plik: BazaDanych.cs

// Dodaje wybrany produkt do koszyka
public void DodajDoKoszyka(int produktId, int ilosc)
{
    using var con = new SQLiteConnection(sciezka);
    con.Open();

    string sql =
        "INSERT INTO Koszyk (produkt_id, ilosc) " +
        "VALUES (@pid, @ilosc)";

    using var cmd = new SQLiteCommand(sql, con);
    cmd.Parameters.AddWithValue("@pid", produktId);
    cmd.Parameters.AddWithValue("@ilosc", ilosc);

    cmd.ExecuteNonQuery();
}

Wyjaśnienie:

  • zapisujemy ID produktu, a nie jego nazwę,
  • ilość jest przechowywana jako liczba,
  • używamy parametrów SQL (bezpieczne rozwiązanie).

11.5. Pobieranie koszyka – połączenie danych z dwóch tabel

Aby wyświetlić koszyk w aplikacji, potrzebujemy:

  • nazwy produktu,
  • ceny,
  • ilości,
  • wartości pozycji (cena × ilość).

W tym miejscu łączymy dane z dwóch tabel.

📄 Plik: BazaDanych.cs

// Pobiera zawartość koszyka wraz z danymi produktu
public DataTable PobierzKoszyk()
{
    using var con = new SQLiteConnection(sciezka);
    con.Open();

    string sql =
        "SELECT Koszyk.id, Produkty.nazwa, Produkty.cena, Koszyk.ilosc, " +
        "(Produkty.cena * Koszyk.ilosc) AS suma " +
        "FROM Koszyk " +
        "JOIN Produkty ON Koszyk.produkt_id = Produkty.id";

    using var cmd = new SQLiteCommand(sql, con);
    using SQLiteDataReader r = cmd.ExecuteReader();

    DataTable dt = new DataTable();
    dt.Load(r);
    return dt;
}

Wyjaśnienie:

  • JOIN łączy dane z tabel Koszyk i Produkty,
  • łączenie odbywa się przez produkt_id,
  • kolumna suma obliczana jest bezpośrednio w SQL.

📌 To jedyny moment, w którym używamy JOIN.


11.6. Podsumowanie rozbudowy bazy danych

Po tej modyfikacji aplikacja:

  • posiada dwie tabele w bazie danych,
  • wykorzystuje relację między tabelami,
  • tworzy koszyk zamówień,
  • nie powiela danych produktów,
  • działa całkowicie offline.

A TERAZ WYGLĄD i LOGIKA

11.7. Rozbudowa interfejsu – dodanie koszyka zamówień

Skoro baza danych została rozbudowana o drugą tabelę (Koszyk),
musimy umożliwić użytkownikowi:

  • dodanie produktu do koszyka,
  • wyświetlenie zawartości koszyka.

Zrobimy to bez tworzenia nowego okna, tylko przez rozbudowę istniejącego interfejsu.


11.7.1. Zmiany w pliku MainWindow.xaml

Dodajemy:

  • pole do wpisania ilości,
  • przycisk „Dodaj do koszyka”,
  • drugi DataGrid do wyświetlenia koszyka.

Nie usuwamy nic z poprzedniego interfejsu – tylko dopisujemy.


Fragment do dopisania w panelu danych (pod polem ID)

<TextBlock Text="Ilość:" Margin="0,10,0,0"/>
<TextBox x:Name="txtIlosc"/>

<Button Content="Dodaj do koszyka"
        Margin="0,10,0,0"
        Click="btnDodajDoKoszyka_Click"/>

Zmiana układu siatki (na samej górze pliku)

Zmieniamy definicję wierszy:

<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
    <RowDefinition Height="*"/>
</Grid.RowDefinitions>

Drugi DataGrid – koszyk (na dole okna)

<DataGrid x:Name="gridKoszyk"
          Grid.Row="2"
          Margin="0,10,0,0"
          AutoGenerateColumns="True"/>

Wyjaśnienie dla ucznia:

  • pierwszy DataGrid → lista produktów,
  • drugi DataGrid → koszyk,
  • koszyk nie jest edytowany ręcznie – tylko przez przycisk.

11.8. Logika koszyka – MainWindow.xaml.cs

Teraz musimy:

  • obsłużyć kliknięcie przycisku,
  • odświeżyć widok koszyka.

11.8.1. Metoda wyświetlania koszyka

Plik: MainWindow.xaml.cs

// Wyświetlenie zawartości koszyka
private void WyswietlKoszyk()
{
    gridKoszyk.ItemsSource = baza.PobierzKoszyk().DefaultView;
}

Analogicznie do WyswietlProdukty().


11.8.2. Dodawanie produktu do koszyka

private void btnDodajDoKoszyka_Click(object sender, RoutedEventArgs e)
{
    // Sprawdzenie, czy wybrano produkt
    if (!int.TryParse(txtID.Text, out int produktId))
    {
        MessageBox.Show("Wybierz produkt z listy.");
        return;
    }

    // Sprawdzenie ilości
    if (!int.TryParse(txtIlosc.Text, out int ilosc))
    {
        MessageBox.Show("Podaj poprawną ilość.");
        return;
    }

    // Dodanie produktu do koszyka
    baza.DodajDoKoszyka(produktId, ilosc);

    // Odświeżenie widoku koszyka
    WyswietlKoszyk();
}

CAŁOŚĆ XAML

<Window x:Class="BazaDanychWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Baza Danych WPF - Produkty i Koszyk"
        Height="650" Width="750">

    <Grid Margin="10">

        <!-- ZMIANA: dodajemy trzeci wiersz na koszyk -->
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <!-- NOWE -->
        </Grid.RowDefinitions>

        <!-- GÓRNY PANEL -->
        <StackPanel Orientation="Horizontal" Margin="0,0,0,10">

            <!-- LEWY PANEL: formularz produktu -->
            <StackPanel Width="220">
                <TextBlock Text="Nazwa:"/>
                <TextBox x:Name="txtNazwa"/>

                <TextBlock Text="Cena:" Margin="0,10,0,0"/>
                <TextBox x:Name="txtCena"/>

                <TextBlock Text="ID (edycja/usuwanie):" Margin="0,10,0,0"/>
                <TextBox x:Name="txtID"/>

                <Button Content="Dodaj"
                        Margin="0,10,0,0"
                        Click="btnDodaj_Click"/>

                <Button Content="Zapisz zmiany"
                        Click="btnZapisz_Click"/>

                <Button Content="Usuń"
                        Click="btnUsun_Click"/>

                <!-- ============================== -->
                <!-- NOWE ELEMENTY – KOSZYK -->
                <!-- ============================== -->

                <TextBlock Text="Ilość:"
                           Margin="0,20,0,0"/>
                <!-- NOWE -->
                <TextBox x:Name="txtIlosc"/>
                <!-- NOWE -->

                <Button Content="Dodaj do koszyka"
                        Margin="0,10,0,0"
                        Click="btnDodajDoKoszyka_Click"/>
                <!-- NOWE -->
            </StackPanel>

            <!-- PRAWY PANEL: wyszukiwanie -->
            <StackPanel Margin="20,0,0,0" Width="200">
                <TextBlock Text="Szukaj nazwy:"/>
                <TextBox x:Name="txtSzukaj"/>

                <Button Content="Szukaj"
                        Margin="0,10,0,0"
                        Click="btnSzukaj_Click"/>

                <Button Content="Odśwież"
                        Click="btnOdswiez_Click"/>
            </StackPanel>

        </StackPanel>

        <!-- TABELA PRODUKTÓW -->
        <DataGrid x:Name="gridProdukty"
                  Grid.Row="1"
                  Margin="0,0,0,10"
                  AutoGenerateColumns="True"
                  SelectionChanged="gridProdukty_SelectionChanged"/>

        <!-- NOWE: TABELA KOSZYKA -->
        <DataGrid x:Name="gridKoszyk"
                  Grid.Row="2"
                  AutoGenerateColumns="True"/>
        <!-- NOWE -->

    </Grid>
</Window>