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.
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
Efekt końcowy
Drugi ekran: gotowa wizytówka
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 |
|---|---|---|
TextBox | txtImie, txtNazwisko | Pola obowiązkowe. |
TextBox | txtEmail, txtTelefon, txtStanowisko, txtFirma | Dane dodatkowe. |
PictureBox | picZdjecie | SizeMode = Zoom, około 150 × 150. |
Button | btnWczytajZdjecie | Wczytaj zdjęcie. |
Button | btnPokaz | Pokaż wizytówkę. |
Button | btnWyczysc | Wyczyść. |
Button | btnOProgramie | O programie. |
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.
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; }
}
public static classNie tworzysz obiektu. Właściwości są wspólne dla całej aplikacji.string?iImage?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 |
|---|---|---|
Panel | panelWizytowka | Bezpośrednio na formularzu. |
PictureBox | picZdjecie | Wewnątrz panelu. |
Label | lblImieNazwisko, lblStanowisko, lblFirma, lblEmail, lblTelefon | Wewnątrz panelu. |
Button | btnZapisz, btnPowrot | Pod panelem, na formularzu. |
6 · Krok 4
Metody do podpięcia
| Kontrolka | Zdarzenie | Metoda |
|---|---|---|
btnWczytajZdjecie | Click | btnWczytajZdjecie_Click |
btnPokaz | Click | btnPokaz_Click |
btnWyczysc | Click | btnWyczysc_Click |
btnOProgramie | Click | btnOProgramie_Click |
btnZapisz | Click | btnZapisz_Click |
btnPowrot | Click | btnPowrot_Click |
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.
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);
}
}
using OpenFileDialog dialog = new() { ... }Okno wyboru pliku w nowoczesnym zapisie.usingzwolni je automatycznie.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ąć.picZdjecie.Image?.Dispose()Przed podmianą zwalniamy poprzedni obraz.?.pomija wywołanie, gdy obrazu nie ma.string.IsNullOrWhiteSpaceOdrzuca puste pole i pole z samymi spacjami.Hide()...ShowDialog()...finally { Show(); }Chowamy pierwszy formularz, otwieramy drugi i czekamy. Blokfinallygwarantuje, że Form1 wróci, nawet gdy coś się nie powiedzie.OnFormClosedPrzy 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.
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);
}
}
$"{Imie} {Nazwisko}".ToUpperInvariant()Napis z interpolacją, zamieniony na wielkie litery niezależnie od ustawień regionalnych.PokazLubMyslnik(...)Metoda pomocnicza: puste pole zamienia na---. Dzięki niej nie ma powtarzanych warunków.DaneUzytkownika.Zdjecie is not nullZdjęcie jest opcjonalne, więc najpierw sprawdzamy, czy istnieje.panelWizytowka.DrawToBitmap(...)Rysuje panel razem z kontrolkami wewnątrz do obiektuBitmap.bitmapa.Save(sciezka, ImageFormat.Png)Zapis bitmapy do pliku PNG pod ścieżką wybraną wSaveFileDialog.Close()wbtnPowrot_ClickZamyka drugi formularz, więcShowDialog()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.
// 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();
- Jasność koloru Wzór ważony zielenią, bo oko widzi ją jako najjaśniejszą. Przy jasnym tle tekst jest czarny, przy ciemnym biały.
- 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
| Test | Oczekiwany rezultat |
|---|---|
| Kliknij „Pokaż wizytówkę” bez danych | Ostrzeżenie, aplikacja pozostaje otwarta. |
| Wpisz tylko imię i nazwisko | Drugi ekran działa, brakujące dane pokazują ---. |
| Wybierz zdjęcie JPG lub PNG | Podgląd zachowuje proporcje i nie rozciąga obrazu. |
| Po wczytaniu zmień nazwę pliku źródłowego | Operacja 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 PNG | Plik zawiera cały panel: tło, zdjęcie i tekst. |
| Kliknij „Wyczyść” | Pola i zdjęcie znikają, kursor wraca do pola imienia. |
10 · Czego unikać
Cztery typowe kłopoty
PNG bez etykiet
Etykiety leżą na formularzu obok panelWizytowkaW pliku jest tylko tło i zdjęcie.
Przeciągnij etykiety **na** panelWizytowka (panel staje się ich rodzicem)DrawToBitmap() rysuje kontrolkę razem z jej dziećmi.
Zdjęcie rozciągnięte
picZdjecie.SizeMode = PictureBoxSizeMode.StretchImage;Obraz traci proporcje.
picZdjecie.SizeMode = PictureBoxSizeMode.Zoom;Zoom skaluje obraz proporcjonalnie.
Nie można podmienić pliku zdjęcia
picZdjecie.Image = Image.FromFile(sciezka);System blokuje plik, dopóki obraz jest używany.
using Image obraz = Image.FromFile(sciezka);
picZdjecie.Image = new Bitmap(obraz);Kopia przez new Bitmap uwalnia plik źródłowy.
Pierwszy formularz nie wraca
Hide();
wizytowka.ShowDialog();Po zamknięciu wizytówki Form1 pozostaje ukryty (zwłaszcza po błędzie).
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.
Podsumowanie
Co warto zapamiętać
- Dwa formularze współpracują przez
Hide(),ShowDialog()iShow()wfinally. - Dane między ekranami można przekazać schowkiem statycznym (prosto) albo obiektem przez konstruktor (lepiej).
- Zdjęcie wczytuj przez kopię
new Bitmap(obraz)i zwalniaj obrazy przezDispose(). DrawToBitmap()zapisuje kontrolkę z jej dziećmi, więc cała wizytówka musi leżeć na panelu.- 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ę →