Przejdź do treści
School IT / Przestrzeń do nauki

Pliki i okna dialogowe

Do tej pory wszystko, co użytkownik wpisał, znikało po zamknięciu programu. W tej lekcji program zacznie zapisywać dane do pliku i wczytywać je z powrotem — przez znajome okna systemu Windows „Otwórz” i „Zapisz jako”. Poznasz też try/catch, bo przy plikach błędy zdarzają się naprawdę: plik jest otwarty w innym programie, dysk jest tylko do odczytu, ktoś podał zły format.

XAML C# OpenFileDialog SaveFileDialog File try / catch CSV 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zapiszesz i odczytasz plik tekstowy w całości oraz wiersz po wierszu.
  • Obsłużysz błędy wejścia-wyjścia przez try/catch.
  • Pozwolisz użytkownikowi wybrać plik w oknie „Otwórz” i „Zapisz jako”.
  • Zapiszesz listę obiektów w pliku CSV i wczytasz ją z powrotem, pomijając błędne wiersze.
  • Zapytasz o zapis niezapisanych zmian przed zamknięciem okna.
TEORIA

Klasa File

Klasa File z przestrzeni nazw System.IO ma gotowe metody, które jednym wywołaniem otwierają plik, zapisują lub czytają całość i zamykają go:

MainWindow.xaml.cs
using System.IO;

// cały tekst naraz
File.WriteAllText("notatka.txt", Notatka.Text);       // tworzy albo nadpisuje
string tekst = File.ReadAllText("notatka.txt");

// wiersz po wierszu — tablica napisów
string[] wiersze = File.ReadAllLines("oceny.txt");
File.WriteAllLines("oceny.txt", wiersze);              // tablica albo List<string>

// dopisanie na końcu (np. dziennik zdarzeń)
File.AppendAllText("log.txt", $"{DateTime.Now:T} uruchomiono\n");

bool jest = File.Exists("notatka.txt");

// części ścieżki
string pelna = @"C:\Users\Ola\Documents\oceny.txt";
string nazwa = Path.GetFileName(pelna);         // "oceny.txt"
string rozszerzenie = Path.GetExtension(pelna); // ".txt"

Sama nazwa pliku (bez folderu) oznacza plik obok programu — w folderze bin\Debug\net10.0-windows. Pliki użytkownika lepiej zapisywać tam, gdzie on wskaże, dlatego zwykle używa się okien dialogowych. Polskie znaki są zapisywane w kodowaniu UTF-8.

Znak @ przed napisem wyłącza specjalne znaczenie \ — inaczej "C:\new" zawierałoby znak nowej linii (\n).

TEORIA

try i catch

Do tej pory błędnym danym zapobiegaliśmy przez TryParse i warunki. Przy plikach nie da się wszystkiego sprawdzić z góry: plik mógł zostać usunięty sekundę temu albo jest zablokowany przez Excela. Metody File zgłaszają wtedy wyjątek, który bez obsługi kończy program. Obsługuje się go tak:

MainWindow.xaml.cs
try
{
    // kod, który może się nie udać
    string tekst = File.ReadAllText(sciezka);
    Notatka.Text = tekst;
    Info.Text = "Wczytano.";        // wykona się tylko, gdy odczyt się udał
}
catch (Exception ex)
{
    // tu trafiamy, gdy cokolwiek w try zgłosiło wyjątek
    MessageBox.Show($"Nie udało się otworzyć pliku:\n{ex.Message}", "Błąd",
                    MessageBoxButton.OK, MessageBoxImage.Error);
}

Gdy w bloku try wystąpi wyjątek, reszta tego bloku jest pomijana, a program przechodzi do catch. ex.Message zawiera opis błędu (po polsku, jeśli Windows jest po polsku). Exception łapie każdy wyjątek; można też łapać węższe rodzaje, np. IOException albo UnauthorizedAccessException (brak uprawnień).

Nie zamiast sprawdzania

