Aplikacje desktopowe · Windows Forms

Aplikacja dwuekranowa: formularz i wizytówka

Zbudujesz aplikację Windows Forms, w której pierwszy ekran zbiera dane, a drugi zamienia je w wizytówkę i zapisuje jako plik PNG. To dobry krok po Notatniku 4P: program nie robi już jednej rzeczy w jednym oknie, tylko prowadzi użytkownika przez cały proces.

  • około 90 minut
  • Windows Forms · dwa formularze · PictureBox
  • INF.04.5

Po tej lekcji: zbudować dwa współpracujące formularze, przekazać dane z jednego do drugiego, wczytać zdjęcie bez blokowania pliku i zapisać kontrolkę jako obraz PNG.

Zacznijmy od znanego układu

Najpierw dane, potem podgląd, na końcu zapis

Taki sam układ spotkasz w formularzu zamówienia, kreatorze profilu albo generatorze certyfikatu: użytkownik uzupełnia dane, widzi podgląd i dopiero wtedy zapisuje lub zatwierdza. Rozdzielenie etapów upraszcza ekran i zmniejsza liczbę pomyłek.

W projekcie ćwiczysz dwa formularze, walidację, przekazywanie danych, pracę ze zdjęciem i zapis wyniku.

Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji

1 · Zobacz, do czego dążysz

Dwa ekrany, jedna wizytówka

Kliknij „Pokaż wizytówkę” przy pustym imieniu: pojawi się ostrzeżenie. Wczytaj „zdjęcie”, wpisz dane i otwórz wizytówkę. Kliknij „Zapisz PNG” i zobacz zawartość pliku. Potem zaznacz opcję „etykiety obok panelu” i zapisz jeszcze raz.

Ta interakcja wymaga włączonego JavaScriptu.

DrawToBitmap() zapisuje kontrolkę razem z jej dziećmi. Wszystko, co ma trafić do PNG, musi leżeć wewnątrz panelu wizytówki.

Efekt końcowy

Pierwszy ekran: formularz danych

Pierwszy formularz aplikacji: pola danych i podgląd zdjęcia
Pierwszy ekran: dwie kolumny, po lewej pola, po prawej zdjęcie.

Efekt końcowy

Drugi ekran: gotowa wizytówka

Drugi ekran: gotowa wizytówka z zapisem do PNG
Drugi ekran: wizytówka w panelu i przyciski zapisu oraz powrotu.

2 · Przygotowanie

Utwórz projekt i zaplanuj przepływ

Główny przepływ ma dwie formy: Form1 zbiera dane i pozwala wskazać zdjęcie, a FormWizytowka pokazuje rezultat i zapisuje panel do PNG. Pomiędzy nimi działa klasa DaneUzytkownika.

3 · Krok 1

Kontrolki Form1

Kontrolka`(Name)`Ustawienie lub tekst
TextBoxtxtImie, txtNazwiskoPola obowiązkowe.
TextBoxtxtEmail, txtTelefon, txtStanowisko, txtFirmaDane dodatkowe.
PictureBoxpicZdjecieSizeMode = Zoom, około 150 × 150.
ButtonbtnWczytajZdjecieWczytaj zdjęcie.
ButtonbtnPokazPokaż wizytówkę.
ButtonbtnWyczyscWyczyść.
ButtonbtnOProgramieO programie.
Cienkie separatory zrób z `Panel` o wysokości 2 px. Zachowuje się jak zwykły element, więc łatwo zmienisz jego kolor i szerokość. **Punkt kontrolny:** dwie czytelne kolumny, a najważniejszy przycisk prowadzi do podglądu.

4 · Krok 2

DaneUzytkownika.cs

Klasa statyczna: oba formularze odwołują się do tego samego zestawu danych. To celowe uproszczenie, które pozwala najpierw zrozumieć przejście między ekranami. W zadaniu 3 zastąpisz je przekazywaniem obiektu przez konstruktor.

DaneUzytkownika.cscsharp
using System.Drawing;

namespace Wizytowka;

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 Image? Zdjecie { get; set; }
}
  1. public static class Nie tworzysz obiektu. Właściwości są wspólne dla całej aplikacji.
  2. string? i Image? Znak zapytania oznacza, że wartości może nie być (np. nie wczytano zdjęcia).

5 · Krok 3

FormWizytowka: wszystko na panelu

