DatePicker i Calendar
W lekcji 22 datę urodzenia wpisywało się w pole tekstowe i sprawdzało przez DateTime.TryParse. Wygodniej i bezpieczniej jest dać użytkownikowi kalendarz: nie pomyli formatu, nie wybierze 31 lutego, a program może z góry zablokować daty z przeszłości albo zajęte terminy. W tej lekcji poznasz pole daty z rozwijanym kalendarzem i kalendarz wyświetlany na stałe oraz policzysz noce, dni robocze i ceny.
Czego się dziś nauczysz
- Dodasz pole wyboru daty i odczytasz wybraną datę typu
DateTime?. - Ograniczysz wybór do zakresu dat i zablokujesz wybrane dni.
- Wyświetlisz kalendarz na stałe i odczytasz zaznaczony zakres dni.
- Policzysz liczbę dni między datami i przejdziesz pętlą po kolejnych dniach.
- Rozpoznasz weekend i sformatujesz datę z nazwą dnia tygodnia.
DatePicker
<StackPanel Margin="15" Width="220" HorizontalAlignment="Left">
<Label Content="_Data sprawdzianu:" Target="{Binding ElementName=Data}"/>
<DatePicker x:Name="Data" SelectedDateFormat="Long"
SelectedDateChanged="Data_SelectedDateChanged"/>
<TextBlock x:Name="Info" Margin="0,8,0,0"/>
</StackPanel>
rozwinięty kalendarz pola daty — obwódka to dzisiejszy dzień, wypełnienie — wybrany
Użytkownik może wybrać datę z kalendarza albo wpisać ją w pole. Wybraną datę zawiera SelectedDate typu DateTime? — tak jak IsChecked w lekcji 14, wartość może być pusta (null), gdy nic nie wybrano:
private void Data_SelectedDateChanged(object? sender, SelectionChangedEventArgs e)
{
if (!Data.SelectedDate.HasValue) // nic nie wybrano
{
Info.Text = "";
return;
}
DateTime d = Data.SelectedDate.Value; // zwykły DateTime
int dni = (d - DateTime.Today).Days;
Info.Text = $"{d:dddd} — za {dni} dni";
}
Data.SelectedDate = DateTime.Today.AddDays(7); // ustawienie z kodu
Data.SelectedDate = null; // wyczyszczenie
SelectedDateFormat="Long" wyświetla datę z nazwą dnia i miesiąca, Short (domyślny) — jako „15.10.2026”. Format zależy od ustawień regionalnych Windows.
Zakres i zablokowane dni
// tylko od dziś do końca roku szkolnego
Data.DisplayDateStart = DateTime.Today;
Data.DisplayDateEnd = new DateTime(2027, 6, 25);
// zablokowane dni (przekreślone, nie da się ich wybrać)
Data.BlackoutDates.Add(new CalendarDateRange(new DateTime(2026, 12, 23), new DateTime(2026, 12, 31)));
Data.BlackoutDates.Add(new CalendarDateRange(new DateTime(2026, 11, 11))); // jeden dzień
// czy dzień jest zablokowany?
bool zajety = Data.BlackoutDates.Contains(new DateTime(2026, 12, 24));
Dni spoza zakresu DisplayDateStart–DisplayDateEnd nie są pokazywane, a dni z BlackoutDates są przekreślone. Data wpisana ręcznie w zablokowany dzień zostanie odrzucona. Uwaga: dnia, który jest aktualnie wybrany, nie wolno dodać do BlackoutDates — program zakończy się wyjątkiem. Najpierw wyczyść wybór.
Calendar
Calendar to ten sam kalendarz, ale widoczny na stałe. Może zaznaczać więcej niż jeden dzień:
SelectionMode | Zaznaczanie |
|---|---|
SingleDate (domyślny) | jeden dzień; odczyt z SelectedDate |
SingleRange | jeden ciągły zakres — przeciągnięcie myszą lub Shift + klik |
MultipleRange | wiele zakresów — z Ctrl |
None | tylko podgląd, bez zaznaczania |
// w XAML: <Calendar x:Name="Kalendarz" SelectionMode="SingleRange"
// SelectedDatesChanged="Kalendarz_SelectedDatesChanged"/>
int ile = Kalendarz.SelectedDates.Count; // liczba zaznaczonych dni
DateTime pierwszy = Kalendarz.SelectedDates[0]; // kolejność = kolejność zaznaczania
Kalendarz.SelectedDates.Clear();
Kalendarz.SelectedDates.AddRange(od, doDnia); // zaznaczenie zakresu z kodu
Przy zakresie zaznaczonym „od tyłu” (od późniejszej daty do wcześniejszej) pierwszy element SelectedDates jest późniejszy — dlatego w przykładzie daty są porównywane i w razie potrzeby zamieniane.
Liczenie na datach
DateTime dzis = DateTime.Today; // dziś, godzina 00:00
DateTime termin = dzis.AddDays(14); // za dwa tygodnie
int dni = (termin - dzis).Days; // 14
bool weekend = termin.DayOfWeek == DayOfWeek.Saturday || termin.DayOfWeek == DayOfWeek.Sunday;
// pętla po kolejnych dniach (od włącznie, do — bez)
int robocze = 0;
for (DateTime d = dzis; d < termin; d = d.AddDays(1))
{
if (d.DayOfWeek != DayOfWeek.Saturday && d.DayOfWeek != DayOfWeek.Sunday)
{
robocze++;
}
}
Info.Text = $"{termin:dddd, d MMMM yyyy}"; // „czwartek, 22 października 2026”
Do porównań dat używaj DateTime.Today, a nie DateTime.Now — Now zawiera godzinę, więc „dziś o 14:30” jest późniejsze niż data z kalendarza „dziś” (godzina 00:00).
Rezerwacja noclegu w schronisku
Gość wybiera daty przyjazdu i wyjazdu w polach daty albo zaznacza pobyt w kalendarzu — oba sposoby są ze sobą zsynchronizowane. Zajęte noce są przekreślone. Program liczy noce, rozróżnia noce weekendowe (droższe) i blokuje rezerwację przy złych datach. Po rezerwacji wybrane noce stają się zajęte.
Noce: 4 (w tym weekendowe: 2)
Do zapłaty: 560,00 zł
pobyt czw–pon: 2 noce zwykłe (czw, nd) i 2 weekendowe (pt, sob)
<Window x:Class="Lekcja32.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Schronisko „Pod Śnieżką” — rezerwacja" SizeToContent="WidthAndHeight"
ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
<Grid Margin="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="260"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,0,15,0">
<Label Content="_Przyjazd:" Padding="0,0,0,2" Target="{Binding ElementName=Przyjazd}"/>
<DatePicker x:Name="Przyjazd" SelectedDateChanged="Data_Zmiana"/>
<Label Content="_Wyjazd:" Padding="0,8,0,2" Target="{Binding ElementName=Wyjazd}"/>
<DatePicker x:Name="Wyjazd" SelectedDateChanged="Data_Zmiana"/>
<TextBlock Text="Pt i sob: 160 zł za noc, pozostałe noce: 120 zł. Najwyżej 14 nocy."
TextWrapping="Wrap" Foreground="Gray" FontSize="11" Margin="0,8,0,0"/>
<Border x:Name="Wynik" Margin="0,12,0,0" Padding="10" CornerRadius="6"
Background="#FFF3F4F6">
<TextBlock x:Name="WynikTekst" TextWrapping="Wrap" Text="Wybierz daty pobytu."/>
</Border>
<Button x:Name="Rezerwuj" Content="Rezerwuj" Height="30" Margin="0,12,0,0"
IsEnabled="False" Click="Rezerwuj_Click"/>
</StackPanel>
<!-- kalendarz: można w nim zaznaczyć zakres przeciągnięciem myszy -->
<StackPanel Grid.Column="1">
<TextBlock Text="Albo zaznacz pobyt w kalendarzu:" Margin="0,0,0,4"/>
<Calendar x:Name="Kalendarz" SelectionMode="SingleRange"
SelectedDatesChanged="Kalendarz_SelectedDatesChanged"/>
<TextBlock Text="Daty przekreślone są już zajęte." Foreground="Gray" FontSize="11"/>
</StackPanel>
</Grid>
</Window>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Lekcja32;
public partial class MainWindow : Window
{
private const decimal CenaZwykla = 120m;
private const decimal CenaWeekend = 160m;
private const int MaksNocy = 14;
private bool synchronizacja = false; // true, gdy kod sam zmienia daty
public MainWindow()
{
InitializeComponent();
DateTime dzis = DateTime.Today;
// wybór tylko od dziś do pół roku naprzód
Przyjazd.DisplayDateStart = dzis;
Przyjazd.DisplayDateEnd = dzis.AddMonths(6);
Wyjazd.DisplayDateStart = dzis.AddDays(1);
Wyjazd.DisplayDateEnd = dzis.AddMonths(6);
Kalendarz.DisplayDateStart = dzis;
Kalendarz.DisplayDateEnd = dzis.AddMonths(6);
// noce już zarezerwowane przez innych gości
DodajZajete(dzis.AddDays(5), dzis.AddDays(6));
DodajZajete(dzis.AddDays(18), dzis.AddDays(21));
}
private void DodajZajete(DateTime od, DateTime doDnia)
{
// każda kontrolka dostaje własny obiekt zakresu
Kalendarz.BlackoutDates.Add(new CalendarDateRange(od, doDnia));
Przyjazd.BlackoutDates.Add(new CalendarDateRange(od, doDnia));
}
// Zmiana w którymkolwiek polu daty
private void Data_Zmiana(object? sender, SelectionChangedEventArgs e)
{
if (synchronizacja)
{
return;
}
// pokaż wybrany pobyt także w kalendarzu
synchronizacja = true;
Kalendarz.SelectedDates.Clear();
if (Przyjazd.SelectedDate.HasValue && Wyjazd.SelectedDate.HasValue)
{
DateTime od = Przyjazd.SelectedDate.Value;
DateTime ostatniaNoc = Wyjazd.SelectedDate.Value.AddDays(-1);
// kalendarz nie przyjmie zakresu z zajętymi dniami — zaznaczamy tylko wolny
if (ostatniaNoc >= od && WolneNoce(od, ostatniaNoc))
{
Kalendarz.SelectedDates.AddRange(od, ostatniaNoc);
}
}
synchronizacja = false;
Przelicz();
}
// Zaznaczenie zakresu w kalendarzu ustawia oba pola daty
private void Kalendarz_SelectedDatesChanged(object? sender, SelectionChangedEventArgs e)
{
if (synchronizacja || Kalendarz.SelectedDates.Count == 0)
{
return;
}
DateTime pierwsza = Kalendarz.SelectedDates[0];
DateTime ostatnia = Kalendarz.SelectedDates[Kalendarz.SelectedDates.Count - 1];
if (ostatnia < pierwsza) // zaznaczono „od tyłu”
{
DateTime pom = pierwsza;
pierwsza = ostatnia;
ostatnia = pom;
}
synchronizacja = true;
Przyjazd.SelectedDate = pierwsza;
Wyjazd.SelectedDate = ostatnia.AddDays(1); // wyjazd rano po ostatniej nocy
synchronizacja = false;
Przelicz();
}
private void Rezerwuj_Click(object sender, RoutedEventArgs e)
{
if (!Przyjazd.SelectedDate.HasValue || !Wyjazd.SelectedDate.HasValue)
{
return;
}
DateTime od = Przyjazd.SelectedDate.Value;
DateTime doDnia = Wyjazd.SelectedDate.Value;
MessageBox.Show($"Zarezerwowano pobyt {od:dd.MM.yyyy} – {doDnia:dd.MM.yyyy}.\n{WynikTekst.Text}",
"Rezerwacja", MessageBoxButton.OK, MessageBoxImage.Information);
// najpierw czyścimy wybór — zaznaczonej daty nie można oznaczyć jako zajętej
synchronizacja = true;
Przyjazd.SelectedDate = null;
Wyjazd.SelectedDate = null;
Kalendarz.SelectedDates.Clear();
synchronizacja = false;
DodajZajete(od, doDnia.AddDays(-1)); // te noce są teraz zajęte
Przelicz();
}
// Czy żadna noc z zakresu nie jest zajęta
private bool WolneNoce(DateTime od, DateTime ostatniaNoc)
{
for (DateTime noc = od; noc <= ostatniaNoc; noc = noc.AddDays(1))
{
if (Kalendarz.BlackoutDates.Contains(noc))
{
return false;
}
}
return true;
}
private void Przelicz()
{
Rezerwuj.IsEnabled = false;
Wynik.Background = Brushes.MistyRose;
if (!Przyjazd.SelectedDate.HasValue || !Wyjazd.SelectedDate.HasValue)
{
WynikTekst.Text = "Wybierz daty pobytu.";
Wynik.Background = new SolidColorBrush(Color.FromRgb(243, 244, 246));
return;
}
DateTime od = Przyjazd.SelectedDate.Value;
DateTime doDnia = Wyjazd.SelectedDate.Value;
int noce = (doDnia - od).Days;
if (noce < 1)
{
WynikTekst.Text = "Wyjazd musi być co najmniej dzień po przyjeździe.";
return;
}
if (noce > MaksNocy)
{
WynikTekst.Text = $"Najdłuższy pobyt to {MaksNocy} nocy (wybrano {noce}).";
return;
}
// każda noc: sprawdzamy, czy wolna, i doliczamy cenę
decimal razem = 0;
int weekendowe = 0;
for (DateTime noc = od; noc < doDnia; noc = noc.AddDays(1))
{
if (Kalendarz.BlackoutDates.Contains(noc))
{
WynikTekst.Text = $"Noc {noc:dd.MM} jest już zajęta. Wybierz inny termin.";
return;
}
if (noc.DayOfWeek == DayOfWeek.Friday || noc.DayOfWeek == DayOfWeek.Saturday)
{
razem += CenaWeekend;
weekendowe++;
}
else
{
razem += CenaZwykla;
}
}
WynikTekst.Text = $"{od:ddd dd.MM} → {doDnia:ddd dd.MM}\n" +
$"Noce: {noce} (w tym weekendowe: {weekendowe})\n" +
$"Do zapłaty: {razem:N2} zł";
Wynik.Background = Brushes.Honeydew;
Rezerwuj.IsEnabled = true;
}
}
Co dzieje się po kolei
- Konstruktor ogranicza wybór do najbliższych sześciu miesięcy i oznacza dwa zajęte terminy, liczone od dzisiejszej daty — przykład działa niezależnie od tego, kiedy go uruchomisz.
- Zajęte noce są zablokowane w kalendarzu i w polu przyjazdu, ale nie w polu wyjazdu: można wyjechać rano w dniu, w którym wieczorem przyjeżdża następny gość.
- Pobyt to noce od dnia przyjazdu do dnia przed wyjazdem. Dlatego w kalendarzu zaznaczamy zakres „przyjazd … wyjazd − 1 dzień”, a z zakresu w kalendarzu wyliczamy wyjazd jako „ostatnia noc + 1 dzień”.
- Pola daty i kalendarz zmieniają się nawzajem. Pole
synchronizacjazapobiega nieskończonemu „odbijaniu” zmian: gdy kod sam ustawia daty, zdarzenia wychodzą od razu. WolneNocesprawdza dzień po dniu, czy zakres nie zawiera zablokowanych dni — kalendarz nie przyjąłby takiego zaznaczenia.Przeliczsprawdza kolejno: czy obie daty są wybrane, czy wyjazd jest po przyjeździe, czy pobyt nie jest za długi, a potem w pętli po nocach dolicza ceny i wykrywa zajęte noce.- Po rezerwacji najpierw czyścimy wybór, a dopiero potem dodajemy zarezerwowane noce do zablokowanych.
Sprawdź trzy rzeczy
- Przeciągnij myszą w kalendarzu przez zajęte dni. Zaznaczenie zatrzyma się na wolnych dniach.
- Wpisz w pole przyjazdu ręcznie datę z przeszłości i naciśnij Tab. Pole ją odrzuci.
- Zamień w
Rezerwuj_Clickkolejność: najpierwDodajZajete, potem czyszczenie. Rezerwacja zakończy się wyjątkiem.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
DatePicker.SelectedDate | DateTime? | Wybrana data albo null. |
HasValue, Value | bool, DateTime | Czy wartość jest; sama wartość. |
SelectedDateChanged | zdarzenie | Zmiana daty w polu; parametr SelectionChangedEventArgs. |
SelectedDateFormat | Short, Long | Sposób wyświetlania daty. |
DisplayDateStart, DisplayDateEnd | DateTime? | Zakres dostępnych dat. |
BlackoutDates | kolekcja | Zablokowane dni: Add(new CalendarDateRange(od, do)), Contains(d). |
Calendar.SelectionMode | wyliczenie | SingleDate, SingleRange, MultipleRange, None. |
SelectedDates | kolekcja | Zaznaczone dni; AddRange, Clear, Count. |
SelectedDatesChanged | zdarzenie | Zmiana zaznaczenia w kalendarzu. |
DayOfWeek | wyliczenie | Dzień tygodnia daty. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
DateTime d = Data.SelectedDate; | CS0266 — DateTime? to nie DateTime. Sprawdź HasValue i użyj Value. |
Data.SelectedDate.Value bez sprawdzenia | InvalidOperationException, gdy nic nie wybrano. |
dodanie wybranego dnia do BlackoutDates | ArgumentOutOfRangeException. Najpierw wyczyść wybór. |
porównanie z DateTime.Now | „Dziś” wybrane w kalendarzu jest wcześniejsze niż „teraz”. Używaj DateTime.Today. |
(do - od).Days + 1 jako liczba nocy | Noce to różnica dni bez dodawania 1; dni pobytu — z dodaniem. |
| synchronizacja dwóch kontrolek bez flagi | Zdarzenia wywołują się nawzajem — dziwne skoki albo przepełnienie stosu. |
Zadania
Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Daty wybieraj kontrolkami z tej lekcji, nie wpisuj w pola tekstowe. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Pole daty urodzenia (wybór tylko do dzisiaj, od 1900 roku). Po wyborze program pokazuje: wiek w latach (z poprawką na urodziny w tym roku), liczbę przeżytych dni, dzień tygodnia urodzenia („urodziłeś się w środę”) i liczbę dni do najbliższych urodzin (albo „Wszystkiego najlepszego!” w dniu urodzin).
pokaż wskazówkę
- Najbliższe urodziny: data urodzin w tym roku; jeśli już minęła — w przyszłym. Uwaga na osoby urodzone 29 lutego.
Lista rozwijana ważnych dat roku szkolnego (ferie zimowe, egzamin zawodowy, zakończenie roku — wpisz daty sam) i pozycja „inna data”, która odblokowuje pole daty. Program pokazuje: liczbę dni, pełnych tygodni i dni roboczych (bez sobót i niedziel) do wybranej daty oraz dzień tygodnia tej daty. Dla daty z przeszłości: „To już było 12 dni temu”.
pokaż wskazówkę
- Dni robocze policzysz pętlą po dniach jak w teorii.
Kalendarz z wyborem jednego dnia i formularz: przedmiot (lista) + przycisk Dodaj sprawdzian. Sprawdziany zapisuj w liście obiektów klasy Sprawdzian (data, przedmiot) i pokazuj na liście posortowanej według daty. Reguły szkolne: najwyżej jeden sprawdzian dziennie i najwyżej trzy w tygodniu (pn–pt); weekendy i przerwa świąteczna są zablokowane w kalendarzu. Złamanie reguły — komunikat z wyjaśnieniem.
pokaż wskazówkę
- Poniedziałek tygodnia daty
d:d.AddDays(-(((int)d.DayOfWeek + 6) % 7)). Sprawdzian jest w tym samym tygodniu, gdy ma ten sam poniedziałek. - Weekendy w
BlackoutDatesdodaj w pętli po dniach widocznego zakresu.
Kalkulator dla sekretariatu: kalendarz z wyborem zakresu (SingleRange). Program liczy w zaznaczonym zakresie: wszystkie dni, soboty i niedziele, święta ustawowe przypadające w dni robocze (zapisz w tablicy daty świąt w bieżącym roku) i wynikowe dni robocze. Pod spodem lista świąt wpadających w zakres z nazwami. Święta są w kalendarzu przekreślone, ale wliczają się do zakresu.
pokaż wskazówkę
- Zablokowanych dni nie da się zaznaczyć w
SingleRange— więc świąt nie blokuj, tylko pokaż je w liście obok.
Biblioteka: data wypożyczenia (pole daty, domyślnie dziś), liczba dni wypożyczenia (suwak 7–60) i wyliczony termin zwrotu. Jeśli termin wypada w niedzielę lub święto z tablicy, przesuwa się na najbliższy dzień roboczy biblioteki (pn–sob). Drugie pole daty: faktyczny zwrot. Za każdy dzień spóźnienia 0,50 zł, najwyżej 30 zł. Wynik: „Zwrot 4 dni po terminie — kara 2,00 zł”.
pokaż wskazówkę
- Przesunięcie terminu: pętla
while, dopóki dzień jest niedzielą albo świętem.
Lista nauczycieli i kalendarz z wyborem wielu dni (MultipleRange). Po wybraniu nauczyciela kalendarz pokazuje zaznaczone jego dni dyżurów; zmiana zaznaczenia zapisuje je. Każdego dnia roboczego dyżur może mieć najwyżej dwóch nauczycieli — przy próbie dodania trzeciego program cofa zaznaczenie tego dnia i pokazuje, kto już ma dyżur. Pod spodem tabela-podsumowanie: nauczyciel i liczba dyżurów w miesiącu, a dni robocze bez żadnego dyżuru są wypisane na czerwono.
pokaż wskazówkę
- Klasa
Nauczycielz listąList<DateTime> Dyzury. Zmieniając nauczyciela, odtwórz zaznaczenie kalendarza z jego listy — z flagą blokującą zapis w tym czasie.
Co trzeba zapamiętać
DatePicker— pole z rozwijanym kalendarzem;SelectedDatetoDateTime?:HasValue,Value.DisplayDateStart/Endograniczają zakres,BlackoutDatesblokują dni.Calendar— kalendarz na stałe;SelectionModeiSelectedDatesdla zakresów.- Różnica dat to
TimeSpan(.Days); pętlaforzAddDays(1)przechodzi po dniach. - Porównuj z
DateTime.Today; dwie zsynchronizowane kontrolki potrzebują flagi blokującej.
Dokumentacja: Microsoft Learn — DatePicker, Calendar.