try/catch służy do sytuacji, których program nie może przewidzieć. Liczby z pola tekstowego nadal sprawdzaj przez TryParse — to prostsze i czytelniejsze niż łapanie wyjątku z int.Parse.

TEORIA

OpenFileDialog

Okna dialogowe plików są w przestrzeni nazw Microsoft.Win32. Tworzy się je jak zwykły obiekt, ustawia właściwości i pokazuje metodą ShowDialog, która czeka, aż użytkownik zamknie okno:

MainWindow.xaml.cs
using Microsoft.Win32;

OpenFileDialog okno = new OpenFileDialog();
okno.Title = "Wybierz plik z ocenami";
okno.Filter = "Pliki tekstowe (*.txt)|*.txt|Wszystkie pliki (*.*)|*.*";

if (okno.ShowDialog() == true)        // bool? — true tylko po kliknięciu „Otwórz”
{
    string sciezka = okno.FileName;   // pełna ścieżka wybranego pliku
    Notatka.Text = File.ReadAllText(sciezka);
}
Wybierz plik z ocenami✕
← → ↑
Ten komputer › Dokumenty › Szkoła
📁 Pulpit
📁 Dokumenty
📁 Pobrane
📄 oceny-1TI.txt
📄 oceny-2TI.txt
📄 plan.txt
Nazwa pliku:
oceny-2TI.txt
Pliki tekstowe (*.txt) ▾
OtwórzAnuluj

systemowe okno „Otwórz” — lista typów plików pochodzi z Filter

Filter to pary opis|wzorzec rozdzielone znakiem |. Kilka wzorców w jednej parze oddziela się średnikiem: "Obrazy|*.png;*.jpg". InitialDirectory ustawia folder startowy, a Multiselect = true pozwala wybrać wiele plików (lista w FileNames). Podobnie działa OpenFolderDialog — wybór folderu (FolderName).

TEORIA

SaveFileDialog

MainWindow.xaml.cs
SaveFileDialog okno = new SaveFileDialog();
okno.Filter = "Pliki tekstowe (*.txt)|*.txt";
okno.FileName = "notatka.txt";          // podpowiedź nazwy

if (okno.ShowDialog() == true)
{
    File.WriteAllText(okno.FileName, Notatka.Text);
}

Okno „Zapisz jako” samo pyta, czy nadpisać istniejący plik, i samo dopisuje rozszerzenie z filtru, gdy użytkownik go nie poda. Ani OpenFileDialog, ani SaveFileDialog nie czyta i nie zapisuje pliku — tylko zwraca ścieżkę. Pracę z plikiem wykonuje File.

TEORIA

Plik CSV — lista obiektów w pliku

Najprostszy sposób na zapis listy obiektów: jeden obiekt w jednym wierszu, pola rozdzielone średnikiem. Taki plik (CSV) otworzy też Excel. Zapis to pętla budująca wiersze, odczyt — Split(';') każdego wiersza i sprawdzenie pól:

uczniowie.csv
Nazwisko;Imie;Klasa
Nowak;Anna;2TI
Kowalski;Jan;2TI
MainWindow.xaml.cs
foreach (string wiersz in File.ReadAllLines(sciezka))
{
    string[] pola = wiersz.Split(';');
    if (pola.Length != 3)
    {
        continue;                    // zły wiersz — pomijamy
    }
    uczniowie.Add(new Uczen(pola[0], pola[1], pola[2]));
}

Średnik w danych zepsułby podział na pola, dlatego formularz w przykładzie nie przyjmuje nazwy ze średnikiem. Pierwszy wiersz z nazwami kolumn (nagłówek) pomaga człowiekowi, który otworzy plik, ale przy odczycie trzeba go pominąć.

TEORIA

Niezapisane zmiany

Program z plikami potrzebuje dwóch pól: ścieżki bieżącego pliku (albo null, gdy jeszcze go nie zapisano) i flagi „są zmiany”. Każda edycja ustawia flagę, zapis i odczyt ją zerują. Przed porzuceniem danych — nowy plik, otwarcie innego, zamknięcie okna (Closing z lekcji 8) — program pyta:

