Grafikus C# – Windows Forms tanfolyam › 6.10 lecke

Windows Forms · 6.10 · záró mini projekt

Mini projekt: átméretezhető űrlap – táborjelentkezés

A 6. fejezet záró projektjében a felülettervezés minden eszközét egyetlen programban használod: egy nyári tábor jelentkezési űrlapját készíted el. Az űrlap átméretezhető (Anchor, Dock, SplitContainer), rendezett (TableLayoutPanel, FlowLayoutPanel, GroupBox), több lapra osztott (TabControl), egér nélkül is kitölthető (tabulátorsorrend, mnemonikus betűk, Enter-navigáció, érvényesítés), tisztességes alapértelmezett gombokkal (AcceptButton, CancelButton), nagyobb betűmérettel is rendezett, és hozzáférhető. Tanárként akár a saját iskolai programod űrlapját is ilyenre cserélheted.

  • kb. 4–5 óra
  • haladó szint
  • záró projekt
Diák C# programozást gyakorol egy tanulósarokban

A projekt végére

  • egy összetett, több tárolóból álló űrlapot tervezel meg és építesz fel;
  • az elrendezést úgy készíted el, hogy átméretezésnél és nagyobb betűnél is rendezett maradjon;
  • a lapok közötti lépést ellenőrzéssel kötöd (Selecting), a mezőket érvényesíted (Validating, ErrorProvider);
  • a fókuszt, a tabulátorsorrendet, az Enter-t és az alapértelmezett gombokat tudatosan állítod be;
  • élő összegzést és árszámítást építesz a Tag adatokra;
  • a hozzáférhetőségi szempontokat (kontraszt, címkék, AccessibleName, nem csak színnel jelzés) érvényesíted.

Mielőtt elkezded

Ehhez a projekthez a 6.1–6.9 lecke és az 5.5–5.8 lecke eszközei szükségesek. Tarts nyitva mellé egy-két korábbi leckét, itt az összerakás a feladat. Új Windows Forms projektet készíts, és vedd fel a BetuMeret (float, User, alapérték 9) beállítást a Properties → Settings.settings fájlban.

6.9 Hozzáférhetőség →

Követelmények és pontozás

A Táborjelentkezés program egy ablak, amelyen a szülő vagy a diák kitölti a jelentkezési adatokat, kiválasztja a turnust, az étkezést és a tevékenységeket, és látja az előzetes díjat.

RészKövetelményPont
Elrendezés (25)Az ablak átméretezhető: bal oldalon a TabControl (három lap: Adatok, Választások, Összegzés), jobb oldalon egy SplitContainer panel az élő kártyával és a díjjal; alul gombsor, állapotsor.10
Az Adatok lap TableLayoutPanel (címke AutoSize, mező Percent 100), a Választások lapon GroupBox-ok és egy FlowLayoutPanel.8
A gombsor FlowLayoutPanel (jobbra igazított), a jobb oldali panel összecsukható (Panel2Collapsed).7
Működés (30)Mezők: név (kötelező, legalább 3 karakter), e-mail (kötelező, érvényes alak), telefon (nem kötelező, csak számjegy, +, szóköz, -), születési dátum, osztály (kötelező, ComboBox).10
Választások: turnus és étkezés (egy-egy kötelező választógomb-csoport), tevékenységek (jelölőnégyzetek), megjegyzés (többsoros).10
Az előzetes díj és az élő kártya minden változásra frissül (az ár a Tag értékekből).10
Navigáció és ellenőrzés (25)Logikus tabulátorsorrend, mnemonikus betűk (Alt+betű), induló fókusz a névmezőn, Enter a következő mezőre (egysoros mezőben).8
Érvényesítés Validating és ErrorProvider segítségével; az Adatok lapról csak helyes adatokkal lehet továbblépni (Selecting); AutoValidate = EnableAllowFocusChange.9
AcceptButton (a Tovább, az utolsó lapon a Küldés), CancelButton (Mégse, CausesValidation = false); kilépés megerősítése kitöltött adatnál.8
Betűméret és hozzáférhetőség (20)Betűméret-választó (normál 9 pt, nagy 12 pt, nagyon nagy 15 pt): az űrlap minden méretnél rendezett, a minimális ablakméret a tartalomhoz igazodik, a választás a Settings-ben megmarad.10
Hibajelzés színnel és jellel/szöveggel, AccessibleName a címke nélküli elemeken, a szövegszínek kontrasztja legalább 4,5 : 1, rendszerszínek használata.10

Összesen: 100 pont.

Gondold végig!

Az űrlap három lapra osztása helyesebb, mint egyetlen hosszú űrlap? Mikor nem jó a füles felület, és mikor jobb az egyoldalas űrlap görgetősávval?

Egy lehetséges válasz

A füles (vagy lépésenkénti) felület akkor jó, ha az adatok logikusan csoportokra bomlanak, és a felhasználó nem akarja egyszerre látni az egészet (hosszú űrlap, több szakasz). Nem jó, ha a lapok között gyakran kell ugrálni, vagy a mezők egymástól függnek (az egyik lapon beírt adat befolyásolja a másikat, és a felhasználó nem látja a kapcsolatot). Rövid űrlapnál (5–8 mező) az egyoldalas elrendezés gyorsabb és átláthatóbb. Itt a díj és a kártya ezért mindig látható, a jobb oldali panelen.

