Pliki i zasoby w WPF: Dialogi systemowe, odczyt tekstu, zdjęcia i sekret Build Action pod INF.04

Przechodzimy do konkretów: jak sprawić, by Twoja aplikacja okienkowa przestała być tylko statyczną makietą, a zaczęła wchodzić w interakcję z dyskiem twardym?

W tym wpisie dowiesz się, jak poprawnie wywoływać okna dialogowe Windowsa (OpenFileDialog, SaveFileDialog, MessageBox), jak błyskawicznie czytać pliki tekstowe bez walki ze strumieniami oraz jak dynamicznie podmieniać zdjęcia w kontrolkach. Przede wszystkim jednak odkryjemy tajemnicę Build Action: Content oraz przełącznika Copy if newer - konfiguracji, która na egzaminie zawodowym INF.04 ratuje przed utratą punktów za brakujące pliki.

1. Dialogi systemowe: Nie każ użytkownikowi wpisywać ścieżek!

Zmuszanie użytkownika do ręcznego wklepywania ścieżki typu C:\Users\Jan\Dokumenty\dane.txt w pole tekstowe to proszenie się o literówki i awarię programu. W aplikacjach desktopowych standardem są systemowe okna dialogowe.

Wszystkie trzy kluczowe dialogi znajdziesz w bibliotece Microsoft.Win32 oraz System.Windows:

A. OpenFileDialog - wybieranie pliku z dysku

Okno dialogowe jest modalne - wywołujemy je metodą ShowDialog(), która zatrzymuje działanie okna głównego i zwraca true, jeśli użytkownik wybrał plik i kliknął "Otwórz".

using Microsoft.Win32; // Wymagana przestrzeń nazw dla okien dialogowych plików

private void WybierzPlik_Click(object sender, RoutedEventArgs e)
{
    OpenFileDialog openFileDialog = new OpenFileDialog();
    
    // Ustawiamy filtr rozszerzeń (Format: Nazwa|*.rozszerzenie|Nazwa2|*.rozszerzenie2)
    openFileDialog.Filter = "Pliki tekstowe (*.txt)|*.txt|Wszystkie pliki (*.*)|*.*";
    openFileDialog.Title = "Wybierz plik z danymi do wczytania";

    // ShowDialog() zwraca true tylko, gdy użytkownik potwierdzi wybór
    if (openFileDialog.ShowDialog() == true)
    {
        string wybranaSciezka = openFileDialog.FileName;
        txtSciezka.Text = wybranaSciezka;
    }
}

B. SaveFileDialog - wybieranie miejsca zapisu

Działa bliźniaczo do OpenFileDialog, ale pozwala użytkownikowi wpisać nową nazwę pliku:

SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "Plik tekstowy (*.txt)|*.txt";
saveFileDialog.DefaultExt = "txt";
saveFileDialog.FileName = "raport.txt"; // Podpowiedź domyślnej nazwy

if (saveFileDialog.ShowDialog() == true)
{
    string sciezkaDoZapisu = saveFileDialog.FileName;
    File.WriteAllText(sciezkaDoZapisu, "Przykładowa zawartość raportu");
}

C. MessageBox - szybkie komunikaty i potwierdzenia

Nie twórz nowego okna XAML tylko po to, by pokazać komunikat o błędzie lub zapytać o zgodę!

// Wariant 1: Zwykła informacja
MessageBox.Show("Dane zostały pomyślnie zapisane!", "Sukces", MessageBoxButton.OK, MessageBoxImage.Information);

// Wariant 2: Pytanie z weryfikacją wyboru użytkownika
MessageBoxResult wynik = MessageBox.Show(
    "Czy na pewno chcesz usunąć ten element?", 
    "Potwierdzenie operacji", 
    MessageBoxButton.YesNo, 
    MessageBoxImage.Warning);

if (wynik == MessageBoxResult.Yes)
{
    // Wykonaj usunięcie
}

// WAŻNE: Metoda .Show() ma wiele przeładowań i zależnie od tego, co chcemy wyświetlić możemy podać następujące parametry
MessageBox.Show("tekst dialogu", "tytuł dialogu", "Rodzaj przycisków z MessageBoxButton (domyślnie bez podania tego parametru tylko OK)", "Rodzaj ikony z MessageBoxImage (domyślnie bez ikony jeżeli nie podano tego parametru)");

2. Tajemnica Build Action: Dlaczego na INF.04 lepiej użyć Content?

Gdy w zadaniu egzaminacyjnym otrzymujesz plik hasla.txt lub folder ze zdjęciami obrazy/, pierwsze co robisz, to przeciągasz je do okna Solution Explorer w Visual Studio. W tym momencie decyduje się los punktów z zadania.

Pliki w projekcie posiadają właściwość Build Action (Akcja kompilacji):

Build Action Co robi Visual Studio? Gdzie trafia plik? Zalety i wady na egzaminie
Resource Wprasowuje plik wewnątrz pliku .exe. Wewnątrz binarki

✅ Zdjęcie zawsze jest w .exe.

❌ Trudny odczyt strumieniami, brak możliwości zapisu, nie działa zwykłe File.ReadAllText.