OdpowiedźCo robi program
TakZapisuje. Jeśli zapis się nie uda albo użytkownik anuluje okno „Zapisz jako” — zostaje.
NiePorzuca zmiany i kontynuuje.
AnulujNic nie robi — wraca do edycji.

Gwiazdka w tytule okna („ewidencja.csv *”) to powszechny znak niezapisanych zmian.

PRZYKŁAD

Ewidencja sprzętu w pracowni

Opiekun pracowni prowadzi spis sprzętu: numer inwentarzowy, nazwa, sala, stan. Ewidencję można zapisać do pliku CSV, otworzyć ponownie i wyeksportować raport sprzętu do naprawy jako plik tekstowy. Program pilnuje niezapisanych zmian. W folderze projektu, w podfolderze dane, jest gotowy plik sala14.csv z jednym celowo błędnym wierszem.

Ewidencja sprzętu — sala14.csv *–□✕
Plik
Nowa pozycja
Nr inwentarzowyNazwaSala
MYS-1401
Mysz optyczna
14
sprawnyDodaj
KOM-1401 Komputer Dell OptiPlex (s. 14)
KOM-1402 Komputer Dell OptiPlex (s. 14) ⚠ do naprawy
KOM-1403 Komputer Dell OptiPlex (s. 14)
MON-1401 Monitor 24 cale (s. 14)
PRZ-1401 Przełącznik 24 porty (s. 14)
PRO-1401 Projektor (s. 14) ⚠ do naprawy
Usuń zaznaczoneZmień stan
C:\Users\Ola\Documents\sala14.csvPozycji: 6, do naprawy: 2

wczytany plik sala14.csv (wiersz z drukarką był błędny i został pominięty), w trakcie dodawania nowej pozycji — gwiazdka w tytule oznacza niezapisane zmiany

Sprzet.cs — pełny plik
namespace Lekcja38;

// Jedna pozycja ewidencji sprzętu w pracowni
public class Sprzet
{
    public string NrInwentarzowy { get; set; }
    public string Nazwa { get; set; }
    public int Sala { get; set; }
    public bool Sprawny { get; set; }

    public Sprzet(string nrInwentarzowy, string nazwa, int sala, bool sprawny)
    {
        NrInwentarzowy = nrInwentarzowy;
        Nazwa = nazwa;
        Sala = sala;
        Sprawny = sprawny;
    }

    // Wiersz pliku CSV: KOM-014;Komputer Dell;14;tak
    public string DoCsv()
    {
        string stan = Sprawny ? "tak" : "nie";
        return $"{NrInwentarzowy};{Nazwa};{Sala};{stan}";
    }

