INF.04 · Aplikacje mobilne · Projekt krok po kroku
Wizytówka MAUI — aplikacja dwuekranowa
Wpisujesz dane na pierwszym ekranie, naciskasz przycisk i oglądasz gotową wizytówkę na drugim. Zbuduj to sam: od projektu w Visual Studio, przez walidację i nawigację, aż po zdjęcie i plik PNG.
- 2 × 45 minut + dodatki
- C# + XAML · .NET MAUI 10
- Dwa ekrany, jeden projekt
Po tej lekcji: zbudować formularz, przekazać dane do drugiej strony, wrócić bez utraty formularza oraz wyjaśnić, który fragment kodu odpowiada za każdą czynność.
Dwie strony zamiast jednego długiego formularza
Wyobraź sobie szkolne spotkanie projektowe. Chcesz pokazać, kim jesteś i czym się zajmujesz. Pierwsza strona służy do wpisania danych. Druga pokazuje tylko gotową kartę — bez pól edycji.
To formularz lokalny, nie rejestracja konta: nie ma serwera, hasła ani bazy użytkowników. Do prób wpisuj dane fikcyjne. Na pierwszych 45 minutach zbuduj formularz i przejście tam i z powrotem. Na drugich dodaj zdjęcie i sprawdź eksport. Jeśli zabraknie czasu, eksport i rozszerzenia dokończ później.
Sprawdź w działaniu
Najpierw przejdź drogę danych
W modelu jeden przycisk aplikacji rozbiliśmy na trzy kroki. Sprawdź pola, zapisz je do schowka i dopiero wtedy otwórz wizytówkę. Potem wróć, zmień imię i przejdź ponownie. Sprawdź też puste nazwisko i same spacje.
PushAsync dokłada stronę na stos. PopAsync zdejmuje ją i odsłania ten sam formularz. Model nie uruchamia C# ani nie testuje systemowych okien wyboru plików.
Pliki do ćwiczenia
Pliki do własnego projektu
Końcowe pliki do porównania i README. Zachowaj szablon projektu oraz katalog Platforms.
1. Utwórz projekt i uruchom sam szablon
Pracujemy w Visual Studio 2026 na Windows, .NET 10 i .NET MAUI 10. Nie wybieraj .NET 8 lub 9 „zamiennie” — w starszych wersjach różnią się m.in. nazwy metod okien dialogowych. Instalację MAUI wykonaj w Visual Studio Installer, jeśli szablonu nie ma.
Zanim wkleisz kod: co do czego należy?
Otwórz Widok → Eksplorator rozwiązań. Strzałka obok pliku XAML rozwija jego plik C#. Wklejaj kod do wskazanego pliku, nie wszystko do MainPage.xaml.cs.
| Plik | Odpowiedzialność |
|---|---|
App.xaml.cs | Tworzy okno i stos NavigationPage. |
MainPage.xaml | Wygląd formularza: pola i przyciski. |
MainPage.xaml.cs | Reakcje na kliknięcia, walidacja, odczyt pól i wybór zdjęcia. |
DaneUzytkownika.cs | Wspólne dane w pamięci procesu. |
WizytowkaPage.xaml | Wygląd karty na drugiej stronie. |
WizytowkaPage.xaml.cs | Wypełnienie karty, powrót i eksport PNG. |
Cztery rzeczy, które łączą XAML z C#
Nie musisz znać na pamięć wszystkich kontrolek. Najpierw zrozum połączenie między nazwą elementu, jego właściwością i zdarzeniem.
ContentPage
Jedna strona aplikacji. W naszym projekcie są dwie: formularz i wizytówka.
Na przykład public partial class MainPage : ContentPage
x:Name
Nazwa kontrolki, pod którą C# może ją znaleźć po InitializeComponent(). To nie napis widoczny dla użytkownika.
Na przykład x:Name="entImie" → entImie.Text
Clicked
Zdarzenie kliknięcia przycisku. W XAML podajesz nazwę metody, a jej ciało piszesz w pliku C# tej samej strony.
Na przykład Clicked="OnPokazClicked"
partial + InitializeComponent()
Klasa strony jest składana z kilku części. InitializeComponent tworzy kontrolki opisane w XAML i łączy ich nazwy oraz zdarzenia z C#.
Na przykład Najpierw InitializeComponent();, potem odczyt lblFirma lub entImie.
2. Dodaj schowek na dane
Zastąp zawartość nowej klasy. To uproszczenie do małego projektu z jedną wizytówką. static oznacza wspólne właściwości, bez new DaneUzytkownika(). Nie jest to wzorzec zarządzania stanem całej dużej aplikacji.
namespace WizytowkaMaui;
// Schowek w pamięci procesu, nie baza danych i nie zapis na dysku.
public static class DaneUzytkownika
{
public static string? Imie { get; set; }
public static string? Nazwisko { get; set; }
public static string? Email { get; set; }
public static string? Telefon { get; set; }
public static string? Stanowisko { get; set; }
public static string? Firma { get; set; }
public static string? SciezkaZdjecia { get; set; }
public static void Wyczysc()
{
Imie = Nazwisko = Email = Telefon = Stanowisko = Firma = null;
SciezkaZdjecia = null;
}
}
public static string? Imie { get; set; }Publiczna właściwość tekstowa, wspólna dla stron.getodczytuje,setzapisuje.?dopuszczanull, czyli brak wartości — to inna wartość niż pusty tekst"".DaneUzytkownika.Imie = "Jan";Zapisujesz tekst. Później druga strona odczytuje tę samą właściwość.Wyczysc()Przywraca brak danych. Po zamknięciu procesu schowek także znika. Nie usuwa zdjęć ani wyeksportowanych plików PNG.
3. Uruchom NavigationPage zamiast AppShell
Zastąp plik App.xaml.cs. W szablonie startem bywa new AppShell(): tutaj wybieramy NavigationPage. AppShell może pozostać w projekcie, ale nie jest uruchamiany. Nie wkładaj NavigationPage do Shell i nie łącz w tej wersji dwóch sposobów nawigacji.
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
namespace WizytowkaMaui;
public partial class App : Application
{
public App()
{
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
var nawigacja = new NavigationPage(new MainPage())
{
BarBackgroundColor = Color.FromArgb("#19376E"),
BarTextColor = Colors.White
};
var okno = new Window(nawigacja) { Title = "Wizytówka MAUI" };
#if WINDOWS
okno.Width = 480;
okno.Height = 820;
#endif
return okno;
}
}
new NavigationPage(new MainPage())Tworzy stos i kładzie formularz jako pierwszą stronę. Bez tego zwykła ContentPage nie zapewni tej nawigacji PushAsync.CreateWindow(IActivationState? activationState)Metoda wywoływana przy tworzeniu okna aplikacji.overrideoznacza własną wersję metody z klasy Application. Parametr może być null; w tym projekcie go nie używamy.return new Window(...)Zwraca okno z wybraną zawartością.#if WINDOWSogranicza ustawienie rozmiaru do kompilacji Windows. Telefon sam zarządza rozmiarem ekranu.Color.FromArgb("#19376E")Tworzy kolor z zapisu szesnastkowego.Colors.Whiteto gotowy biały kolor. Pasek nawigacji można ostylować; nie zawsze jest niebieski.
4. Układ formularza, zanim zobaczysz cały XAML
Główna postać: jedna strona zawiera ScrollView, a on jeden układ z kontrolkami. Dwa przyciski obok siebie umieścimy w Grid. Nie zakładaj stałej wysokości całego formularza — na mniejszym ekranie będzie przewijany.
| Podstawowa forma | Co robi w tym projekcie |
|---|---|
<ScrollView>…</ScrollView> | Pozwala przewijać zawartość; ma jeden bezpośredni element potomny. |
<VerticalStackLayout Padding="20" Spacing="12"> | Układa elementy pionowo. Padding daje odstęp od brzegów, Spacing między elementami. |
<Entry x:Name="entImie" /> | Jednowierszowe pole; jego zawartość odczytasz przez .Text. |
<Label Text="Imię" /> | Napis opisujący pole. Placeholder nie zastępuje etykiety. |
<Border StrokeShape="RoundRectangle 60"> | Obramowanie z zaokrąglonym kształtem. Nie traktuj samego Border jako gwarancji przycięcia obrazu do koła na każdej platformie — sprawdź efekt, a w razie potrzeby użyj Clip. |
<Image Source="avatar.png" Aspect="AspectFill" /> | Wyświetla obraz; AspectFill wypełnia obszar i może obciąć jego brzegi. |
<Grid ColumnDefinitions="*,*"> | Dwie kolumny o takim samym udziale szerokości; Grid.Column="1" wybiera drugą. |
<BoxView HeightRequest="2" Color="#C8D2E6" /> | Prosty pasek pełniący rolę separatora. Rozmiary MAUI są jednostkami niezależnymi od gęstości, nie zawsze fizycznymi pikselami. |
Wklej wygląd pierwszej strony
Zastąp cały plik. Jeszcze nie uruchamiaj: zdarzenia z XAML dostaną metody w następnym kroku. x:Class="WizytowkaMaui.MainPage" musi odpowiadać nazwie przestrzeni i klasy w pliku C#.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="WizytowkaMaui.MainPage"
Title="Twoje dane" BackgroundColor="#F0F4F8">
<ScrollView>
<VerticalStackLayout Padding="20" Spacing="12">
<Label Text="Utwórz swoją wizytówkę" FontSize="24"
FontAttributes="Bold" TextColor="#19376E" />
<Label Text="To lokalny formularz, nie rejestracja konta."
TextColor="#34465E" />
<BoxView HeightRequest="2" Color="#C8D2E6" />
<Border Stroke="#7090C0" StrokeThickness="2"
StrokeShape="RoundRectangle 60" BackgroundColor="White"
WidthRequest="120" HeightRequest="120" HorizontalOptions="Center">
<Image x:Name="imgZdjecie" Source="avatar.png" Aspect="AspectFill"
SemanticProperties.Description="Wybrane zdjęcie do wizytówki" />
</Border>
<Button x:Name="btnWczytajZdjecie" Text="Wczytaj zdjęcie"
Clicked="OnWczytajZdjecieClicked" />
<Label Text="Imię (wymagane)" TextColor="#19376E" />
<Entry x:Name="entImie" Placeholder="np. Jan" MaxLength="50"
BackgroundColor="White" TextColor="#16243C" />
<Label Text="Nazwisko (wymagane)" TextColor="#19376E" />
<Entry x:Name="entNazwisko" Placeholder="np. Kowalski" MaxLength="70"
BackgroundColor="White" TextColor="#16243C" />
<Label Text="E-mail" TextColor="#19376E" />
<Entry x:Name="entEmail" Keyboard="Email" MaxLength="120"
BackgroundColor="White" TextColor="#16243C" />
<Label Text="Telefon" TextColor="#19376E" />
<Entry x:Name="entTelefon" Keyboard="Telephone" MaxLength="30"
BackgroundColor="White" TextColor="#16243C" />
<Label Text="Stanowisko" TextColor="#19376E" />
<Entry x:Name="entStanowisko" MaxLength="80"
BackgroundColor="White" TextColor="#16243C" />
<Label Text="Firma / szkoła" TextColor="#19376E" />
<Entry x:Name="entFirma" MaxLength="100"
BackgroundColor="White" TextColor="#16243C" />
<BoxView HeightRequest="2" Color="#C8D2E6" />
<Button x:Name="btnPokaz" Text="Pokaż wizytówkę"
BackgroundColor="#176849" TextColor="White" Clicked="OnPokazClicked" />
<Grid ColumnDefinitions="*,*" ColumnSpacing="10">
<Button Grid.Column="0" Text="Wyczyść" Clicked="OnWyczyscClicked" />
<Button Grid.Column="1" Text="O programie" Clicked="OnOProgramieClicked" />
</Grid>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
Keyboard="Email"iKeyboard="Telephone"Sugerują klawiaturę na telefonie. Nie sprawdzają poprawności wpisanego e-maila ani numeru telefonu.MaxLengthOgranicza liczbę wpisanych znaków. Nie dowodzi poprawności treści.Clicked="OnWczytajZdjecieClicked"Nazwa musi być identyczna z metodą w MainPage.xaml.cs. C# rozróżnia wielkie i małe litery.
5. Jak czytać metodę kliknięcia?
Klikaj części zapisu. Ten sam schemat zobaczysz przy wyborze zdjęcia, pokazaniu wizytówki i powrocie.
Kto uruchamia tę metodę?
Przycisk wskazany przez Clicked. private ogranicza dostęp do klasy. sender oznacza źródło zdarzenia, e jego dane. async pozwala użyć await. async void stosujemy tu do handlera zdarzenia, nie jako ogólną metodę operacji.
Kliknięcie przycisku uruchamia instrukcje wewnątrz klamer.
Kiedy zatrzymamy użytkownika?
Gdy przynajmniej jedno wymagane pole jest null, puste lub zawiera tylko białe znaki. Operator || oznacza „lub”.
" " nie jest poprawnym imieniem.
Co robią komunikat i return?
Metoda pokazuje tytuł, opis i przycisk OK. await czeka na zamknięcie okna bez blokowania całego wątku interfejsu. return kończy handler, więc zapis i nawigacja się nie wykonają.
Nie otwieramy pustej wizytówki.
Co oznaczają kropka, pytajnik i Trim?
Kropka wybiera właściwość lub metodę. ?. wywoła Trim tylko dla wartości innej niż null. Trim usuwa białe znaki z początku i końca, nie spacje wewnątrz nazwiska.
" Jan Nowak " staje się "Jan Nowak".
Co zostaje na stosie?
new tworzy nową stronę. PushAsync dokłada ją nad istniejącym formularzem. await czeka na zakończenie przejścia; formularz nie zostaje zniszczony.
MainPage → WizytowkaPage
Wklej logikę pierwszej strony
To wersja etapu podstawowego: wszystkie przyciski mają metody, a wybór zdjęcia na razie wyświetla informację. W kroku 8 wymienisz tylko tę jedną metodę. Zapisz plik. Kompiluj dopiero po utworzeniu drugiej strony w kroku 6.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Storage;
namespace WizytowkaMaui;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private async void OnPokazClicked(object? sender, EventArgs e)
{
// 1. Sprawdzenie wymaganych danych przed przejściem dalej.
if (string.IsNullOrWhiteSpace(entImie.Text) ||
string.IsNullOrWhiteSpace(entNazwisko.Text))
{
await DisplayAlertAsync("Brakuje danych", "Podaj imię i nazwisko.", "OK");
return;
}
btnPokaz.IsEnabled = false;
try
{
// 2. Odczyt formularza; Trim usuwa spacje tylko z obu końców tekstu.
DaneUzytkownika.Imie = entImie.Text.Trim();
DaneUzytkownika.Nazwisko = entNazwisko.Text.Trim();
DaneUzytkownika.Email = entEmail.Text?.Trim();
DaneUzytkownika.Telefon = entTelefon.Text?.Trim();
DaneUzytkownika.Stanowisko = entStanowisko.Text?.Trim();
DaneUzytkownika.Firma = entFirma.Text?.Trim();
// 3. Nowa strona trafia na stos nawigacji.
await Navigation.PushAsync(new WizytowkaPage());
}
catch (Exception)
{
await DisplayAlertAsync("Nawigacja", "Nie udało się otworzyć wizytówki. Sprawdź NavigationPage.", "OK");
}
finally
{
btnPokaz.IsEnabled = true;
}
}
// Na razie krótki komunikat. W kroku 8 podmienisz całą tę metodę.
private async void OnWczytajZdjecieClicked(object? sender, EventArgs e)
{
await DisplayAlertAsync("Zdjęcie", "Najpierw uruchom dwa ekrany. Zdjęcie dodamy w kroku 8.", "OK");
}
private void OnWyczyscClicked(object? sender, EventArgs e)
{
entImie.Text = entNazwisko.Text = entEmail.Text = string.Empty;
entTelefon.Text = entStanowisko.Text = entFirma.Text = string.Empty;
imgZdjecie.Source = "avatar.png";
DaneUzytkownika.Wyczysc();
entImie.Focus();
}
private async void OnOProgramieClicked(object? sender, EventArgs e)
{
await DisplayAlertAsync("O programie",
"Wpisz dane, wybierz zdjęcie i pokaż wizytówkę. Dane formularza są w pamięci. PNG możesz zapisać i udostępnić.", "OK");
}
}
btnPokaz.IsEnabled = falseWyłącza przycisk na czas przejścia, żeby szybkie podwójne kliknięcie nie dodało dwóch stron.try / catch / finallytry zawiera operację, catch reaguje na wyjątek, finally przywraca przycisk także po błędzie. To nie zastępuje walidacji.string.EmptyPusty tekst. Przypisanie łańcuchowe czyści kilka pól tą samą wartością.entImie.Focus()Przenosi fokus na pierwsze pole. Nie wpisuje do niego tekstu.OnOProgramieClickedWyświetla okno dialogowe. Wersja bazowa nadal ma dokładnie dwie strony, nie trzy.
6. Zbuduj drugi ekran: gotową kartę
Otwórz dodaną wcześniej stronę i zastąp jej XAML. Karta układa zdjęcie nad nazwą, żeby długie nazwisko miało miejsce także na telefonie. Zwróć uwagę na kartaWizytowka: później przechwycimy właśnie ten Border, bez przycisków pod nim.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="WizytowkaMaui.WizytowkaPage"
Title="Moja wizytówka" BackgroundColor="#E6EBF5">
<ScrollView>
<VerticalStackLayout Padding="20" Spacing="18">
<Border x:Name="kartaWizytowka" BackgroundColor="#19376E"
Stroke="#2980B9" StrokeThickness="2"
StrokeShape="RoundRectangle 16" Padding="0">
<VerticalStackLayout Spacing="0">
<BoxView HeightRequest="8" Color="#44D7B6" />
<VerticalStackLayout Padding="20" Spacing="12">
<Border WidthRequest="96" HeightRequest="96"
Stroke="#8BDCCE" StrokeThickness="2"
StrokeShape="RoundRectangle 48" HorizontalOptions="Start">
<Image x:Name="imgWizytowka" Source="avatar.png" Aspect="AspectFill"
SemanticProperties.Description="Zdjęcie właściciela wizytówki" />
</Border>
<Label x:Name="lblImieNazwisko" FontSize="25" FontAttributes="Bold"
TextColor="White" LineBreakMode="WordWrap" />
<Label x:Name="lblStanowisko" FontSize="17" TextColor="#B8E8FF" />
<Label x:Name="lblFirma" FontSize="15" TextColor="#D0DFEF" />
<BoxView HeightRequest="1" Color="#6885B1" />
<Label x:Name="lblEmail" TextColor="White" LineBreakMode="CharacterWrap" />
<Label x:Name="lblTelefon" TextColor="White" />
</VerticalStackLayout>
</VerticalStackLayout>
</Border>
<Button x:Name="btnZapiszPng" Text="Zapisz i udostępnij PNG"
Clicked="OnZapiszPngClicked" BackgroundColor="#176849" TextColor="White" />
<Button Text="Powrót do formularza" Clicked="OnPowrotClicked" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
LineBreakMode="WordWrap"Pozwala zawijać nazwę między słowami. Dla długiego e-maila używamy CharacterWrap, aby zawijał również wewnątrz ciągu.x:Name="lblImieNazwisko"Nazwa Label, do którego C# wpisze gotowy tekst. Samo istnienie klasy DaneUzytkownika niczego nie wyświetli.
Odczytaj dane i obsłuż powrót
Zastąp plik wersją etapu podstawowego. Konstruktor tworzy kontrolki, a następnie wypełnia je danymi. Zapisz wszystkie pliki. Teraz uruchom aplikację.
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Media;
using Microsoft.Maui.Storage;
using Microsoft.Maui.ApplicationModel.DataTransfer;
namespace WizytowkaMaui;
public partial class WizytowkaPage : ContentPage
{
public WizytowkaPage()
{
InitializeComponent();
WypelnijDanymi();
}
private static string TekstLubKreska(string? tekst)
{
return string.IsNullOrWhiteSpace(tekst) ? "—" : tekst;
}
private void WypelnijDanymi()
{
lblImieNazwisko.Text = $"{DaneUzytkownika.Imie} {DaneUzytkownika.Nazwisko}";
lblStanowisko.Text = TekstLubKreska(DaneUzytkownika.Stanowisko);
lblFirma.Text = TekstLubKreska(DaneUzytkownika.Firma);
lblEmail.Text = $"E-mail: {TekstLubKreska(DaneUzytkownika.Email)}";
lblTelefon.Text = $"Telefon: {TekstLubKreska(DaneUzytkownika.Telefon)}";
string? sciezka = DaneUzytkownika.SciezkaZdjecia;
if (!string.IsNullOrWhiteSpace(sciezka) && File.Exists(sciezka))
imgWizytowka.Source = ImageSource.FromFile(sciezka);
}
// Eksport dodamy w kroku 9.
private async void OnZapiszPngClicked(object? sender, EventArgs e)
{
await DisplayAlertAsync("PNG", "Eksport dodasz po sprawdzeniu nawigacji.", "OK");
}
private async void OnPowrotClicked(object? sender, EventArgs e)
{
await Navigation.PopAsync();
}
}
private static string TekstLubKreska(string? tekst)Pomocnicza metoda przyjmuje tekst i zwraca tekst. Składniawarunek ? wartość1 : wartość2wybiera jeden z dwóch wyników. Tutaj brak treści zastępujemy kreską.$"{DaneUzytkownika.Imie} {DaneUzytkownika.Nazwisko}"Interpolacja: zawartość klamer trafia do napisu, a spacja pomiędzy nimi pozostaje zwykłym tekstem.File.Exists(sciezka)Sprawdza istnienie lokalnej kopii zdjęcia, nie poprawność obrazu. Gdy kopia zniknie z cache, zostaje avatar domyślny.await Navigation.PopAsync()Zdejmuje obecną stronę i wraca do tego samego formularza. Nie twórz tutaj nowego MainPage przez PushAsync — to rozbudowałoby stos zamiast wrócić.
7. Zatrzymaj się: sprawdź działające dwa ekrany
Nie dodawaj jeszcze kolejnych funkcji. Najpierw potwierdź ten mały rezultat.
8. Zdjęcie: wybierz plik, potem zrób własną kopię
Na Windows FullPath często jest zwykłą ścieżką pliku. Na Androidzie wynik wyboru może być adresem dostępu do treści zamiast ścieżki pliku. Dlatego otwieramy dane przez OpenReadAsync(), kopiujemy je do CacheDirectory, a dopiero tę własną ścieżkę podajemy ImageSource.
Używamy FilePicker, bo wybieramy istniejący plik. MediaPicker służy do wyboru lub wykonywania multimediów — to inne API. Anulowanie wyboru zwraca brak wyniku i po prostu kończy metodę. Na razie wybieraj nieduże, prawidłowe obrazy PNG/JPG, najlepiej do około 2 MB.
Podmień tylko metodę wyboru zdjęcia
W MainPage.xaml.cs znajdź metodę OnWczytajZdjecieClicked z krótkim komunikatem. Usuń tę metodę w całości i wstaw poniższą, wewnątrz klasy MainPage. Pozostałych metod nie zmieniaj. Potrzebujesz using Microsoft.Maui.Storage; na początku pliku — jest już w poprzednim kodzie.
private async void OnWczytajZdjecieClicked(object? sender, EventArgs e)
{
btnWczytajZdjecie.IsEnabled = false;
try
{
FileResult? plik = await FilePicker.Default.PickAsync(new PickOptions
{
PickerTitle = "Wybierz zdjęcie PNG lub JPG",
FileTypes = FilePickerFileType.Images
});
if (plik is null) return; // Anulowanie nie jest błędem.
string rozszerzenie = Path.GetExtension(plik.FileName).ToLowerInvariant();
if (rozszerzenie is not ".png" and not ".jpg" and not ".jpeg")
{
await DisplayAlertAsync("Format zdjęcia", "Wybierz plik PNG lub JPG.", "OK");
return;
}
// FullPath nie musi być ścieżką pliku na Androidzie. Czytamy strumień.
string cel = Path.Combine(FileSystem.CacheDirectory,
$"avatar_{Guid.NewGuid():N}{rozszerzenie}");
using (Stream wejscie = await plik.OpenReadAsync())
using (FileStream wyjscie = File.Create(cel))
{
await wejscie.CopyToAsync(wyjscie);
} // Strumienie są już zamknięte, więc Image może odczytać kopię.
DaneUzytkownika.SciezkaZdjecia = cel;
imgZdjecie.Source = ImageSource.FromFile(cel);
}
catch (Exception)
{
await DisplayAlertAsync("Zdjęcie", "Nie udało się odczytać zdjęcia. Spróbuj innego pliku PNG lub JPG.", "OK");
}
finally
{
btnWczytajZdjecie.IsEnabled = true;
}
}
await FilePicker.Default.PickAsync(new PickOptions { … })Otwiera systemowy wybór pliku. Obiekt PickOptions ustala tytuł i filtr obrazów. FileResult opisuje wybrany plik; null oznacza anulowanie.Path.GetExtension(...).ToLowerInvariant()Odczytuje rozszerzenie i ujednolica wielkość liter. Filtr rozszerzeń ogranicza przykład, lecz sam nie dowodzi, że plik jest prawidłowym obrazem.is not ".png" and not ".jpg" and not ".jpeg"Warunek jest prawdziwy, gdy rozszerzenie nie jest żadnym z trzech dopuszczonych. To wzorce C#, nie operatory JavaScript.Guid.NewGuid():NGeneruje praktycznie unikalny identyfikator bez myślników. Nie wkładamy danych osobowych ani oryginalnej nazwy pliku do nazwy kopii.Path.Combine(folder, nazwa)Łączy katalog z nazwą pliku poprawnie dla danego systemu.using (Stream …) / using (FileStream …)Ogranicza czas używania strumieni do bloku. CopyToAsync kopiuje bajty. Po wyjściu z klamer oba strumienie zostają zwolnione.ImageSource.FromFile(cel)Wyświetla własną lokalną kopię. Ścieżkę zapisujemy też w schowku, aby mogła z niej skorzystać druga strona.
9. PNG: przechwyć kartę i udostępnij plik
Wariant ze źródła zapisuje do MyPictures, co jest wygodne na Windows, ale nie jest przenośnym sposobem zapisu w galerii telefonu. Tutaj najpierw zapisujemy PNG we własnym AppDataDirectory, a potem otwieramy systemowy panel udostępniania. Użytkownik sam wybiera, co zrobić z plikiem.
Nie obiecujemy automatycznego zapisu do galerii. Anulowanie panelu nie usuwa utworzonej kopii. Na tym etapie wyeksportuj jedną kartę i sprawdź wynik — później warto dodać sprzątanie starych plików.
Podmień metodę eksportu
W WizytowkaPage.xaml.cs podmień całą metodę OnZapiszPngClicked z komunikatem etapu podstawowego. W pliku pozostaw wszystkie wcześniejsze using, w szczególności Microsoft.Maui, Microsoft.Maui.Media, Microsoft.Maui.Storage oraz Microsoft.Maui.ApplicationModel.DataTransfer.
private async void OnZapiszPngClicked(object? sender, EventArgs e)
{
btnZapiszPng.IsEnabled = false;
try
{
// Przechwytujemy tylko widoczną kartę, nie cały ekran z przyciskami.
IScreenshotResult? zrzut = await kartaWizytowka.CaptureAsync();
if (zrzut is null)
{
await DisplayAlertAsync("PNG", "Nie udało się przechwycić karty. Upewnij się, że jest widoczna.", "OK");
return;
}
string nazwa = $"wizytowka_{Guid.NewGuid():N}.png";
string sciezka = Path.Combine(FileSystem.AppDataDirectory, nazwa);
using (FileStream plik = File.Create(sciezka))
{
await zrzut.CopyToAsync(plik, ScreenshotFormat.Png);
} // Zamknij plik przed otwarciem systemowego panelu udostępniania.
await DisplayAlertAsync("Zapisano PNG", $"Kopia jest w katalogu aplikacji:\n{sciezka}", "Dalej");
await Share.Default.RequestAsync(new ShareFileRequest
{
Title = "Wizytówka PNG",
File = new ShareFile(sciezka, "image/png")
});
}
catch (Exception)
{
await DisplayAlertAsync("Eksport PNG",
"Nie udało się zakończyć eksportu lub udostępniania. Jeśli komunikat o zapisie był widoczny, kopia PNG pozostała w katalogu aplikacji.", "OK");
}
finally
{
btnZapiszPng.IsEnabled = true;
}
}
await kartaWizytowka.CaptureAsync()Metoda rozszerzająca dla widoku MAUI, z przestrzeni Microsoft.Maui. Przechwytuje kartę, nie cały ekran. Potrzebuje widocznego, wyrenderowanego widoku; wynik może być null.File.Create(sciezka)Tworzy plik, a istniejący o tej samej nazwie skraca do zera. To istotna różnica względem File.OpenWrite, które nie musi usunąć starych końcowych bajtów. Tutaj używamy dodatkowo unikalnej nazwy.await zrzut.CopyToAsync(plik, ScreenshotFormat.Png)Zapisuje obraz jako PNG. Rozszerzenie .png samo nie zmieni formatu danych — określamy również ScreenshotFormat.Png.new ShareFileRequest { … }Opis operacji udostępnienia. ShareFile wskazuje gotowy plik i jego typ image/png. Panel systemowy nie gwarantuje wysłania lub zapisania do galerii.- Klamry
usingprzedSharePlik jest już zamknięty przed przekazaniem go innemu mechanizmowi. Inaczej system może nie móc go odczytać.
10. Odbiór projektu: co ma działać?
W paczce z początku lekcji masz komplet końcowych plików do szablonu, jeśli chcesz porównać nawiasy i nazwy. Nie podmieniaj nimi całego projektu ani katalogu Platforms.
Gdy coś nie działa, sprawdź konkretną rzecz
Błąd przy PushAsync
return new Window(new MainPage());Brak stosu NavigationPage dla tego sposobu przechodzenia.
return new Window(new NavigationPage(new MainPage()));W tej lekcji zastępujemy start AppShell przez NavigationPage. Nie opakowujemy Shell.
Nazwa kontrolki nie istnieje
<Entry x:Name="entImię" />C# szuka entImie, a w XAML jest inna nazwa.
<Entry x:Name="entImie" />Sprawdź x:Name, x:Class i namespace. Dla nazw plików i kontrolek używamy tu zapisu bez polskich liter.
Zdjęcie działa tylko na komputerze
ImageSource.FromFile(plik.FullPath);Na telefonie FullPath może być adresem treści, nie ścieżką do odczytu przez FromFile.
using Stream wejscie = await plik.OpenReadAsync();Odczytaj wybrany plik jako strumień i skopiuj go do własnego katalogu tak jak w kroku 8.
Powrót tworzy kolejny formularz
await Navigation.PushAsync(new MainPage());Na stos trafia nowy ekran. To nie jest powrót do poprzedniego.
await Navigation.PopAsync();Zdejmij wizytówkę i odsłoń istniejący formularz.
Nie ma metody DisplayAlertAsync
// Projekt .NET MAUI 8/9 + kod dla MAUI 10Wersja API nie zgadza się z instrukcją.
// W tej lekcji: .NET 10 + MAUI 10W starszych materiałach spotkasz DisplayAlert. Nie mieszaj wersji bez świadomego dostosowania projektu.
Dla chętnych: przycisk „Zadzwoń”
Na drugiej stronie, pod kartą, dodaj w XAML Button z Text="Zadzwoń" i Clicked="OnZadzwonClicked". W WizytowkaPage.xaml.cs dodaj poniższą metodę wewnątrz klasy. To otwarcie systemowego dialera z numerem, a nie automatyczne wykonanie połączenia. Sprawdź platformę: na komputerze funkcja może nie być obsługiwana. Zadanie z walidacji telefonu w biurku pomoże oczyścić i sprawdzić numer. Na Androidzie 11+ dodaj też deklarację queries opisaną w następnym fragmencie.
private async void OnZadzwonClicked(object? sender, EventArgs e)
{
string? numer = DaneUzytkownika.Telefon;
if (string.IsNullOrWhiteSpace(numer))
{
await DisplayAlertAsync("Telefon", "Najpierw podaj numer.", "OK");
return;
}
if (!Microsoft.Maui.ApplicationModel.Communication.PhoneDialer.Default.IsSupported)
{
await DisplayAlertAsync("Telefon", "Na tym urządzeniu dialer jest niedostępny.", "OK");
return;
}
try
{
Microsoft.Maui.ApplicationModel.Communication.PhoneDialer.Default.Open(numer);
}
catch (Exception)
{
await DisplayAlertAsync("Telefon", "Nie udało się otworzyć dialera.", "OK");
}
}
PhoneDialer.Default.IsSupportedSprawdza obsługę tej funkcji na platformie. Nie sprawdza poprawności numeru ani połączenia z siecią.Open(numer)Otwiera systemowy dialer z wstępnie wpisanym numerem. To użytkownik decyduje o wykonaniu połączenia.
Android 11+: pozwól sprawdzić obsługę dialera
Dodaj queries jako dziecko manifest, obok application, a nie w jego wnętrzu. Jeśli queries już istnieje, dodaj tylko intent do niego. To deklaracja wyszukiwania aplikacji dialera, nie zgoda na automatyczne dzwonienie.
<queries>
<intent>
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent>
</queries>
actionidataOpisują czynność wybierania numeru oraz schemat tel. Nie dodawaj uprawnienia CALL_PHONE do tego przykładu — nie wykonuje on połączenia samodzielnie.
Ćwiczenie z pomocą · samodzielna praca
Twoja mała zmiana: dopisz klasę do wizytówki
Dodaj pole „Klasa”, np. 2TP, i wyświetl je na karcie pod szkołą. Pole jest opcjonalne: gdy puste, pokaż kreskę. Nie kopiuj tej zmiany wyłącznie do XAML — dane muszą przejść cały łańcuch.
Jan Kowalski Twórca aplikacji Szkolne studio Klasa: 2TP
? Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź
Dodaj Entry z własną nazwą i etykietą na MainPage.
Krok 2 — pokaż podpowiedź
Dodaj właściwość string? Klasa do DaneUzytkownika. Uwzględnij ją też w Wyczysc().
Krok 3 — pokaż podpowiedź
W OnPokazClicked odczytaj pole przez .Text?.Trim().
Krok 4 — pokaż podpowiedź
Dodaj Label na WizytowkaPage i wypełnij go w WypelnijDanymi().
Krok 5 — pokaż podpowiedź
Sprawdź klasę wpisaną, pustą i zmienioną po powrocie.
Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.
Podsumowanie
Potrafisz prześledzić drogę jednej wartości?
Entry.Text→ walidacja →DaneUzytkownika→ konstruktor drugiej strony →Label.Text.PushAsyncotwiera nową stronę na stosie,PopAsyncwraca do istniejącej.staticwspółdzieli dane w pamięci procesu. Nie zapisuje ich trwale.- Wybór zdjęcia to oddzielna operacja systemowa. Przenośny odczyt zaczyna się od
OpenReadAsync(). CaptureAsync()przechwytuje widok; plik trzeba jeszcze zapisać i ewentualnie udostępnić.- Ta wersja sprawdza wymagane imię i nazwisko. Nie zawiera pełnej walidacji kontaktu ani kont użytkowników.
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ę →Źródła i dalsza nauka
- Microsoft: pierwszy projekt MAUI (.NET 10 / VS 2026)
- Microsoft: NavigationPage i stos stron
- Microsoft: wybór pliku i ograniczenia FullPath
- Microsoft: przechwytywanie konkretnego widoku
- Microsoft: systemowe udostępnianie pliku
- Microsoft: okna dialogowe
- CKE: informator INF.04, jednostka INF.04.6 (s. 31–33)
- Microsoft: dialer i deklaracja Android queries
- Microsoft: zmiany API w .NET MAUI 10