Content ⭐ Pozostawia plik jako fizyczny plik na dysku obok .exe. Folder wyjściowy (bin/Debug) ✅ 100% kompatybilności z File.ReadAllText, File.ReadAllLines, pętlami Directory.GetFiles i łatwa inspekcja.
None Ignoruje plik. Nigdzie (chyba że włączysz kopiowanie) Często domyślne dla zwykłych plików, ale mniej formalne niż Content.

🧠 Ważny detal:

Arkusz egzaminacyjny CKE rzadko precyzuje sposób osadzenia plików, ale egzaminator weryfikuje aplikację uruchamiając projekt w Visual Studio (F5) lub klikając bezpośrednio w plik .exe w folderze bin. Opcja Content gwarantuje, że pliki leżą dokładnie tam, gdzie spodziewa się ich kod C#.

3. Magiczny przełącznik: Copy to Output Directory

Samo wybranie Build Action: Content nie skopiuje jeszcze pliku do folderu uruchomieniowego! Musisz ustawić drugą opcję w oknie Properties (F4):

Dlaczego właśnie "Copy if newer", a nie "Copy always"?

4. Odczyt plików tekstowych: Całość czy linijka po linijce?

Zapomnij o skomplikowanym otwieraniu strumieni i pętlach while (!reader.EndOfStream). Klasa statyczna System.IO.File rozwiązuje 99% zadań egzaminacyjnych w jednej linijce.

 Wariant A: Cały plik na raz → File.ReadAllText()

Idealne do pól tekstowych z regulaminem, opisem zadania czy logami.

using System.IO;
using System.Text;

private void WczytajRegulamin()
{
    string sciezka = "regulamin.txt"; // Plik leży w folderze z plikiem .exe dzięki "Copy if newer"

    // Żelazna zasada egzaminacyjna: ZAWSZE sprawdzaj obecność pliku!
    if (File.Exists(sciezka))
    {
        // Encoding.UTF8 gwarantuje poprawne wyświetlanie polskich znaków: ą, ę, ś, ć...
        string zawartosc = File.ReadAllText(sciezka, Encoding.UTF8);
        txtEdytor.Text = zawartosc;
    }
    else
    {
        MessageBox.Show($"Nie odnaleziono pliku: {sciezka}", "Błąd", MessageBoxButton.OK, MessageBoxImage.Error);
    }
}

Wariant B: Linijka po linijce → File.ReadAllLines()

Klasyk zadań z bazami produktów, użytkowników czy pytań do testu. Zwraca wygodną tablicę string[].

private void WczytajKategorieDoListy()
{
    string sciezka = "kategorie.txt";

    if (File.Exists(sciezka))
    {
        string[] wiersze = File.ReadAllLines(sciezka, Encoding.UTF8);

        // Bezpośrednie zasilenie kontrolki ListBox danymi:
        lstKategorie.ItemsSource = wiersze;
    }
}

⚠️ Uwaga na pułapkę formatu CSV:

Jeśli wiersz zawiera dane oddzielone średnikiem (np. Jan;Kowalski;25), użyj metody .Split(';'). Pamiętaj o pomijaniu pustych linii na końcu pliku:

foreach (string linia in wiersze)
{
    if (string.IsNullOrWhiteSpace(linia)) continue; // Chroni przed IndexOutOfRangeException!
    string[] pola = linia.Split(';');
    // Przetwarzanie pól: pola[0], pola[1]...
}

5. Zdjęcia w WPF: Kontrolka Image i obiekt BitmapImage

Wyświetlanie grafiki w WPF różni się od Windows Forms - właściwość Source kontrolki Image nie przyjmuje bezpośrednio napisu string, ani obiektu z biblioteki System.Drawing.

Krok 1: Definicja w XAML

<!-- Stretch="Uniform" gwarantuje zachowanie proporcji grafiki bez jej rozciągania -->
<Image x:Name="imgPodglad" 
       Width="250" 
       Height="250" 
       Stretch="Uniform" 
       HorizontalAlignment="Center" 
       Margin="10" />

Krok 2: Dynamiczna zmiana zdjęcia w C#

Musimy utworzyć instancję klasy BitmapImage z przestrzeni System.Windows.Media.Imaging.

Sposób I: Ścieżka względna do pliku w projekcie (np. losowanie kostki do gry)

using System;
using System.Windows.Media.Imaging;

private void UstawZdjecieWzgledne(int numerKostki)
{
    // Konstruktor Uri wymaga zdefiniowania typu: UriKind.Relative!
    imgPodglad.Source = new BitmapImage(new Uri($"Assets/kosc_{numerKostki}.png", UriKind.Relative));
}

Sposób II: Ścieżka bezwzględna (np. pobrana prosto z okna OpenFileDialog)

private void WybierzZdjecieZDysku()
{
    OpenFileDialog dialog = new OpenFileDialog();
    dialog.Filter = "Obrazy (*.jpg;*.png)|*.jpg;*.png";

    if (dialog.ShowDialog() == true)
    {
        // Gdy mamy pełną ścieżkę typu "C:\...", podajemy UriKind.Absolute
        imgPodglad.Source = new BitmapImage(new Uri(dialog.FileName, UriKind.Absolute));
    }
}