    public override string ToString()
    {
        string stan = Sprawny ? "" : "  ⚠ do naprawy";
        return $"{NrInwentarzowy}  {Nazwa}  (s. {Sala}){stan}";
    }
}
MainWindow.xaml — pełny plik
<Window x:Class="Lekcja38.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ewidencja sprzętu" Width="620" Height="460" MinWidth="520" MinHeight="340"
        WindowStartupLocation="CenterScreen" Closing="Window_Closing" KeyDown="Window_KeyDown">

    <DockPanel>
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="_Plik">
                <MenuItem Header="_Nowa ewidencja" Click="Nowy_Click"/>
                <MenuItem Header="_Otwórz…" InputGestureText="Ctrl+O" Click="Otworz_Click"/>
                <MenuItem Header="_Zapisz" InputGestureText="Ctrl+S" Click="Zapisz_Click"/>
                <MenuItem Header="Zapisz _jako…" Click="ZapiszJako_Click"/>
                <Separator/>
                <MenuItem Header="_Raport do naprawy…" Click="Raport_Click"/>
                <Separator/>
                <MenuItem Header="Za_kończ" Click="Zakoncz_Click"/>
            </MenuItem>
        </Menu>

        <StatusBar DockPanel.Dock="Bottom">
            <StatusBarItem>
                <TextBlock x:Name="Plik" Text="(nowy plik)"/>
            </StatusBarItem>
            <Separator/>
            <StatusBarItem>
                <TextBlock x:Name="Liczba"/>
            </StatusBarItem>
        </StatusBar>

        <!-- formularz dodawania -->
        <GroupBox DockPanel.Dock="Top" Header="Nowa pozycja" Margin="10,8,10,4" Padding="6">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="110"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="60"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>

                <TextBlock Grid.Column="0" Text="Nr inwentarzowy"/>
                <TextBlock Grid.Column="1" Text="Nazwa" Margin="6,0,0,0"/>
                <TextBlock Grid.Column="2" Text="Sala" Margin="6,0,0,0"/>

                <TextBox x:Name="Nr" Grid.Row="1" Grid.Column="0"/>
                <TextBox x:Name="Nazwa" Grid.Row="1" Grid.Column="1" Margin="6,0,0,0"/>
                <TextBox x:Name="Sala" Grid.Row="1" Grid.Column="2" Margin="6,0,0,0"/>
                <CheckBox x:Name="Sprawny" Grid.Row="1" Grid.Column="3" Content="sprawny"
                          IsChecked="True" VerticalAlignment="Center" Margin="8,0,0,0"/>
                <Button Grid.Row="1" Grid.Column="4" Content="Dodaj" Width="70" Margin="8,0,0,0"
                        IsDefault="True" Click="Dodaj_Click"/>
            </Grid>
        </GroupBox>

        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="10,4,10,8">
            <Button Content="Usuń zaznaczone" Width="120" Click="Usun_Click"/>
            <Button Content="Zmień stan" Width="100" Margin="8,0,0,0" Click="ZmienStan_Click"/>
        </StackPanel>

        <ListBox x:Name="Lista" Margin="10,4,10,4" FontFamily="Consolas"/>
    </DockPanel>
</Window>
MainWindow.xaml.cs — pełny plik
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;

namespace Lekcja38;

public partial class MainWindow : Window
{
    const string Naglowek = "NrInwentarzowy;Nazwa;Sala;Sprawny";
    const string FiltrCsv = "Pliki CSV (*.csv)|*.csv|Wszystkie pliki (*.*)|*.*";

    private List<Sprzet> sprzet = new List<Sprzet>();
    private string? sciezka = null;     // null = ewidencja jeszcze nie była zapisana
    private bool zmiany = false;        // czy są niezapisane zmiany

    public MainWindow()
    {
        InitializeComponent();
        Odswiez();
    }

    // ---------- menu Plik ----------

    private void Nowy_Click(object sender, RoutedEventArgs e)
    {
        if (!PytajOZapis())
        {
            return;
        }
        sprzet.Clear();
        sciezka = null;
        zmiany = false;
        Odswiez();
    }

    private void Otworz_Click(object sender, RoutedEventArgs e)
    {
        if (!PytajOZapis())
        {
            return;
        }

        OpenFileDialog okno = new OpenFileDialog();
        okno.Title = "Otwórz ewidencję";
        okno.Filter = FiltrCsv;

        if (okno.ShowDialog() == true)      // true = użytkownik wybrał plik i kliknął Otwórz
        {
            Wczytaj(okno.FileName);
        }
    }

    private void Zapisz_Click(object sender, RoutedEventArgs e)
    {
        Zapisz();
    }

    private void ZapiszJako_Click(object sender, RoutedEventArgs e)
    {
        ZapiszJako();
    }