A felület felépítése

Vezérlő(Name)Beállítás
FormForm1Text: Táborjelentkezés; StartPosition: CenterScreen; KeyPreview nem kell (ProcessCmdKey)
StatusStripssDock: Bottom; elemek: tsslAllapot (ToolStripStatusLabel), tspbKesz (ToolStripProgressBar, Maximum: 6)
FlowLayoutPanelflpGombokDock: Bottom; FlowDirection: RightToLeft; AutoSize: True, AutoSizeMode: GrowAndShrink; Padding: 8; gombok: btnMegse (&Mégse), btnKuld (&Küldés), btnTovabb (&Tovább), btnVissza (&Vissza), btnElo (Kár&tya ki-be, a jobb oldali panel összecsukásához)
SplitContainersplitDock: Fill; FixedPanel: Panel2; Panel1MinSize: 460; Panel2MinSize: 240. Fontos: a z-sorrendben ez legyen leghátul (Send to Back)
TabControltca split.Panel1-ben, Dock: Fill; lapok: tabAdatok, tabValasztasok, tabOsszegzes
TableLayoutPaneltlpAdatoka tabAdatok-on, Dock: Fill, 2 oszlop (AutoSize, Percent 100); sorok (AutoSize): txtNev, txtEmail, txtTelefon, dtpSzul, cmbOsztaly (mind Dock: Fill) + címkék (Anchor = Left, & betűkkel)
TableLayoutPaneltlpValasztasa tabValasztasok-on, Dock: Fill, 2 oszlop (Percent 50, 50), 3 sor (AutoSize, AutoSize, Percent 100)
GroupBox ×2grpTurnus, grpEtkezesRadioButton ×3 mindkettőben, Tag: ár (egész szám szövegként): turnus 42000, 45000, 48000; étkezés 12000, 8000, 0
FlowLayoutPanelflpTevCheckBox-ok: Úszás (3000), Számítástechnika (4000), Képzőművészet (2500), Foci (2000), Tag: ár; ColumnSpan: 2
TextBoxtxtMegjMultiline, AcceptsReturn: True, Dock: Fill, ColumnSpan: 2
Panel a tabOsszegzes-enchkElfogad, lblHibak, rtbOsszchkElfogad: Text: Elfogadom az adatkezelési tájékoztatót; lblHibak: AutoSize (+ MaximumSize); rtbOssz: ReadOnly, Dock: Fill
Jobb panel (split.Panel2)tlpKartya: lblKartya, lblAr, cmbBetuTableLayoutPanel 1 oszlop; lblKartya: AutoSize + MaximumSize (tördelés); lblAr: nagy félkövér; cmbBetu: DropDownList + címke (&Betűméret)
KomponensekepHiba, ttSugoErrorProvider, ToolTip

Tabulátorsorrend és mnemonikus betűk

Az űrlap felépítése után a View → Tab Order nézetben ellenőrizd a sorrendet: lapokon belül fentről lefelé, a címkék közvetlenül a mezők előtt. Mnemonikus betűk (különbözőek!): &Név, &E-mail, &Telefon, &Születési dátum → S&zületési dátum (az S foglalt), &Osztály, &Megjegyzés → Meg&jegyzés (az M a Mégse gombé), és a gombok: &Vissza, &Tovább, &Küldés, Még&se. Egyeztesd, hogy nincs ütköző betű.

📷 Képernyőkép helye

Mit tegyél ide: a tervezőnézet: bal oldalon a három lapos TabControl, jobb oldalon az élő kártya és a díj, alul a gombsor és az állapotsor.Javasolt fájlnév: winforms-6-10-felulet.webp · Alt szöveg: A táborjelentkezési űrlap felülete a tervezőben

Építés lépésenként

Építsd fel lépésről lépésre, és minden lépés után próbáld ki. Itt csak a feladat és a támpont áll, a teljes kód a következő szakaszban van.

1. lépés: az elrendezés váza

Feladat: Állítsd össze a StatusStrip, a flpGombok és a split vázát, a TabControl-t és a három lapot. Futtasd, és méretezd át az ablakot.

Támpont: Először a szélre dokkolt elemek (állapotsor, gombsor), a SplitContainer (Fill) legyen utolsó: jobb gomb → Send to Back. Ha a gombsor vagy az állapotsor nem látszik, a z-sorrend rossz (6.2 lecke).

2. lépés: az Adatok lap rácsa

Feladat: A tlpAdatok két oszloppal, öt sorral, címkékkel és mezőkkel. A mezők nyúljanak az ablakkal.

Támpont: Oszlopok: AutoSize és Percent 100. Mezők Dock = Fill, címkék Anchor = Left (6.4 lecke).

3. lépés: a Választások lap

Feladat: Két GroupBox választógombokkal (a Tag az ár), egy FlowLayoutPanel a jelölőnégyzetekkel, a megjegyzés mező. Kösd a rácsba, hogy mindkét oszlop azonos szélességű legyen.

Támpont: Egy GroupBox egy cellába kerül (Dock = Fill), az árak szöveges Tag értékek, a kódban int.TryParse-szal olvashatók (5.8 lecke).

