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.

Ta interakcja wymaga włączonego JavaScriptu.

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.

Pobierz XAML, C# i README · ZIP ↓

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.

PlikOdpowiedzialność
App.xaml.csTworzy okno i stos NavigationPage.
MainPage.xamlWygląd formularza: pola i przyciski.
MainPage.xaml.csReakcje na kliknięcia, walidacja, odczyt pól i wybór zdjęcia.
DaneUzytkownika.csWspólne dane w pamięci procesu.
WizytowkaPage.xamlWygląd karty na drugiej stronie.
WizytowkaPage.xaml.csWypeł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.

DaneUzytkownika.cscsharp
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;
    }
}
  1. public static string? Imie { get; set; } Publiczna właściwość tekstowa, wspólna dla stron. get odczytuje, set zapisuje. ? dopuszcza null, czyli brak wartości — to inna wartość niż pusty tekst "".
  2. DaneUzytkownika.Imie = "Jan"; Zapisujesz tekst. Później druga strona odczytuje tę samą właściwość.
  3. 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.

App.xaml.cscsharp
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;
    }
}
  1. new NavigationPage(new MainPage()) Tworzy stos i kładzie formularz jako pierwszą stronę. Bez tego zwykła ContentPage nie zapewni tej nawigacji PushAsync.
  2. CreateWindow(IActivationState? activationState) Metoda wywoływana przy tworzeniu okna aplikacji. override oznacza własną wersję metody z klasy Application. Parametr może być null; w tym projekcie go nie używamy.
  3. return new Window(...) Zwraca okno z wybraną zawartością. #if WINDOWS ogranicza ustawienie rozmiaru do kompilacji Windows. Telefon sam zarządza rozmiarem ekranu.
  4. Color.FromArgb("#19376E") Tworzy kolor z zapisu szesnastkowego. Colors.White to 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 formaCo 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#.

MainPage.xamlxaml
<?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>
  1. Keyboard="Email" i Keyboard="Telephone" Sugerują klawiaturę na telefonie. Nie sprawdzają poprawności wpisanego e-maila ani numeru telefonu.
  2. MaxLength Ogranicza liczbę wpisanych znaków. Nie dowodzi poprawności treści.
  3. 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.

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.

MainPage.xaml.cscsharp
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");
    }
}
  1. btnPokaz.IsEnabled = false Wyłącza przycisk na czas przejścia, żeby szybkie podwójne kliknięcie nie dodało dwóch stron.
  2. try / catch / finally try zawiera operację, catch reaguje na wyjątek, finally przywraca przycisk także po błędzie. To nie zastępuje walidacji.
  3. string.Empty Pusty tekst. Przypisanie łańcuchowe czyści kilka pól tą samą wartością.
  4. entImie.Focus() Przenosi fokus na pierwsze pole. Nie wpisuje do niego tekstu.
  5. OnOProgramieClicked Wyś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.

WizytowkaPage.xamlxaml
<?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>
  1. 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.
  2. 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ę.

WizytowkaPage.xaml.cscsharp
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();
    }
}
  1. private static string TekstLubKreska(string? tekst) Pomocnicza metoda przyjmuje tekst i zwraca tekst. Składnia warunek ? wartość1 : wartość2 wybiera jeden z dwóch wyników. Tutaj brak treści zastępujemy kreską.
  2. $"{DaneUzytkownika.Imie} {DaneUzytkownika.Nazwisko}" Interpolacja: zawartość klamer trafia do napisu, a spacja pomiędzy nimi pozostaje zwykłym tekstem.
  3. File.Exists(sciezka) Sprawdza istnienie lokalnej kopii zdjęcia, nie poprawność obrazu. Gdy kopia zniknie z cache, zostaje avatar domyślny.
  4. 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.

MainPage.xaml.cscsharp
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;
        }
    }
  1. 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.
  2. Path.GetExtension(...).ToLowerInvariant() Odczytuje rozszerzenie i ujednolica wielkość liter. Filtr rozszerzeń ogranicza przykład, lecz sam nie dowodzi, że plik jest prawidłowym obrazem.
  3. 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.
  4. Guid.NewGuid():N Generuje praktycznie unikalny identyfikator bez myślników. Nie wkładamy danych osobowych ani oryginalnej nazwy pliku do nazwy kopii.
  5. Path.Combine(folder, nazwa) Łączy katalog z nazwą pliku poprawnie dla danego systemu.
  6. using (Stream …) / using (FileStream …) Ogranicza czas używania strumieni do bloku. CopyToAsync kopiuje bajty. Po wyjściu z klamer oba strumienie zostają zwolnione.
  7. 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.

WizytowkaPage.xaml.cscsharp
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;
        }
    }
  1. 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.
  2. 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.
  3. await zrzut.CopyToAsync(plik, ScreenshotFormat.Png) Zapisuje obraz jako PNG. Rozszerzenie .png samo nie zmieni formatu danych — określamy również ScreenshotFormat.Png.
  4. 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.
  5. Klamry using przed Share Plik 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
Tak nie
return new Window(new MainPage());

Brak stosu NavigationPage dla tego sposobu przechodzenia.

Tak
return new Window(new NavigationPage(new MainPage()));

W tej lekcji zastępujemy start AppShell przez NavigationPage. Nie opakowujemy Shell.

Nazwa kontrolki nie istnieje
Tak nie
<Entry x:Name="entImię" />

C# szuka entImie, a w XAML jest inna nazwa.

Tak
<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
Tak nie
ImageSource.FromFile(plik.FullPath);

Na telefonie FullPath może być adresem treści, nie ścieżką do odczytu przez FromFile.

Tak
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
Tak nie
await Navigation.PushAsync(new MainPage());

Na stos trafia nowy ekran. To nie jest powrót do poprzedniego.

Tak
await Navigation.PopAsync();

Zdejmij wizytówkę i odsłoń istniejący formularz.

Nie ma metody DisplayAlertAsync
Tak nie
// Projekt .NET MAUI 8/9 + kod dla MAUI 10

Wersja API nie zgadza się z instrukcją.

Tak
// W tej lekcji: .NET 10 + MAUI 10

W 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.

WizytowkaPage.xaml.cs — metoda dodatkowacsharp
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");
    }
}
  1. PhoneDialer.Default.IsSupported Sprawdza obsługę tej funkcji na platformie. Nie sprawdza poprawności numeru ani połączenia z siecią.
  2. 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.

Platforms/Android/AndroidManifest.xml — fragment wewnątrz manifestxml
<queries>
    <intent>
        <action android:name="android.intent.action.DIAL" />
        <data android:scheme="tel" />
    </intent>
</queries>
  1. action i data Opisują 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.

Oczekiwany rezultat
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?

  1. Entry.Text → walidacja → DaneUzytkownika → konstruktor drugiej strony → Label.Text.
  2. PushAsync otwiera nową stronę na stosie, PopAsync wraca do istniejącej.
  3. static współdzieli dane w pamięci procesu. Nie zapisuje ich trwale.
  4. Wybór zdjęcia to oddzielna operacja systemowa. Przenośny odczyt zaczyna się od OpenReadAsync().
  5. CaptureAsync() przechwytuje widok; plik trzeba jeszcze zapisać i ewentualnie udostępnić.
  6. 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