    private void Raport_Click(object sender, RoutedEventArgs e)
    {
        SaveFileDialog okno = new SaveFileDialog();
        okno.Title = "Zapisz raport";
        okno.Filter = "Pliki tekstowe (*.txt)|*.txt";
        okno.FileName = $"do-naprawy-{DateTime.Today:yyyy-MM-dd}.txt";

        if (okno.ShowDialog() != true)
        {
            return;
        }

        string raport = $"Sprzęt do naprawy — stan na {DateTime.Today:d}\n\n";
        int ile = 0;
        foreach (Sprzet s in sprzet)
        {
            if (!s.Sprawny)
            {
                raport += $"sala {s.Sala}: {s.NrInwentarzowy} {s.Nazwa}\n";
                ile++;
            }
        }
        raport += $"\nRazem: {ile}";

        try
        {
            File.WriteAllText(okno.FileName, raport);
            MessageBox.Show($"Zapisano raport ({ile} poz.).", "Raport",
                MessageBoxButton.OK, MessageBoxImage.Information);
        }
        catch (Exception ex)
        {
            MessageBox.Show($"Nie udało się zapisać raportu:\n{ex.Message}", "Błąd",
                MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }

    private void Zakoncz_Click(object sender, RoutedEventArgs e)
    {
        Close();        // pytanie o zapis zada Window_Closing
    }

    private void Window_Closing(object sender, CancelEventArgs e)
    {
        if (!PytajOZapis())
        {
            e.Cancel = true;
        }
    }

    private void Window_KeyDown(object sender, KeyEventArgs e)
    {
        if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.S)
        {
            Zapisz();
            e.Handled = true;
        }
        else if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.O)
        {
            Otworz_Click(sender, e);
            e.Handled = true;
        }
    }

    // ---------- pliki ----------

    private void Wczytaj(string plik)
    {
        string[] wiersze;
        try
        {
            wiersze = File.ReadAllLines(plik);
        }
        catch (Exception ex)
        {
            MessageBox.Show($"Nie udało się otworzyć pliku:\n{ex.Message}", "Błąd",
                MessageBoxButton.OK, MessageBoxImage.Error);
            return;
        }

        sprzet.Clear();
        int bledne = 0;
        foreach (string wiersz in wiersze)
        {
            if (wiersz == Naglowek || wiersz.Trim() == "")
            {
                continue;       // nagłówek i puste wiersze pomijamy
            }

            Sprzet? s = ZWiersza(wiersz);
            if (s == null)
            {
                bledne++;
            }
            else
            {
                sprzet.Add(s);
            }
        }

        sciezka = plik;
        zmiany = false;
        Odswiez();

        if (bledne > 0)
        {
            MessageBox.Show($"Pominięto błędne wiersze: {bledne}.", "Uwaga",
                MessageBoxButton.OK, MessageBoxImage.Warning);
        }
    }

    // Wiersz pliku → obiekt; null, gdy wiersz jest niepoprawny
    private Sprzet? ZWiersza(string wiersz)
    {
        string[] pola = wiersz.Split(';');
        if (pola.Length != 4 || pola[0].Trim() == "" || pola[1].Trim() == "")
        {
            return null;
        }
        if (!int.TryParse(pola[2], out int sala))
        {
            return null;
        }
        if (pola[3] != "tak" && pola[3] != "nie")
        {
            return null;
        }
        return new Sprzet(pola[0].Trim(), pola[1].Trim(), sala, pola[3] == "tak");
    }

    // Zwraca true, gdy zapis się udał
    private bool Zapisz()
    {
        if (sciezka == null)
        {
            return ZapiszJako();        // pierwszy zapis — trzeba zapytać o nazwę
        }
        return ZapiszDo(sciezka);
    }

    private bool ZapiszJako()
    {
        SaveFileDialog okno = new SaveFileDialog();
        okno.Title = "Zapisz ewidencję";
        okno.Filter = FiltrCsv;
        okno.FileName = "ewidencja.csv";

        if (okno.ShowDialog() != true)
        {
            return false;       // Anuluj
        }
        return ZapiszDo(okno.FileName);
    }