4. lépés: élő kártya és díj

Feladat: Egy közös Frissit() metódus minden változásra frissíti a kártyát (név, turnus, étkezés, tevékenységek), a díjat és a haladásjelzőt. Köss minden mezőt egy közös kezelőhöz rekurzív bejárással.

Támpont: A TextBox a TextChanged, a ComboBox a SelectedIndexChanged, a CheckBox és RadioButton a CheckedChanged eseményt adja (5.5, 5.8 lecke). A formátum: {0:N0} Ft.

5. lépés: érvényesítés és lapváltás

Feladat: Az AdatokRendben(bool jelez) metódus ellenőrzi az öt mezőt. A Validating kezelők és a tc_Selecting ezt hívják. A hibát ErrorProvider és az állapotsor jelzi, jellel és szöveggel.

Támpont: A Selecting csak az Adatok lap elhagyásánál ellenőriz (6.5 lecke). A Frissit() hívása NevOk(false) alakban jelzés nélkül ellenőriz.

6. lépés: navigáció és gombok

Feladat: A Vissza és Tovább gombok lapoznak, az utolsó lapon a Küldés látszik. Az AcceptButton a lapnak megfelelő gomb, a CancelButton a Mégse. A Enter egysoros mezőben továbblép (ProcessCmdKey), az induló fókusz a névmezőn.

Támpont: A rejtett gomb nem foglal helyet a FlowLayoutPanelben (6.4 lecke), és nem lehet AcceptButton. A Mégse gomb CausesValidation = false (6.6, 6.7 lecke).

7. lépés: betűméret

Feladat: A cmbBetu három betűméretet kínál (9, 12, 15 pt). A választás az ablak Font értékét állítja, a lblAr az ablak betűjéhez viszonyított méretű, és az ablak MinimumSize értékét a tartalomhoz igazítod. Mentsd a választást a Settings-be.

Támpont: SuspendLayout/ResumeLayout, GetPreferredSize (6.8 lecke). A betöltésnél ellenőrizd a mentett értéket.

8. lépés: Küldés, Összegzés és hozzáférhetőség

Feladat: Az Összegzés lap a lapra lépéskor felépíti a szöveget. A Küldés ellenőrzi az összes kötelező elemet, a hibákat jellel (✖) és szöveggel felsorolja, és az első hibás elemhez visz. Állítsd be az AccessibleName értékeket, ellenőrizd a színek kontrasztját, és használj rendszerszíneket.

Támpont: Sötét árnyalatú zöld és piros (4,5 : 1 felett), SystemColors mindenhol, ahol nincs szükség saját színre (6.9 lecke).

Mintamegoldás

Előbb próbáld meg önállóan!

A mintamegoldás csak az ellenőrzésre való. A felület kialakítása sokféle lehet, és a tiéd jobb is lehet. Először a lépéseket próbáld meg a támpontok alapján, és csak elakadásnál nézd meg a kódot.

Megoldás: Form1.cs (teljes kód)
Form1.csC#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Globalization;
using System.Windows.Forms;