Kontrolka`(Name)`Gdzie leży
PanelpanelWizytowkaBezpośrednio na formularzu.
PictureBoxpicZdjecieWewnątrz panelu.
LabellblImieNazwisko, lblStanowisko, lblFirma, lblEmail, lblTelefonWewnątrz panelu.
ButtonbtnZapisz, btnPowrotPod panelem, na formularzu.
Dlaczego kontrolki muszą leżeć na panelu? `DrawToBitmap()` rysuje wskazaną kontrolkę razem z jej dziećmi. Etykiety obok panelu nie trafią do pliku PNG.

6 · Krok 4

Metody do podpięcia

KontrolkaZdarzenieMetoda
btnWczytajZdjecieClickbtnWczytajZdjecie_Click
btnPokazClickbtnPokaz_Click
btnWyczyscClickbtnWyczysc_Click
btnOProgramieClickbtnOProgramie_Click
btnZapiszClickbtnZapisz_Click
btnPowrotClickbtnPowrot_Click
Najprościej kliknąć przycisk dwa razy w projektancie: Visual Studio utworzy metodę i powiąże ją ze zdarzeniem. Kod wpisuj w pliku formularza, nigdy ręcznie w `*.Designer.cs`.

7 · Pierwszy ekran

Form1.cs

Nie wklejaj całości od razu. Najpierw uruchom pusty formularz, potem walidację imienia i nazwiska, następnie przejście do drugiego okna, a na końcu zdjęcie.

Form1.cscsharp
namespace Wizytowka;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        picZdjecie.SizeMode = PictureBoxSizeMode.Zoom;
    }

    private void btnWczytajZdjecie_Click(object sender, EventArgs e)
    {
        using OpenFileDialog dialog = new()
        {
            Filter = "Pliki graficzne|*.jpg;*.jpeg;*.png;*.bmp|Wszystkie pliki|*.*",
            Title = "Wybierz zdjęcie profilowe"
        };

        if (dialog.ShowDialog() != DialogResult.OK)
            return;

        // Niezależna kopia obrazu: plik źródłowy nie pozostaje zablokowany
        using Image obrazZPliku = Image.FromFile(dialog.FileName);
        Image noweZdjecie = new Bitmap(obrazZPliku);

        picZdjecie.Image?.Dispose();
        DaneUzytkownika.Zdjecie?.Dispose();

        DaneUzytkownika.Zdjecie = noweZdjecie;
        picZdjecie.Image = new Bitmap(noweZdjecie);
    }

    private void btnPokaz_Click(object sender, EventArgs e)
    {
        if (string.IsNullOrWhiteSpace(txtImie.Text) ||
            string.IsNullOrWhiteSpace(txtNazwisko.Text))
        {
            MessageBox.Show("Podaj imię i nazwisko.", "Brakuje danych",
                MessageBoxButtons.OK, MessageBoxIcon.Warning);
            return;
        }

        DaneUzytkownika.Imie = txtImie.Text.Trim();
        DaneUzytkownika.Nazwisko = txtNazwisko.Text.Trim();
        DaneUzytkownika.Email = txtEmail.Text.Trim();
        DaneUzytkownika.Telefon = txtTelefon.Text.Trim();
        DaneUzytkownika.Stanowisko = txtStanowisko.Text.Trim();
        DaneUzytkownika.Firma = txtFirma.Text.Trim();

        using FormWizytowka wizytowka = new();
        Hide();
        try
        {
            wizytowka.ShowDialog();
        }
        finally
        {
            Show();
            Activate();
        }
    }

    private void btnWyczysc_Click(object sender, EventArgs e)
    {
        txtImie.Clear();
        txtNazwisko.Clear();
        txtEmail.Clear();
        txtTelefon.Clear();
        txtStanowisko.Clear();
        txtFirma.Clear();

        picZdjecie.Image?.Dispose();
        picZdjecie.Image = null;
        DaneUzytkownika.Zdjecie?.Dispose();
        DaneUzytkownika.Zdjecie = null;
        txtImie.Focus();
    }

    private void btnOProgramie_Click(object sender, EventArgs e)
    {
        MessageBox.Show("Wizytówka cyfrowa\nProjekt edukacyjny INF.04", "O programie",
            MessageBoxButtons.OK, MessageBoxIcon.Information);
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        picZdjecie.Image?.Dispose();
        DaneUzytkownika.Zdjecie?.Dispose();
        DaneUzytkownika.Zdjecie = null;
        base.OnFormClosed(e);
    }
}
  1. using OpenFileDialog dialog = new() { ... } Okno wyboru pliku w nowoczesnym zapisie. using zwolni je automatycznie.
  2. Image.FromFile + new Bitmap(obraz) Tworzymy niezależną kopię, a plik źródłowy zostaje zwolniony. Dzięki temu można go później przenieść lub usunąć.
  3. picZdjecie.Image?.Dispose() Przed podmianą zwalniamy poprzedni obraz. ?. pomija wywołanie, gdy obrazu nie ma.
  4. string.IsNullOrWhiteSpace Odrzuca puste pole i pole z samymi spacjami.
  5. Hide() ... ShowDialog() ... finally { Show(); } Chowamy pierwszy formularz, otwieramy drugi i czekamy. Blok finally gwarantuje, że Form1 wróci, nawet gdy coś się nie powiedzie.
  6. OnFormClosed Przy zamykaniu zwalniamy obrazy, aby nie zostawiać zasobów.