    private bool ZapiszDo(string plik)
    {
        List<string> wiersze = new List<string>();
        wiersze.Add(Naglowek);
        foreach (Sprzet s in sprzet)
        {
            wiersze.Add(s.DoCsv());
        }

        try
        {
            File.WriteAllLines(plik, wiersze);
        }
        catch (Exception ex)
        {
            MessageBox.Show($"Nie udało się zapisać pliku:\n{ex.Message}", "Błąd",
                MessageBoxButton.OK, MessageBoxImage.Error);
            return false;
        }

        sciezka = plik;
        zmiany = false;
        Odswiez();
        return true;
    }

    // Zwraca true, gdy można porzucić bieżącą ewidencję
    private bool PytajOZapis()
    {
        if (!zmiany)
        {
            return true;
        }

        MessageBoxResult odp = MessageBox.Show("Ewidencja ma niezapisane zmiany. Zapisać je?",
            "Ewidencja sprzętu", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);

        if (odp == MessageBoxResult.Yes)
        {
            return Zapisz();        // gdy zapis się nie uda lub zostanie anulowany — zostajemy
        }
        return odp == MessageBoxResult.No;
    }

    // ---------- edycja ----------

    private void Dodaj_Click(object sender, RoutedEventArgs e)
    {
        if (Nr.Text.Trim() == "" || Nazwa.Text.Trim() == "" ||
            !int.TryParse(Sala.Text, out int sala) || Nazwa.Text.Contains(';'))
        {
            MessageBox.Show("Podaj numer, nazwę (bez znaku ;) i numer sali.", "Nowa pozycja",
                MessageBoxButton.OK, MessageBoxImage.Warning);
            return;
        }

        sprzet.Add(new Sprzet(Nr.Text.Trim(), Nazwa.Text.Trim(), sala, Sprawny.IsChecked == true));
        Nr.Clear();
        Nazwa.Clear();
        Nr.Focus();
        zmiany = true;
        Odswiez();
    }

    private void Usun_Click(object sender, RoutedEventArgs e)
    {
        Sprzet? s = Lista.SelectedItem as Sprzet;
        if (s != null)
        {
            sprzet.Remove(s);
            zmiany = true;
            Odswiez();
        }
    }

    private void ZmienStan_Click(object sender, RoutedEventArgs e)
    {
        Sprzet? s = Lista.SelectedItem as Sprzet;
        if (s != null)
        {
            s.Sprawny = !s.Sprawny;
            zmiany = true;
            Odswiez();
            Lista.SelectedItem = s;
        }
    }

    // Lista, pasek stanu i tytuł okna
    private void Odswiez()
    {
        Lista.Items.Clear();
        int doNaprawy = 0;
        foreach (Sprzet s in sprzet)
        {
            Lista.Items.Add(s);
            if (!s.Sprawny)
            {
                doNaprawy++;
            }
        }

        string nazwa = sciezka == null ? "bez nazwy" : Path.GetFileName(sciezka);
        Title = $"Ewidencja sprzętu — {nazwa}{(zmiany ? " *" : "")}";
        Plik.Text = sciezka == null ? "(nowy plik)" : sciezka;
        Liczba.Text = $"Pozycji: {sprzet.Count}, do naprawy: {doNaprawy}";
    }
}

Co dzieje się po kolei

  • Pola sciezka i zmiany opisują stan pliku. Odswiez buduje listę, tytuł z gwiazdką i pasek stanu na ich podstawie.
  • Otworz_Click najpierw pyta o niezapisane zmiany, potem pokazuje okno „Otwórz”. Odczyt robi Wczytaj — w try, bo plik może być niedostępny.
  • Wczytaj pomija nagłówek i puste wiersze, a każdy pozostały wiersz zamienia w obiekt metodą ZWiersza. Błędne wiersze są liczone i zgłaszane jednym komunikatem.
  • Sprzet.DoCsv i ZWiersza są swoimi odwrotnościami: pierwsza robi z obiektu wiersz, druga — z wiersza obiekt.
  • Zapisz przy pierwszym zapisie przechodzi do ZapiszJako. Obie metody zwracają bool, dzięki czemu PytajOZapis wie, czy zapis naprawdę się udał.
  • Window_Closing anuluje zamknięcie, gdy użytkownik wybrał „Anuluj” albo zapis się nie powiódł. Zakoncz_Click po prostu woła Close, więc pytanie pojawia się także przy krzyżyku.
  • Raport to jeden napis budowany w pętli i zapisany przez File.WriteAllText pod nazwą z datą.