namespace Tabor
{
public partial class Form1 : Form
{
private bool betoltve = false;
private bool elkuldve = false;

private readonly Color zold = Color.FromArgb(0, 100, 40);
private readonly Color piros = Color.FromArgb(170, 20, 20);

public Form1()
{
InitializeComponent();
}

// ---- indulás ----

private void Form1_Load(object sender, EventArgs e)
{
this.AutoValidate = AutoValidate.EnableAllowFocusChange;
this.AcceptButton = btnTovabb;
this.CancelButton = btnMegse;
btnMegse.CausesValidation = false;

split.Panel1MinSize = 460;
split.Panel2MinSize = 240;
split.FixedPanel = FixedPanel.Panel2;

cmbOsztaly.Items.AddRange(new object[] { "5. osztály", "6. osztály", "7. osztály", "8. osztály", "9. osztály", "10. osztály" });
cmbBetu.Items.AddRange(new object[] { "Normál (9 pt)", "Nagy (12 pt)", "Nagyon nagy (15 pt)" });

// hozzáférhetőség: név a címke nélküli, illetve a másik panelen lévő elemeknek
txtMegj.AccessibleName = "Megjegyzés";
cmbBetu.AccessibleName = "Betűméret";
rtbOssz.AccessibleName = "A jelentkezés összegzése";
ttSugo.SetToolTip(cmbBetu, "A felület betűméretének választása");
ttSugo.SetToolTip(chkElfogad, "A küldéshez el kell fogadni a tájékoztatót.");

float mentett = Properties.Settings.Default.BetuMeret;
int index = mentett >= 15 ? 2 : (mentett >= 12 ? 1 : 0);
cmbBetu.SelectedIndex = index;
BetuBeallit(index);

this.ActiveControl = txtNev;
Kot(this);

betoltve = true;
GombFrissit();
Frissit();
}

private void Kot(Control szulo)
{
foreach (Control c in szulo.Controls)
{
if (c == cmbBetu)
{
continue;
}

if (c is TextBox)
{
((TextBox)c).TextChanged += Valtozas;
}
else if (c is ComboBox)
{
((ComboBox)c).SelectedIndexChanged += Valtozas;
}
else if (c is CheckBox)
{
((CheckBox)c).CheckedChanged += Valtozas;
}
else if (c is RadioButton)
{
((RadioButton)c).CheckedChanged += Valtozas;
}

Kot(c);
}
}

private void Valtozas(object sender, EventArgs e)
{
Frissit();
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
Properties.Settings.Default.Save();
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason != CloseReason.UserClosing || elkuldve)
{
return;
}

bool kitoltott = txtNev.Text.Trim().Length > 0 || txtEmail.Text.Trim().Length > 0;

if (kitoltott && MessageBox.Show("A jelentkezés még nincs elküldve. Biztosan kilépsz?", "Kilépés", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No)
{
e.Cancel = true;
}
}

// ---- betűméret és méretezés ----

private void BetuBeallit(int index)
{
float[] meretek = { 9f, 12f, 15f };

this.SuspendLayout();

try
{
this.Font = new Font(this.Font.FontFamily, meretek[index]);
lblAr.Font = new Font(this.Font.FontFamily, meretek[index] * 1.6f, FontStyle.Bold);
lblKartya.MaximumSize = new Size(Math.Max(120, split.Panel2.ClientSize.Width - 20), 0);
}
finally
{
this.ResumeLayout(true);
}

MinimumBeallit();
Properties.Settings.Default.BetuMeret = meretek[index];
}

private void MinimumBeallit()
{
Size kivant = tlpValasztas.GetPreferredSize(Size.Empty);
int keretSzel = this.Width - this.ClientSize.Width;
int keretMag = this.Height - this.ClientSize.Height;

// közelítés: a lap és a jobb panel minimuma, a gombsor és az állapotsor
int szel = Math.Max(kivant.Width + 40, split.Panel1MinSize) + split.Panel2MinSize + split.SplitterWidth + keretSzel;
int mag = kivant.Height + flpGombok.Height + ss.Height + 90 + keretMag;

this.MinimumSize = new Size(szel, mag);
}

private void cmbBetu_SelectionChangeCommitted(object sender, EventArgs e)
{
BetuBeallit(cmbBetu.SelectedIndex);
}

private void split_SizeChanged(object sender, EventArgs e)
{
if (betoltve)
{
lblKartya.MaximumSize = new Size(Math.Max(120, split.Panel2.ClientSize.Width - 20), 0);
}
}

private void btnElo_Click(object sender, EventArgs e)
{
split.Panel2Collapsed = !split.Panel2Collapsed;
}

// ---- ellenőrzés ----

private bool Hiba(Control mezo, string uzenet, bool jelez)
{
if (jelez)
{
epHiba.SetError(mezo, uzenet);
}

return uzenet.Length == 0;
}

private bool NevOk(bool jelez)
{
return Hiba(txtNev, txtNev.Text.Trim().Length < 3 ? "A név legalább 3 karakter legyen." : "", jelez);
}

private bool EmailOk(bool jelez)
{
string s = txtEmail.Text.Trim();
int at = s.IndexOf('@');
bool ok = at > 0 && s.LastIndexOf('.') > at + 1 && !s.EndsWith(".");

return Hiba(txtEmail, ok ? "" : "Az e-mail cím alakja: nev@pelda.hu", jelez);
}

private bool TelefonOk(bool jelez)
{
string uzenet = "";

foreach (char c in txtTelefon.Text)
{
if (!char.IsDigit(c) && c != '+' && c != ' ' && c != '-')
{
uzenet = "A telefonszám csak számjegyet, +, - jelet és szóközt tartalmazhat.";
break;
}
}

return Hiba(txtTelefon, uzenet, jelez);
}

private bool OsztalyOk(bool jelez)
{
return Hiba(cmbOsztaly, cmbOsztaly.SelectedIndex < 0 ? "Válaszd ki az osztályt." : "", jelez);
}

private bool AdatokRendben(bool jelez)
{
bool a = NevOk(jelez);
bool b = EmailOk(jelez);
bool c = TelefonOk(jelez);
bool d = OsztalyOk(jelez);

return a && b && c && d;
}

private void txtNev_Validating(object sender, CancelEventArgs e)
{
if (!NevOk(true)) { e.Cancel = true; }
}

private void txtEmail_Validating(object sender, CancelEventArgs e)
{
if (!EmailOk(true)) { e.Cancel = true; }
}

private void txtTelefon_Validating(object sender, CancelEventArgs e)
{
if (!TelefonOk(true)) { e.Cancel = true; }
}

private void cmbOsztaly_Validating(object sender, CancelEventArgs e)
{
if (!OsztalyOk(true)) { e.Cancel = true; }
}

// ---- lapok és gombok ----

private void tc_Selecting(object sender, TabControlCancelEventArgs e)
{
if (!betoltve)
{
return;
}

if (tc.SelectedTab == tabAdatok && e.TabPageIndex > 0 && !AdatokRendben(true))
{
tsslAllapot.Text = "✖ Előbb javítsd az Adatok lap hibáit.";
e.Cancel = true;
}
}

private void tc_SelectedIndexChanged(object sender, EventArgs e)
{
if (!betoltve)
{
return;
}

if (tc.SelectedTab == tabOsszegzes)
{
OsszegzesEpit();
}

GombFrissit();
}

private void GombFrissit()
{
btnVissza.Enabled = tc.SelectedIndex > 0;
btnTovabb.Visible = tc.SelectedIndex < tc.TabCount - 1;
btnKuld.Visible = tc.SelectedIndex == tc.TabCount - 1;

this.AcceptButton = btnTovabb.Visible ? btnTovabb : btnKuld;
}

private void btnTovabb_Click(object sender, EventArgs e)
{
if (tc.SelectedIndex < tc.TabCount - 1)
{
tc.SelectedIndex = tc.SelectedIndex + 1; // a Selecting ellenőriz
}
}

private void btnVissza_Click(object sender, EventArgs e)
{
if (tc.SelectedIndex > 0)
{
tc.SelectedIndex = tc.SelectedIndex - 1;
}
}

private void btnMegse_Click(object sender, EventArgs e)
{
this.Close();
}

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Enter || keyData == (Keys.Shift | Keys.Enter))
{
TextBox mezo = this.ActiveControl as TextBox;

if (mezo != null && !mezo.Multiline)
{
this.SelectNextControl(mezo, keyData == Keys.Enter, true, true, true);
return true;
}
}

return base.ProcessCmdKey(ref msg, keyData);
}

