Aplikacje desktopowe · Pliki
Praca z plikami w WPF: zapis i odczyt danych
Dane trzymane tylko w pamięci znikają po zamknięciu programu. Żeby przetrwały między uruchomieniami, trzeba je zapisać na dysku. Nauczysz się robić to w aplikacji okienkowej: odczyt przy starcie, zapis przy zamykaniu i okna wyboru pliku.
- około 50 minut
- WPF · File · JSON · SaveFileDialog
- INF.04.5.3
Po tej lekcji: wczytać dane przy starcie i zapisać je w zdarzeniu Closing, sprawdzić File.Exists, użyć SaveFileDialog i zadbać o poprawną ścieżkę pliku.
Zacznijmy od notatnika
Aplikacja, która „pamięta”
Notatnik zapisuje treść przy zamknięciu i wczytuje ją przy następnym uruchomieniu. Użytkownik ma wrażenie, że program pamięta jego pracę, ale naprawdę za każdym razem odczytuje ją z pliku.
Zasady pracy z plikami w WPF są takie same jak w aplikacji konsolowej: używasz File.ReadAllText, File.WriteAllText i System.Text.Json. Zmienia się tylko to, kiedy je wywołujesz: odczyt przy starcie okna, zapis przy jego zamykaniu albo po każdej zmianie.
Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji
1 · Sprawdź w działaniu
Kiedy dane przetrwają
Uruchom program, dodaj kilka książek, zamknij okno i uruchom program ponownie. Potem wyłącz zapis w Closing i powtórz: dane przepadną. Wyłącz też File.Exists na świeżym starcie, aby zobaczyć awarię.
Dane są bezpieczne dopiero wtedy, gdy trafią na dysk. Wszystko, co zostało tylko w pamięci, znika po zamknięciu programu.
2 · Zapis i odczyt
MainWindow.xaml.cs
Trzy zadania: wczytać dane przy starcie, zapisać przy zamykaniu i pozwolić użytkownikowi wyeksportować je do wybranego pliku.
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Windows;
using BibliotekaApp.Modele;
using Microsoft.Win32;
namespace BibliotekaApp;
public partial class MainWindow : Window
{
// Plik obok programu (a nie w „bieżącym katalogu”, który bywa inny)
private static readonly string PlikDanych = Path.Combine(AppContext.BaseDirectory, "ksiazki.json");
public ObservableCollection<Ksiazka> Ksiazki { get; set; } = new();
public MainWindow()
{
InitializeComponent();
WczytajDane();
ListaKsiazek.ItemsSource = Ksiazki;
// Zapisz dane automatycznie przy zamykaniu okna
Closing += (s, e) => ZapiszDane();
}
private void WczytajDane()
{
if (!File.Exists(PlikDanych))
{
return; // pierwsze uruchomienie: pliku jeszcze nie ma
}
string json = File.ReadAllText(PlikDanych);
var wczytane = JsonSerializer.Deserialize<Ksiazka[]>(json);
if (wczytane != null)
{
foreach (var k in wczytane)
{
Ksiazki.Add(k);
}
}
}
private void ZapiszDane()
{
string json = JsonSerializer.Serialize(Ksiazki, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(PlikDanych, json);
}
private void PrzyciskZapisz_Click(object sender, RoutedEventArgs e)
{
ZapiszDane();
}
private void PrzyciskEksportuj_Click(object sender, RoutedEventArgs e)
{
var dialog = new SaveFileDialog
{
Filter = "Pliki tekstowe (*.txt)|*.txt",
FileName = "eksport_ksiazek.txt"
};
if (dialog.ShowDialog() == true)
{
string tekst = string.Join("\n", Ksiazki.Select(k => $"{k.Tytul};{k.Autor};{k.Status}"));
File.WriteAllText(dialog.FileName, tekst);
}
}
}
Path.Combine(AppContext.BaseDirectory, "ksiazki.json")Plik leży obok programu. Sama nazwa"ksiazki.json"oznacza „bieżący katalog”, który zależy od sposobu uruchomienia i bywa inny niż myślisz.File.Exists(PlikDanych)Przy pierwszym uruchomieniu pliku jeszcze nie ma, więc metoda kończy się bez błędu.JsonSerializer.Deserialize<Ksiazka[]>(json)Zamienia tekst JSON z powrotem na tablicę obiektów. Wynik może byćnull, więc sprawdzamy go.Closing += (s, e) => ZapiszDane()Zapis uruchamia się za każdym razem, gdy okno jest zamykane, nawet krzyżykiem.WriteIndented = truePlik JSON ma czytelne wcięcia, dzięki czemu można go obejrzeć w edytorze.dialog.ShowDialog() == trueOkno wyboru pliku jest modalne. Ścieżkę używamy dopiero po kliknięciu „Zapisz”, a nie „Anuluj”.string.Join("\n", ...)Łączy linie znakiem nowej linii.\nw cudzysłowie to znak końca linii, a samonbyłoby zwykłą literą.
3 · Wygląd
MainWindow.xaml
Tabela z książkami i dwa przyciski: ręczny zapis oraz eksport do pliku tekstowego.
<Window x:Class="BibliotekaApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Biblioteka" Height="380" Width="460">
<DockPanel Margin="15">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="0,10,0,0">
<Button Content="Zapisz teraz" Click="PrzyciskZapisz_Click" Margin="0,0,8,0" />
<Button Content="Eksportuj do TXT" Click="PrzyciskEksportuj_Click" />
</StackPanel>
<DataGrid x:Name="ListaKsiazek" AutoGenerateColumns="True" IsReadOnly="True" />
</DockPanel>
</Window>
Click="PrzyciskZapisz_Click"Ręczny zapis na wypadek awarii przed zamknięciem okna.
4 · Ściągawka
Najważniejsze klasy i metody
| Zadanie | Zapis w C# | Uwagi |
|---|---|---|
| Czy plik istnieje | File.Exists(sciezka) | Zawsze przed odczytem. |
| Odczyt całego tekstu | File.ReadAllText(sciezka) | Zwraca string. |
| Zapis całego tekstu | File.WriteAllText(sciezka, tekst) | Nadpisuje plik. |
| Obiekt → JSON | JsonSerializer.Serialize(obiekt) | Przestrzeń System.Text.Json. |
| JSON → obiekt | JsonSerializer.Deserialize<T>(json) | Wynik bywa null. |
| Wybór miejsca zapisu | new SaveFileDialog().ShowDialog() | Microsoft.Win32, sprawdź wynik == true. |
| Wybór pliku do otwarcia | new OpenFileDialog().ShowDialog() | Ścieżka w dialog.FileName. |
5 · Czego unikać
Cztery błędy z plikami
Odczyt bez `File.Exists`
string json = File.ReadAllText(PlikDanych);Przy pierwszym uruchomieniu: FileNotFoundException i awaria programu.
if (!File.Exists(PlikDanych)) return;
string json = File.ReadAllText(PlikDanych);Pierwszego uruchomienia nie poprzedza żaden zapis, więc pliku nie ma.
Ścieżka względna zależna od miejsca uruchomienia
const string PlikDanych = "ksiazki.json";Plik ląduje w innym katalogu, zależnie od sposobu uruchomienia, i dane „znikają”.
static readonly string PlikDanych = Path.Combine(AppContext.BaseDirectory, "ksiazki.json");Jawna ścieżka obok programu jest taka sama za każdym razem.
Ignorowanie wyniku `ShowDialog()`
dialog.ShowDialog();
File.WriteAllText(dialog.FileName, tekst);Po kliknięciu „Anuluj” FileName jest puste, więc zapis kończy się błędem.
if (dialog.ShowDialog() == true)
{
File.WriteAllText(dialog.FileName, tekst);
}Wynik true oznacza, że użytkownik wybrał plik i zatwierdził.
Zapis tylko po kliknięciu przycisku
// zapis tylko w PrzyciskZapisz_ClickUżytkownik zamyka okno krzyżykiem bez klikania „Zapisz” i traci dane.
Closing += (s, e) => ZapiszDane();Automatyczny zapis przy zamykaniu chroni przed utratą pracy.
6 · Sprawdź się
Kiedy odczyt, a kiedy zapis?
Cztery sytuacje z plikami.
Podsumowanie
Co warto zapamiętać
- Dane z pamięci znikają po zamknięciu programu. Żeby przetrwały, zapisz je na dysku.
- Odczyt robisz przy starcie okna, zapis przy zamykaniu (
Closing) lub po zmianie danych. - Przed odczytem sprawdzaj
File.Exists, bo przy pierwszym uruchomieniu pliku nie ma. - Ścieżkę buduj z
AppContext.BaseDirectory, a nie z samej nazwy pliku. - Wynik
SaveFileDialogiOpenFileDialogsprawdzaj jako== true.
Dalej na swoim koncie
Rozgrzewki, quiz i zadania są w biurku ucznia.
Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.
Zaloguj się →