8 · Drugi ekran

FormWizytowka.cs

Drugi ekran odczytuje dane ze schowka, pokazuje je w panelu i zapisuje dokładnie ten panel.

FormWizytowka.cscsharp
using System.Drawing.Imaging;

namespace Wizytowka;

public partial class FormWizytowka : Form
{
    public FormWizytowka()
    {
        InitializeComponent();
        WypelnijDanymi();
    }

    private void WypelnijDanymi()
    {
        lblImieNazwisko.Text = $"{DaneUzytkownika.Imie} {DaneUzytkownika.Nazwisko}".ToUpperInvariant();
        lblStanowisko.Text = PokazLubMyslnik(DaneUzytkownika.Stanowisko);
        lblFirma.Text = PokazLubMyslnik(DaneUzytkownika.Firma);
        lblEmail.Text = "E-mail: " + PokazLubMyslnik(DaneUzytkownika.Email);
        lblTelefon.Text = "Telefon: " + PokazLubMyslnik(DaneUzytkownika.Telefon);

        if (DaneUzytkownika.Zdjecie is not null)
        {
            picZdjecie.Image = new Bitmap(DaneUzytkownika.Zdjecie);
            picZdjecie.SizeMode = PictureBoxSizeMode.Zoom;
        }
    }

    private static string PokazLubMyslnik(string? tekst)
    {
        return string.IsNullOrWhiteSpace(tekst) ? "---" : tekst;
    }

    private void btnZapisz_Click(object sender, EventArgs e)
    {
        using SaveFileDialog dialog = new()
        {
            Filter = "Obraz PNG|*.png",
            DefaultExt = "png",
            AddExtension = true,
            FileName = "wizytowka.png"
        };

        if (dialog.ShowDialog() != DialogResult.OK)
            return;

        using Bitmap bitmapa = new(panelWizytowka.Width, panelWizytowka.Height);
        panelWizytowka.DrawToBitmap(bitmapa, new Rectangle(0, 0, panelWizytowka.Width, panelWizytowka.Height));
        bitmapa.Save(dialog.FileName, ImageFormat.Png);

        MessageBox.Show("Wizytówka została zapisana.", "Gotowe",
            MessageBoxButtons.OK, MessageBoxIcon.Information);
    }

    private void btnPowrot_Click(object sender, EventArgs e)
    {
        Close();
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        picZdjecie.Image?.Dispose();
        base.OnFormClosed(e);
    }
}
  1. $"{Imie} {Nazwisko}".ToUpperInvariant() Napis z interpolacją, zamieniony na wielkie litery niezależnie od ustawień regionalnych.
  2. PokazLubMyslnik(...) Metoda pomocnicza: puste pole zamienia na ---. Dzięki niej nie ma powtarzanych warunków.
  3. DaneUzytkownika.Zdjecie is not null Zdjęcie jest opcjonalne, więc najpierw sprawdzamy, czy istnieje.
  4. panelWizytowka.DrawToBitmap(...) Rysuje panel razem z kontrolkami wewnątrz do obiektu Bitmap.
  5. bitmapa.Save(sciezka, ImageFormat.Png) Zapis bitmapy do pliku PNG pod ścieżką wybraną w SaveFileDialog.
  6. Close() w btnPowrot_Click Zamyka drugi formularz, więc ShowDialog() w Form1 się kończy.

Do zadań

ColorDialog, kontrast tekstu i dane przekazane konstruktorem

Zadanie 2 używa ColorDialog, a zadanie 3 usuwa klasę statyczną. Oba rozwiązania poniżej.

Form1.cs i FormWizytowka.cscsharp
// Zadanie 2: kolor tła wizytówki
private void btnKolor_Click(object sender, EventArgs e)
{
    using ColorDialog dialog = new();
    if (dialog.ShowDialog() != DialogResult.OK)
        return;                                        // Anuluj nic nie zmienia

    Color kolor = dialog.Color;
    panelWizytowka.BackColor = kolor;

    double jasnosc = 0.299 * kolor.R + 0.587 * kolor.G + 0.114 * kolor.B;
    Color tekst = jasnosc > 140 ? Color.Black : Color.White;
    foreach (Label etykieta in panelWizytowka.Controls.OfType<Label>())   // using System.Linq;
        etykieta.ForeColor = tekst;
}