Sprawdź trzy rzeczy

  1. Otwórz dane/sala14.csv. Pojawi się komunikat o jednym pominiętym wierszu (stan „może”).
  2. Otwórz zapisany plik CSV w Excelu, zostaw go otwartego i zapisz ewidencję z programu. Zamiast awarii zobaczysz komunikat z catch.
  3. Dodaj pozycję i zamknij okno krzyżykiem, a w pytaniu wybierz Tak, a potem Anuluj w oknie zapisu. Okno się nie zamknie.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
File.ReadAllText, WriteAllTextmetodyOdczyt i zapis całego tekstu.
File.ReadAllLines, WriteAllLinesmetodyOdczyt do string[], zapis tablicy lub listy wierszy.
File.AppendAllTextmetodaDopisanie na końcu pliku.
File.ExistsboolCzy plik istnieje.
Path.GetFileName, GetExtensionmetodyNazwa i rozszerzenie z pełnej ścieżki.
try / catch (Exception ex)instrukcjaObsługa wyjątku; ex.Message — opis błędu.
OpenFileDialog, SaveFileDialogklasySystemowe okna wyboru pliku (Microsoft.Win32).
Filter, Title, FileName, InitialDirectorywłaściwościTypy plików, tytuł okna, wybrana ścieżka, folder startowy.
ShowDialog()bool?true — użytkownik zatwierdził wybór.
OpenFolderDialogklasaWybór folderu; wynik w FolderName.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
File bez using System.IO;CS0103 — nazwa File nie istnieje w bieżącym kontekście.
if (okno.ShowDialog())CS0266 — wynik to bool?; porównaj z true.
odczyt okno.FileName bez sprawdzenia wynikuPo „Anuluj” nazwa jest pusta — ArgumentException przy odczycie.
"C:\nowy\plik.txt"\n to nowa linia. Użyj @"C:\nowy\plik.txt".
zapis i odczyt plików bez try/catchZablokowany plik kończy program awarią.
pusty catch { }Błąd znika bez śladu — użytkownik myśli, że zapisał dane.
odczyt CSV bez sprawdzenia pola.LengthIndexOutOfRangeException na pierwszym krótszym wierszu.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Każdy zapis i odczyt pliku umieść w try/catch. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Notatnik z plikami★☆☆

Wielowierszowe pole i menu „Plik”: Nowy, Otwórz…, Zapisz, Zapisz jako…, Zakończ. Tytuł okna pokazuje nazwę pliku i gwiazdkę przy zmianach, a program pyta o zapis przed porzuceniem tekstu i przy zamknięciu okna. Filtr: pliki tekstowe i wszystkie pliki.

pokaż wskazówkę
  • Flagę zmian ustawiaj w TextChanged — ale nie wtedy, gdy tekst wstawił kod po odczycie pliku (np. ustaw flagę na false po wstawieniu).
ZAD 2Dziennik czytelniczy★☆☆

Formularz: tytuł książki, autor, data przeczytania (DatePicker), ocena 1–5 (Slider). Przycisk Dopisz dodaje wiersz do pliku przeczytane.csv obok programu (bez okna dialogowego) i odświeża listę wczytaną z tego pliku. Przy starcie program wczytuje plik, jeśli istnieje. Na dole: liczba książek i średnia ocena.

pokaż wskazówkę
  • Dopisywanie: File.AppendAllText z \n na końcu wiersza; przy starcie sprawdź File.Exists.
ZAD 3Analiza wyników sprawdzianu★★☆