// ---- választások, díj, kártya ----

private RadioButton ValasztottGomb(GroupBox csoport)
{
foreach (Control c in csoport.Controls)
{
RadioButton rb = c as RadioButton;

if (rb != null && rb.Checked)
{
return rb;
}
}

return null;
}

private int TagAr(Control c)
{
int ar;

if (c != null && c.Tag != null && int.TryParse(c.Tag.ToString(), out ar))
{
return ar;
}

return 0;
}

private List<CheckBox> Tevekenysegek()
{
List<CheckBox> lista = new List<CheckBox>();

foreach (Control c in flpTev.Controls)
{
CheckBox cb = c as CheckBox;

if (cb != null && cb.Checked)
{
lista.Add(cb);
}
}

return lista;
}

private int Ar()
{
int osszeg = TagAr(ValasztottGomb(grpTurnus)) + TagAr(ValasztottGomb(grpEtkezes));

foreach (CheckBox cb in Tevekenysegek())
{
osszeg = osszeg + TagAr(cb);
}

return osszeg;
}

private void Frissit()
{
if (!betoltve)
{
return;
}

int kesz = 0;

if (NevOk(false)) { kesz++; }
if (EmailOk(false)) { kesz++; }
if (OsztalyOk(false)) { kesz++; }
if (ValasztottGomb(grpTurnus) != null) { kesz++; }
if (ValasztottGomb(grpEtkezes) != null) { kesz++; }
if (chkElfogad.Checked) { kesz++; }

tspbKesz.Value = kesz;
tsslAllapot.Text = $"Kitöltöttség: {kesz}/6 kötelező elem";

RadioButton turnus = ValasztottGomb(grpTurnus);
RadioButton etkezes = ValasztottGomb(grpEtkezes);
List<string> tev = new List<string>();

foreach (CheckBox cb in Tevekenysegek())
{
tev.Add(cb.Text.Replace("&", ""));
}

lblKartya.Text = "Név: " + (txtNev.Text.Trim().Length == 0 ? "–" : txtNev.Text.Trim())
+ "\r\nTurnus: " + (turnus == null ? "–" : turnus.Text.Replace("&", ""))
+ "\r\nÉtkezés: " + (etkezes == null ? "–" : etkezes.Text.Replace("&", ""))
+ "\r\nTevékenységek: " + (tev.Count == 0 ? "–" : string.Join(", ", tev));

lblAr.Text = string.Format(CultureInfo.CurrentCulture, "{0:N0} Ft", Ar());
this.Text = txtNev.Text.Trim().Length == 0 ? "Táborjelentkezés" : "Táborjelentkezés – " + txtNev.Text.Trim();
}

private void OsszegzesEpit()
{
List<string> sorok = new List<string>();
RadioButton turnus = ValasztottGomb(grpTurnus);
RadioButton etkezes = ValasztottGomb(grpEtkezes);

sorok.Add("Név: " + txtNev.Text.Trim());
sorok.Add("E-mail: " + txtEmail.Text.Trim());
sorok.Add("Telefon: " + (txtTelefon.Text.Trim().Length == 0 ? "(nincs megadva)" : txtTelefon.Text.Trim()));
sorok.Add("Születési dátum: " + dtpSzul.Value.ToString("yyyy. MMMM d."));
sorok.Add("Osztály: " + (cmbOsztaly.SelectedIndex < 0 ? "–" : cmbOsztaly.Text));
sorok.Add("Turnus: " + (turnus == null ? "–" : turnus.Text.Replace("&", "")));
sorok.Add("Étkezés: " + (etkezes == null ? "–" : etkezes.Text.Replace("&", "")));
sorok.Add("Megjegyzés: " + (txtMegj.Text.Trim().Length == 0 ? "(nincs)" : txtMegj.Text.Trim()));
sorok.Add("Előzetes díj: " + lblAr.Text);

rtbOssz.Text = string.Join(Environment.NewLine, sorok);
}

// ---- küldés ----