// Zadanie 3: zwykła klasa zamiast klasy statycznej
public class Osoba
{
    public string Imie { get; set; } = "";
    public string Nazwisko { get; set; } = "";
    public string Email { get; set; } = "";
}

public partial class FormWizytowka : Form
{
    private readonly Osoba osoba;

    public FormWizytowka(Osoba osoba)                  // dane przychodzą przez konstruktor
    {
        InitializeComponent();
        this.osoba = osoba;
        lblImieNazwisko.Text = osoba.Imie + " " + osoba.Nazwisko;
        lblEmail.Text = string.IsNullOrWhiteSpace(osoba.Email) ? "---" : osoba.Email;
    }
}

// w Form1:
Osoba osoba = new() { Imie = txtImie.Text.Trim(), Nazwisko = txtNazwisko.Text.Trim() };
using FormWizytowka wizytowka = new(osoba);
wizytowka.ShowDialog();
  1. Jasność koloru Wzór ważony zielenią, bo oko widzi ją jako najjaśniejszą. Przy jasnym tle tekst jest czarny, przy ciemnym biały.
  2. Konstruktor z parametrem Formularz dostaje dokładnie jeden obiekt Osoba, więc dwa formularze z dwiema osobami są niezależne. Projektant WinForms czasem prosi o dodatkowy konstruktor bez parametrów, ale program działa i bez niego.

9 · Sprawdź aplikację

Scenariusze do przejścia

TestOczekiwany rezultat
Kliknij „Pokaż wizytówkę” bez danychOstrzeżenie, aplikacja pozostaje otwarta.
Wpisz tylko imię i nazwiskoDrugi ekran działa, brakujące dane pokazują ---.
Wybierz zdjęcie JPG lub PNGPodgląd zachowuje proporcje i nie rozciąga obrazu.
Po wczytaniu zmień nazwę pliku źródłowegoOperacja jest możliwa, bo aplikacja nie blokuje pliku.
Otwórz wizytówkę i kliknij „Powrót”Wraca pierwszy formularz z wpisanymi danymi.
Zapisz wizytówkę jako PNGPlik zawiera cały panel: tło, zdjęcie i tekst.
Kliknij „Wyczyść”Pola i zdjęcie znikają, kursor wraca do pola imienia.
Samo uruchomienie okna nie wystarcza. Przejdź cały scenariusz.

10 · Czego unikać

Cztery typowe kłopoty

PNG bez etykiet
Tak nie
Etykiety leżą na formularzu obok panelWizytowka

W pliku jest tylko tło i zdjęcie.

Tak
Przeciągnij etykiety **na** panelWizytowka (panel staje się ich rodzicem)

DrawToBitmap() rysuje kontrolkę razem z jej dziećmi.

Zdjęcie rozciągnięte
Tak nie
picZdjecie.SizeMode = PictureBoxSizeMode.StretchImage;

Obraz traci proporcje.

Tak
picZdjecie.SizeMode = PictureBoxSizeMode.Zoom;

Zoom skaluje obraz proporcjonalnie.

Nie można podmienić pliku zdjęcia
Tak nie
picZdjecie.Image = Image.FromFile(sciezka);

System blokuje plik, dopóki obraz jest używany.

Tak
using Image obraz = Image.FromFile(sciezka);
picZdjecie.Image = new Bitmap(obraz);

Kopia przez new Bitmap uwalnia plik źródłowy.

Pierwszy formularz nie wraca
Tak nie
Hide();
wizytowka.ShowDialog();

Po zamknięciu wizytówki Form1 pozostaje ukryty (zwłaszcza po błędzie).

Tak
Hide();
try { wizytowka.ShowDialog(); }
finally { Show(); Activate(); }

finally wykona się zawsze, także przy wyjątku.

11 · Sprawdź się

Dwa formularze w praktyce

Cztery krótkie sytuacje.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Dwa formularze współpracują przez Hide(), ShowDialog() i Show() w finally.
  2. Dane między ekranami można przekazać schowkiem statycznym (prosto) albo obiektem przez konstruktor (lepiej).
  3. Zdjęcie wczytuj przez kopię new Bitmap(obraz) i zwalniaj obrazy przez Dispose().
  4. DrawToBitmap() zapisuje kontrolkę z jej dziećmi, więc cała wizytówka musi leżeć na panelu.
  5. Walidację (IsNullOrWhiteSpace) wykonuj, zanim przejdziesz do drugiego ekranu.

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