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 ❌ Trudny odczyt strumieniami, brak możliwości zapisu, nie działa zwykłe |
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
.exew folderzebin. OpcjaContentgwarantuje, ż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"?
-
Copy if newer(Rekomendowane): Kopiuje plik z projektu do folderubin/Debugtylko wtedy, gdy wprowadziłeś w nim zmiany w edytorze. Jeśli Twoja aplikacja dopisuje w trakcie działania nowe rekordy do pliku na dysku, kolejne wciśnięcie F5 nie zresetuje Twoich wpisów! -
Copy always: Przy każdym uruchomieniu bezwzględnie nadpisuje plik wbin/Debugwersją początkową. Jeśli program miał zapisywać stan, wszystkie zmiany wprowadzone w trakcie testów znikną.
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));
}
}