private void btnKuld_Click(object sender, EventArgs e)
{
bool adatok = AdatokRendben(true);
List<string> hibak = new List<string>();

if (!adatok) { hibak.Add("Az Adatok lapon javítandó mezők vannak."); }
if (ValasztottGomb(grpTurnus) == null) { hibak.Add("Válassz turnust."); }
if (ValasztottGomb(grpEtkezes) == null) { hibak.Add("Válassz étkezést."); }
if (!chkElfogad.Checked) { hibak.Add("Fogadd el az adatkezelési tájékoztatót."); }

if (hibak.Count > 0)
{
lblHibak.ForeColor = piros;
lblHibak.Text = "✖ " + string.Join(" ✖ ", hibak);
tsslAllapot.Text = "✖ A jelentkezés nem küldhető el.";

if (!adatok)
{
tc.SelectedTab = tabAdatok;
txtNev.Focus();
}
else if (ValasztottGomb(grpTurnus) == null || ValasztottGomb(grpEtkezes) == null)
{
tc.SelectedTab = tabValasztasok;
}

return;
}

elkuldve = true;
lblHibak.ForeColor = zold;
lblHibak.Text = "✔ Minden rendben, a jelentkezés elküldve.";
tsslAllapot.Text = "✔ Elküldve.";
btnKuld.Enabled = false;

MessageBox.Show($"Köszönjük, {txtNev.Text.Trim()}! A jelentkezést rögzítettük.\r\nElőzetes díj: {lblAr.Text}", "Táborjelentkezés");
}
}
}
Megoldás: miért így épül fel?

A felépítés a fejezet leckéit követi: a szélre dokkolt elemek és a Fill SplitContainer (6.2, 6.3), az űrlapok rácsai (6.4), a füles lapozás és a Selecting ellenőrzés (6.5), a fókusz, az Enter és az érvényesítés (6.6), az alapértelmezett gombok (6.7), a betűméret és a tartalomhoz igazodó minimum (6.8), és a hozzáférhetőség (6.9). Az egyik fontos szerkezeti döntés: az ellenőrző metódusok (NevOk(bool jelez)) kétféleképp is használhatók: jelzéssel (Validating, Selecting, küldés) és jelzés nélkül (a haladásjelző). Így nem kell az ellenőrzést megismételni.

Tesztlista

#TesztVárt eredmény
1A program indul.A fókusz a névmezőn, az Adatok lap látszik, a Vissza gomb letiltott, a haladásjelző 0/6.
2Az ablak nagyítása és kicsinyítése a minimumig.A mezők nyúlnak, a gombok a jobb alsó sarokban maradnak, nincs átlapolódás a minimum alatt.
3Üres név, majd Tab.A hibaikon és üzenet megjelenik; az AutoValidate miatt a fókusz továbbléphet.
4Hibás e-mail (kiss.peter), majd Tovább.A lap nem vált, az állapotsorban ✖ üzenet.
5Helyes adatok, Tovább.A Választások lap jelenik meg, a Vissza aktív.
6Turnus, étkezés és két tevékenység kiválasztása.A díj és a kártya azonnal frissül; a díj a Tag értékek összege.
7Az Enter az egysoros mezőkben.A következő mezőre lép; a megjegyzés mezőben új sort ad.
8Shift+Tab és Alt+betűk.A tabulátorsorrend logikus, az Alt+betűk a megfelelő mezőre/gombra visznek, nincs ütköző betű.
9A Küldés hibás/hiányos adatokkal.A hibák felsorolva ✖ jellel, az első hibás laphoz/mezőhöz visz.
10Az adatkezelési tájékoztató elfogadása nélkül küldés.Hibaüzenet, nem küldhető el.
11A teljes helyes jelentkezés elküldése.✔ üzenet, a Küldés letiltva, a program nem kér megerősítést kilépéskor.
12A Mégse hibás mező mellett.Azonnal megnyomható (CausesValidation = false); kitöltött adatnál megerősítést kér.
13Esc.A Mégse gomb lefut (CancelButton).
14Betűméret: nagy (12 pt), nagyon nagy (15 pt).Az egész felület nagyobb lesz, rendezett marad; a díj felirat arányos; a minimális ablakméret nő.
15Újraindítás a nagy betűméret után.A betűméret megmarad (Settings).
16A jobb oldali panel összecsukása.A lapok kitöltik az ablakot.
17Billentyűzet-teszt: az űrlap kitöltése és elküldése egér nélkül.Minden elérhető, nincs fókuszcsapda.
18Windows méretezés 150%, szövegméret 150%, kontrasztos téma.Az űrlap használható, a szöveg olvasható.

Gyakori hibák és önértékelés

Ezeken szoktak elcsúszni

Gondold végig, nálad előfordul-e valamelyik.

1. A gombsor vagy az állapotsor nem látszik

Az ablak aljáról hiányzik a gombsor.

Ok és javítás

A Fill dokkolású SplitContainer takarja őket: a z-sorrend rossz. Jobb gomb a split vezérlőn → Send to Back (6.2 lecke).

2. A mezők nem nyúlnak az űrlapon

Az ablak nagyításakor a szövegmezők kicsik maradnak.

Ok és javítás

A mezők Dock értéke nem Fill, vagy a rács második oszlopa nem Percent 100 (6.4 lecke).