Przycisk Wczytaj wyniki… otwiera plik CSV w formacie nazwisko;punkty (maks. 40 punktów). Program pokazuje listę z ocenami wyliczonymi z progów procentowych (np. 50% — dop., 70% — dst…), statystykę: liczbę uczniów, średnią punktów, liczbę każdej oceny oraz słupki z ProgressBar. Błędne wiersze są wypisane w osobnej liście z numerem wiersza. Przycisk Zapisz oceny… zapisuje plik nazwisko;punkty;ocena.

pokaż wskazówkę
  • Numer wiersza dostaniesz, przechodząc pętlą for po tablicy z ReadAllLines zamiast foreach.
ZAD 4Przeglądarka obrazów z folderu★★☆

Przycisk Wybierz folder… (OpenFolderDialog) wczytuje listę plików .png i .jpg z wybranego folderu. Kliknięcie na liście pokazuje obraz, nazwę pliku i rozmiar w KB; przyciski ◀ ▶ przechodzą między obrazami. Przycisk Zapisz listę… zapisuje do pliku tekstowego nazwy wszystkich obrazów z rozmiarami.

pokaż wskazówkę
  • Pliki z folderu: Directory.GetFiles(folder, "*.png") (osobno dla *.jpg); rozmiar: new FileInfo(sciezka).Length. Obraz z dysku: new BitmapImage(new Uri(sciezka)).
ZAD 5Fiszki do nauki słówek★★☆

Program wczytuje zestaw słówek z pliku slowo;tlumaczenie wybranego w oknie „Otwórz”. Tryb nauki: pokazuje słowo, przycisk „Pokaż tłumaczenie”, potem „Umiem” / „Nie umiem”. Słówka „nie umiem” wracają na koniec kolejki. Po skończeniu zestawu wynik i przycisk Zapisz trudne słówka…, który zapisuje do nowego pliku tylko te, które choć raz były „nie umiem”.

pokaż wskazówkę
  • Kolejka to zwykła List: bierzesz element o indeksie 0, usuwasz go i — gdy „nie umiem” — dodajesz na koniec.
ZAD 6Biblioteczka z zapisem (rozbudowa lekcji 23)★★★

Weź biblioteczkę klasową z lekcji 23 i dodaj: menu „Plik” z pełną obsługą plików CSV i niezapisanych zmian, automatyczny zapis kopii zapasowej biblioteka.bak.csv obok programu co 2 minuty (DispatcherTimer, informacja w pasku stanu „Kopia zapasowa: 10:42”), import książek z drugiego pliku CSV (bez duplikatów — ten sam tytuł i autor) z raportem „Dodano 12, pominięto 3 duplikaty, 1 błędny wiersz” oraz eksport listy wypożyczonych książek do pliku tekstowego.

pokaż wskazówkę
  • Pole Wypozyczajacy może być puste — w CSV to po prostu dwa średniki obok siebie, a Split zwróci pusty napis.
  • Kopia zapasowa nie może zmieniać ścieżki ani flagi zmian — to zapis „obok”, nie zwykłe „Zapisz”.
PODSUMOWANIE

Co trzeba zapamiętać

  • File.ReadAllLines/WriteAllLines i ReadAllText/WriteAllText — cały plik jednym wywołaniem (using System.IO).
  • Każdy dostęp do pliku w try/catch; komunikat z ex.Message, nie pusty catch.
  • OpenFileDialog/SaveFileDialog (Microsoft.Win32) tylko wybierają ścieżkę; ShowDialog() == true oznacza zatwierdzenie.
  • CSV: obiekt → wiersz z polami przez ;, odczyt przez Split ze sprawdzeniem liczby i formatu pól.
  • Ścieżka bieżącego pliku + flaga zmian + pytanie Tak/Nie/Anuluj przed porzuceniem danych.

Dokumentacja: Microsoft Learn — okna dialogowe plików, File.

Postęp zapisuje się w tej przeglądarce.