3. A lapváltás nem ellenőriz (vagy végtelen hurok)

private void tc_Selecting(object sender, TabControlCancelEventArgs e)
{
if (!AdatokRendben(true))
{
tc.SelectedIndex = 0; // újabb Selecting!
}
}
Hiba és javítás

A Selecting kezelőben ne váltsd a lapot: e.Cancel = true. Az ellenőrzést csak az Adatok lap elhagyásánál végezd (tc.SelectedTab == tabAdatok), különben a visszalépést is megakadályozod.

4. A haladásjelző hibát jelez a mezőkön

A Frissit() hívása minden betű beírásánál piros ikonokat mutat a még ki nem töltött mezőkön.

Ok és javítás

A Frissit() a NevOk(true) változatot hívja. A haladás számolásához jelzés nélküli ellenőrzés kell (NevOk(false)): a hibaikon csak a mezők elhagyásakor és a lapváltásnál jelenjen meg.

5. A nagy betűnél szétcsúszik az űrlap

15 pontos betűnél a jobb oldali kártya kilóg, a gombok levágódnak.

Ok és javítás

Valahol fix méret maradt: egy AutoSize = false címke, fix magasságú sor, fix MinimumSize. Használj AutoSize sorokat, MaximumSize a tördelő címkén, és a MinimumSize értéket számold a tartalomból (6.8 lecke).

6. A hibák csak pirossal vannak jelezve

A hibaüzenet csak piros szöveg.

Ok és javítás

Adj mellé jelet (✖) és szöveges leírást, a fókuszt tedd a hibás mezőre, és sötét árnyalatot használj (4,5 : 1 felett) (6.9 lecke).

Az értékeléshez ellenőrizd az alábbi szempontokat is:

MűködésA tesztlista mind a 18 pontja teljesül.
SzerkezetAz ellenőrzés és a díjszámítás egy-egy metódusban van, nincs ismétlődő kód; a közös kezelők rekurzívan kötődnek.
HasználhatóságEgér nélkül is kitölthető, a hibák érthetők, a gombok a helyükön vannak.
HozzáférhetőségBetűméret, kontraszt, nem csak színnel jelzés, AccessibleName, rendszerszínek.

Bővítési ötletek, ha kész vagy

Ha maradt időd, próbáld ki:

  • Testvérkedvezmény: egy NumericUpDown a gyerekek számához; a második gyerektől 10% kedvezmény.
  • Piszkozat mentése: az adatok mentése fájlba (JSON vagy CSV, 10. fejezet) és visszatöltés induláskor.
  • Nyomtatás: az összegzés nyomtatása (2.15 lecke).
  • Adatbázis: a jelentkezések mentése SQLite-ba (3. fejezet), és egy listázó ablak.
  • Hozzáférhetőségi ellenőrzés: a 6.9 lecke ellenőrzőjének bekötése egy menüpontba, amely a saját űrlapodat vizsgálja.
  • Téma: sötét mód a rendszerszínek mellett (7. fejezet menüjével).

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. A kész űrlapot bővíted. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Tabulátorsorrend leírása

Írd le táblázatban a kész űrlap tabulátorsorrendjét az Adatok lapon (címkék és mezők, TabIndex szerint). Hol kell a címke és hol a mező?

Megoldás megnyitása
TabIndexVezérlőMegjegyzés
0lblNev (&Név)nem kap fókuszt, a mnemonikus billentyű a következőre ugrik
1txtNevinduló fókusz
2lblEmail
3txtEmail
4–5lblTelefon, txtTelefon
6–7lblSzul, dtpSzul
8–9lblOsztaly, cmbOsztaly

2. Árszámítás ellenőrzése

Számold ki kézzel a díjat: 2. turnus (45 000), félpanziós étkezés (8 000), úszás (3 000) és foci (2 000) tevékenység. Hogyan ellenőrzöd, hogy a program ugyanezt adja?

Megoldás megnyitása

45 000 + 8 000 + 3 000 + 2 000 = 58 000 Ft. A program felirata „58 000 Ft” (a csoportosító elválasztó a nyelvi beállítástól függően szóköz). Egy teszteset (a tesztlista 6. sora) az ismert számmal ellenőrzi.

Közepes szint

3. Testvérkedvezmény

Egy nudGyerek mező (1–5) adja a gyerekek számát. Az 1. gyerek teljes árat fizet, a további gyerekek a díj 90%-át. Hogyan számolod az összdíjat, és mire kell vigyázni a kerekítésnél?

Megoldás megnyitása

Egy gyerek díja: Ar(). Összesen: egy + (db - 1) * egy * 0,9. A pénzösszegnél a decimal típus pontosabb (4.4 lecke), és a végösszeget egészre kerekítjük (Math.Round).

Form1.csC#
private decimal OsszesDij()
{
decimal egy = Ar();
int db = (int)nudGyerek.Value;

decimal osszeg = egy + (db - 1) * egy * 0.9m;

return Math.Round(osszeg, 0);
}

4. Nyomtatható összegzés gomb

Az Összegzés lapon egy Nyomtatás gomb a rtbOssz tartalmát nyomtatja. Hol találod meg a megoldás elemeit?

Megoldás megnyitása

A PrintDocument és a PrintDialog a 2.15 leckében szerepel: a PrintPage eseményben a szöveget a Graphics.DrawString rajzolja. Az összegzés szövege a rtbOssz.Text.

Form1.csC#
private void pdNyomtat_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e)
{
using (Font betu = new Font("Segoe UI", 12))
{
e.Graphics.DrawString(rtbOssz.Text, betu, Brushes.Black, e.MarginBounds);
}
}

private void btnNyomtat_Click(object sender, EventArgs e)
{
if (pdlNyomtat.ShowDialog() == DialogResult.OK)
{
pdNyomtat.Print();
}
}

Kihívás szint

5. Kontraszt-ellenőrzés a saját színekre

Ellenőrizd a Kontraszt metódussal (6.9 lecke), hogy a zold és a piros szín fehér háttéren eléri-e a 4,5 : 1 arányt. Mit teszel, ha nem?

Megoldás megnyitása

A Hozzaferhetoseg.Kontraszt(zold, Color.White) és a piros értéke is 4,5 fölé kell kerüljön (a mintaszínek, (0, 100, 40) és (170, 20, 20), elég sötétek). Ha nem, sötétíts rajta, amíg az arány megfelelő. A kontraszt számolása automatikus tesztként is beépíthető a fejlesztésbe.

Form1.csC#
private void SzinEllenorzes()
{
double z = Hozzaferhetoseg.Kontraszt(zold, Color.White);
double p = Hozzaferhetoseg.Kontraszt(piros, Color.White);

lblSzinInfo.Text = $"Zöld: {z:0.0} : 1 {(z >= 4.5 ? "✔" : "✖")} | Piros: {p:0.0} : 1 {(p >= 4.5 ? "✔" : "✖")}";
}

6. A fókusz követése az állapotsorban

Az állapotsor második felirata mutassa, melyik mezőn áll a fókusz, és milyen segítséget ad (Tag vagy AccessibleName). Hogyan oldod meg egyetlen közös kezelővel?

Megoldás megnyitása

A Kot metódus mellé egy második rekurzív metódus az Enter eseményre köt egy közös kezelőt (5.8 lecke). A kezelő a sender AccessibleName vagy Name értékét írja ki.

Form1.csC#
private void FokuszKot(Control szulo)
{
foreach (Control c in szulo.Controls)
{
c.Enter += Fokusz_Enter;
FokuszKot(c);
}
}

private void Fokusz_Enter(object sender, EventArgs e)
{
Control c = (Control)sender;
string nev = string.IsNullOrEmpty(c.AccessibleName) ? c.Name : c.AccessibleName;

tsslFokusz.Text = "Fókusz: " + nev;
}

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Miért kell a Fill SplitContainert leghátul tartani?

Hogy utolsóként kapja a maradék helyet, és ne takarja el a szélre dokkolt gombsort és állapotsort (Send to Back).

2. Hogyan működik a lapváltás ellenőrzése?

A Selecting eseményben az Adatok lap elhagyásánál AdatokRendben(true) fut, és hiba esetén e.Cancel = true.

3. Miért kétféle ellenőrző metódus-használat (jelez igaz/hamis)?

A jelzéssel (ErrorProvider) történő ellenőrzés a mező elhagyásakor kell, a jelzés nélküli a haladásjelzőhöz. Egy metódus mindkettőt kiszolgálja.

4. Miért CausesValidation = false a Mégse gombon?

Hogy hibás mező mellett is azonnal megnyomható legyen, és a felhasználó ne legyen a mezőhöz láncolva.

5. Hogyan marad rendezett az űrlap nagyobb betűnél?

Tartalomhoz igazodó elrendezés (AutoSize sorok, TableLayoutPanel), az ablak Font értékének módosítása, és a MinimumSize számolása a tartalomból.

6. Hogyan számolod a díjat?

A választógombok és jelölőnégyzetek Tag értékeinek összegével (int.TryParse), egy közös Frissit() metódusban.

7. Mi teszi hozzáférhetővé az űrlapot?

Címkék és AccessibleName, logikus billentyűzetes használat, hibajelzés színen kívül is (✖, szöveg), jó kontraszt, rendszerszínek, átméretezhető betű.

Mit tudsz már? (a 6. fejezet végén)

  • Átméretezhető, rendezett űrlapot tervezel Anchor, Dock, SplitContainer, TableLayoutPanel és FlowLayoutPanel segítségével.
  • Füles felületet és csoportokat (TabControl, GroupBox) használsz, a lapváltást ellenőrzéssel kötöd.
  • Az űrlapot egér nélkül is használhatóvá teszed (fókusz, TabIndex, Enter, AcceptButton, CancelButton).
  • Érvényesítést építesz Validating, ErrorProvider és ValidateChildren segítségével.
  • Különböző betűméreten és méretezésen is rendezett, hozzáférhető felületet készítesz.
  • Elkészítettél egy teljes, valódi űrlapot ezekkel az eszközökkel.

Következő fejezet: menük, dialógusok és több ablak

A 6. fejezet kész. A 7. fejezetben a programok keretét építed ki: menük, eszköztárak, állapotsor, párbeszédablakok és több ablak közötti adatátadás.