Grafikus C# – Windows Forms tanfolyam › 12.7 lecke Windows Forms · 12.7 lecke · OOP Nézd meg egy régebbi programod egyik Programlogika külön osztályban
Click eseménykezelőjét: valószínűleg beolvassa a TextBoxokat, számol, ellenőriz, és kiírja az eredményt, mindezt egyetlen metódusban. Ez működik, de nem lehet kipróbálni gomb nélkül, nem használható máshol, és minden módosítás kockázatos. A megoldás: a számítást és a szabályokat kiemeled egy külön osztályba, amely semmit sem tud a felületről. A Form csak beolvassa a bemenetet, meghívja a logikát, és megjeleníti az eredményt. Ebben a leckében megtanulod a lépéseit (Extract Method, Extract Class), a tiszta függvény fogalmát, a függőségek átadását, és azt, hogyan próbálod ki a logikát tesztekkel.
A lecke végére
- felismered a logikát a Form kódjában, és kiemeled egy külön osztályba;
- alkalmazod az Extract Method és Extract Class lépéseit;
- tiszta függvényt írsz (ugyanarra a bemenetre mindig ugyanaz az eredmény, mellékhatás nélkül);
- a függőségeket (idő, véletlen, adatforrás) kívülről adod át;
- egyszerű tesztekkel igazolod a logikát, felület nélkül;
- a Form feladatait pontosan körülhatárolod (beolvas, hív, megjelenít).
Mielőtt elkezded
Ehhez a leckéhez a 12.1–12.6 lecke (osztályok, modellek, felelősségek) szükséges.
Az eredeti kód: minden egy helyen
private void btnSzamol_Click(object sender, EventArgs e)
{
double hossz, szel;
if (!double.TryParse(txtHossz.Text, out hossz) || !double.TryParse(txtSzel.Text, out szel))
{
MessageBox.Show("Számot adj meg!");
return;
}
if (hossz <= 0 || szel <= 0) { MessageBox.Show("Pozitív szám kell!"); return; }
double terulet = hossz * szel;
double keruelet = 2 * (hossz + szel);
double csempe = Math.Ceiling(terulet / 0.09) * 1.1; // 30x30-as csempe, 10% ráhagyás
lblTerulet.Text = terulet.ToString("0.##") + " m²";
lblKeruelet.Text = keruelet.ToString("0.##") + " m";
lblCsempe.Text = (int)Math.Ceiling(csempe) + " db";
}TryParse), ellenőrzés, számítás és kiírás egy metódusban. Ha a csempe-szabály változik (40×40-es csempe), a Click kezelőt kell bolygatni.MessageBox-szal itt a felület és a logika összefonódása: a számolás nem hívható háttérszálból vagy tesztből.Jósolj!
Szeretnél tesztet írni, amely ellenőrzi, hogy egy 3 × 4 méteres szobához hány csempe kell. Megoldható-e a fenti kóddal gombnyomás nélkül?
Jóslat ellenőrzése
Nem: a számítás a gomb eseménykezelőjében, a TextBox tartalmából dolgozik, és az eredményt címkébe írja. A tesztelhetőséghez a számításnak paramétereket kell kapnia és értéket visszaadnia.
Kiemelés lépésről lépésre
// ---- 1. az eredményt egy kis osztály hordozza ----
public class SzobaEredmeny
{
public double Terulet { get; set; }
public double Keruelet { get; set; }
public int Csempe { get; set; }
}
// ---- 2. a logika: nincs benne vezérlő, MessageBox, TryParse ----
public static class SzobaSzamolo
{
public const double CsempeTerulet = 0.09; // 30x30 cm
public const double Raghagyas = 1.10;
public static SzobaEredmeny Szamol(double hossz, double szel)
{
if (hossz <= 0) { throw new ArgumentOutOfRangeException("hossz", "A hossz pozitív szám kell legyen."); }
if (szel <= 0) { throw new ArgumentOutOfRangeException("szel", "A szélesség pozitív szám kell legyen."); }
double terulet = hossz * szel;
return new SzobaEredmeny
{
Terulet = terulet,
Keruelet = 2 * (hossz + szel),
Csempe = (int)Math.Ceiling(Math.Ceiling(terulet / CsempeTerulet) * Raghagyas)
};
}
}
// ---- 3. a Form: beolvas → hív → megjelenít ----
private void btnSzamol_Click(object sender, EventArgs e)
{
double hossz, szel;
if (!double.TryParse(txtHossz.Text, out hossz) || !double.TryParse(txtSzel.Text, out szel))
{
tsslUzenet.Text = "Számot adj meg!";
return;
}
try
{
SzobaEredmeny er = SzobaSzamolo.Szamol(hossz, szel);
lblTerulet.Text = er.Terulet.ToString("0.##") + " m²";
lblKeruelet.Text = er.Keruelet.ToString("0.##") + " m";
lblCsempe.Text = er.Csempe + " db";
}
catch (ArgumentOutOfRangeException ex) { tsslUzenet.Text = ex.Message; }
}A lépések
- Keresd meg a logikát: mit számol, mit ellenőriz, mit tárol.
- Extract Method: tedd metódusba (paraméterek be, érték ki).
- Vedd ki a felületet: a metódus ne érintsen vezérlőt, ne hívjon
MessageBox-ot. - Extract Class: vidd egy külön osztályba (Visual Studio: Ctrl+. → Move to new file).
- Hívd a Formból, és futtasd: a viselkedés változatlan.
Mit nyertél?
- A csempe-szabály egy helyen él.
- A
Szamolfelület nélkül kipróbálható. - Más ablakban, konzolban is használható.
- A Form rövid és áttekinthető.
- A hibaüzenet megjelenítése a Form dolga (állapotsor,
ErrorProvider,MessageBox).
TryParse: ez felületi feladat), meghívja a logikát, és megjeleníti az eredményt vagy a hibát.TryParse-szal kezeli.static class kényelmes. Ha az osztálynak állapota van (beállítások, részeredmények), példányosítandó osztály kell.record hordoz: nincs out paraméter-halmaz, és a használat olvasható.Jósolj!
A SzobaSzamolo.Szamol(0, 5) hívás mit csinál, és hogyan reagál erre a Form?
Jóslat ellenőrzése
ArgumentOutOfRangeException kivételt dob (A hossz pozitív szám kell legyen.); a Form catch blokkja elkapja, és az üzenetet az állapotsorban jeleníti meg. A logika nem tudja, hogyan fog megjelenni a hiba.
Tiszta függvények és függőségek
// ROSSZ: rejtett függőség az aktuális időre; a teszt eredménye minden nap más
public static int Eletkor(DateTime szuletes)
{
return DateTime.Today.Year - szuletes.Year; // a DateTime.Today a függvény BELSEJÉBŐL jön
}
// JÓ: a függőség paraméter: ugyanarra a bemenetre mindig ugyanaz a kimenet (TISZTA függvény)
public static int Eletkor(DateTime szuletes, DateTime mikor)
{
int kor = mikor.Year - szuletes.Year;
if (mikor < szuletes.AddYears(kor)) { kor--; } // még nem volt idén a születésnap
return kor;
}
// a Form adja át a mostani időt
lblKor.Text = Kor.Eletkor(dtpSzuletes.Value, DateTime.Today).ToString();
// véletlen: a Random is átadható (ugyanazzal a maggal mindig ugyanaz a sorozat)
public static int Dob(Random v) { return v.Next(1, 7); }
// adatforrás: függvényként (vagy felületként, 12.8 lecke) kapja, nem fájlból olvassa maga
public static double Atlag(IEnumerable<int> jegyek) { return jegyek.Any() ? jegyek.Average() : 0; }DateTime.Now), a véletlen (new Random()), a fájl vagy az adatbázis rejtett bemenet: a függvény látszólag paraméter nélkül „más eredményt ad”. Add át őket paraméterként.new DateTime(2025, 6, 1)) és a véletlent (new Random(42)): az eredmény reprodukálható.Jósolj!
A rosszul megírt Eletkor(DateTime szuletes) függvényt 2025-ben írtad meg és teszteled: Assert(Eletkor(new DateTime(2000,1,1)) == 25). Mi lesz a teszttel 2026-ban?
Jóslat ellenőrzése
A teszt elbukik (26 lesz): a függvény eredménye a naptártól is függ, nem csak a paramétertől. A mikor paraméter ezt a rejtett függőséget kiveszi.
A logika kipróbálása tesztekkel
public static class Tesztek
{
private static int hibak = 0;
private static void Egyenlo<T>(T vart, T kapott, string nev)
{
if (!Equals(vart, kapott)) { hibak++; Debug.WriteLine("HIBA " + nev + ": várt " + vart + ", kapott " + kapott); }
else { Debug.WriteLine("ok " + nev); }
}
private static void Kivetelt<TKiv>(Action muvelet, string nev) where TKiv : Exception
{
try { muvelet(); hibak++; Debug.WriteLine("HIBA " + nev + ": nem dobott kivételt"); }
catch (TKiv) { Debug.WriteLine("ok " + nev); }
}
public static int Mindet()
{
hibak = 0;
SzobaEredmeny r = SzobaSzamolo.Szamol(3, 4);
Egyenlo(12.0, r.Terulet, "terület 3x4");
Egyenlo(14.0, r.Keruelet, "kerület 3x4");
Egyenlo(148, r.Csempe, "csempe 3x4"); // kézi számítás: 12 / 0,09 = 133,33 → 134 db; × 1,1 = 147,4 → 148 db
Kivetelt<ArgumentOutOfRangeException>(() => SzobaSzamolo.Szamol(0, 5), "nulla hossz");
Kivetelt<ArgumentOutOfRangeException>(() => SzobaSzamolo.Szamol(5, -1), "negatív szélesség");
Egyenlo(24, Kor.Eletkor(new DateTime(2000, 6, 15), new DateTime(2025, 6, 14)), "még nincs szülinap"); // egy nappal a 25. születésnap előtt: 24 éves
return hibak;
}
}Debug.WriteLine-os segéddel mutatjuk.Jósolj!
A tesztben a várt értéket úgy kapod, hogy a program kimenetét bemásolod a vart helyére. A képlet hibás volt. Mit tapasztalsz?
Jóslat ellenőrzése
A teszt zölden fut, pedig a képlet hibás: a hibát bemásoltad a „helyes” értékbe. A teszt értéke független forrásból (kézi számítás, ismert példa) jön.
Mit tegyél ide: a hitelkalkulátor és a Debug ablakban a teszt eredménye (ok / HIBA sorok).Javasolt fájlnév: winforms-12-7-hitel.webp · Alt szöveg: A hitelkalkulátor és a tesztek
Hibakeresés: te vagy a hibadetektív
Mi a baj, és hogyan javítod?
Gondold végig az okot, és csak utána nyisd le a megoldást.
1. A logikai osztály vezérlőt ér el
public static double Szamol() { return double.Parse(Form1.txtHossz.Text) * 2; }Hiba és javítás
A logika a vezérlőhöz kötődik (és a txtHossz privát). Paramétert kapjon: Szamol(double hossz); a Form olvassa ki a TextBox-ot.
2. A teszt minden nap más eredményt ad
int kor = DateTime.Today.Year - szuletes.Year;Hiba és javítás
Rejtett függőség az időre. Add át a mikor paramétert.
3. A kivétel a felhasználóig jut
A hibás bemenetnél a program összeomlik, és a felhasználó a kivételablakot látja. A Form nem kapta el a logika kivételét. Ok és javítás
try–catch a hívásnál, és a hibaszöveg megjelenítése; a nem szám bemenetet előbb a TryParse kezelje.
4. A MessageBox a logikában marad
if (szel <= 0) { MessageBox.Show("Hibás"); return null; }Hiba és javítás
Dobj kivételt (vagy adj vissza hibaszöveget), és a Form jelenítse meg. A return null mellett a hívónak is null-t kell ellenőriznie.
5. A teszt a hibás képletet igazolja
A teszt zöld, a program mégis rossz eredményt ad. A várt értéket a program kimenetéből másoltad. Számold ki kézzel, vagy ismert, független példából.Ok és javítás
6. A számítás két helyen él
Az átméretezés és a nyomtatás is külön számolja a területet, kicsit eltérően. Kiemeletlen logika másolva. Egyetlen Ok és javítás
SzobaSzamolo.Szamol metódust hívjon mindkét hely.
Mini projekt: hitelkalkulátor tesztelhető logikával
Készíts hitelkalkulátort, ahol a pénzügyi számítás külön, felület nélküli osztályban él, és tesztekkel ellenőrzött:
- HitelSzamolo osztály (nincs
using System.Windows.Forms):HaviTorleszto(osszeg, eviKamat, honapok)annuitással:r = kamat/12,t = P·r / (1 − (1+r)^−n)(0% kamatnálP/n). - Törlesztési terv:
Terv(osszeg, eviKamat, honapok)→List<TorlesztesiSor>(hónap, törlesztő, kamat, tőke, maradék); az utolsó sor kerekítési különbségét az utolsó törlesztő rendezi. - Összesítés:
OsszesKamat(terv),VisszafizetendoOsszeg(terv). - Extra törlesztés:
RovidebbTerv(osszeg, kamat, honapok, extraHavi)mennyi hónap alatt fizetik vissza, és mennyi kamatot spórolnak. - Ellenőrzés: hibás bemenetnél (összeg ≤ 0, kamat < 0, hónap < 1)
ArgumentOutOfRangeException. - Form:
txtOsszeg,txtKamat,nudEv, Számol;DataGridViewa tervvel;lblHavi,lblOsszes,lblKamat; aTryParseés a hibák megjelenítése itt; a Form csak hívja a logikát. - Tesztek (
Tesztek.Mindet(), a Debug ablakba): ismert példa (1 000 000 Ft, 12% éves, 12 hónap → havi 88 848,79 Ft ±1), 0% kamat, 1 hónapos hitel, hibás bemenetek, a törlesztési terv végén 0 maradék, az összeg-azonosság (tőkék összege = hitel összege). - Teszteld: a Form és a tesztek is; hibás bemenetek (szöveg, negatív); nagyon hosszú lejárat (360 hónap): a DataGridView sebessége.
Megoldás megnyitása (próbáld ki előbb önállóan!)
public class TorlesztesiSor
{
public int Honap { get; set; }
public decimal Torleszto { get; set; }
public decimal Kamat { get; set; }
public decimal Toke { get; set; }
public decimal Maradek { get; set; }
}
public static class HitelSzamolo
{
private static void Ellenoriz(decimal osszeg, decimal eviKamat, int honapok)
{
if (osszeg <= 0) { throw new ArgumentOutOfRangeException("osszeg", "Az összeg pozitív kell legyen."); }
if (eviKamat < 0) { throw new ArgumentOutOfRangeException("eviKamat", "A kamat nem lehet negatív."); }
if (honapok < 1) { throw new ArgumentOutOfRangeException("honapok", "Legalább 1 hónap kell."); }
}
public static decimal HaviTorleszto(decimal osszeg, decimal eviKamat, int honapok)
{
Ellenoriz(osszeg, eviKamat, honapok);
if (eviKamat == 0) { return Math.Round(osszeg / honapok, 2); }
double r = (double)eviKamat / 100.0 / 12.0;
double t = (double)osszeg * r / (1 - Math.Pow(1 + r, -honapok));
return Math.Round((decimal)t, 2);
}
public static List<TorlesztesiSor> Terv(decimal osszeg, decimal eviKamat, int honapok, decimal extraHavi = 0)
{
Ellenoriz(osszeg, eviKamat, honapok);
if (extraHavi < 0) { throw new ArgumentOutOfRangeException("extraHavi", "Az extra összeg nem lehet negatív."); }
decimal havi = HaviTorleszto(osszeg, eviKamat, honapok) + extraHavi;
decimal r = eviKamat / 100m / 12m;
decimal maradek = osszeg;
List<TorlesztesiSor> terv = new List<TorlesztesiSor>();
for (int ho = 1; ho <= honapok && maradek > 0; ho++)
{
decimal kamat = Math.Round(maradek * r, 2);
decimal torleszto = havi;
if (ho == honapok || torleszto >= maradek + kamat) { torleszto = maradek + kamat; } // az utolsó sor rendezi a kerekítést
decimal toke = torleszto - kamat;
maradek = Math.Round(maradek - toke, 2);
terv.Add(new TorlesztesiSor { Honap = ho, Torleszto = torleszto, Kamat = kamat, Toke = toke, Maradek = maradek });
}
return terv;
}
public static decimal OsszesKamat(IEnumerable<TorlesztesiSor> terv) { return terv.Sum(s => s.Kamat); }
public static decimal Visszafizetendo(IEnumerable<TorlesztesiSor> terv) { return terv.Sum(s => s.Torleszto); }
}
public static class Tesztek
{
private static int hibak;
private static void Igaz(bool feltetel, string nev)
{
if (!feltetel) { hibak++; System.Diagnostics.Debug.WriteLine("HIBA " + nev); }
else { System.Diagnostics.Debug.WriteLine("ok " + nev); }
}
public static int Mindet()
{
hibak = 0;
Igaz(Math.Abs(HitelSzamolo.HaviTorleszto(1000000m, 12m, 12) - 88848.79m) <= 1m, "havi törlesztő 1M, 12%, 12 hó");
Igaz(HitelSzamolo.HaviTorleszto(120000m, 0m, 12) == 10000m, "0% kamat");
List<TorlesztesiSor> terv = HitelSzamolo.Terv(1000000m, 12m, 12);
Igaz(terv.Count == 12, "12 sor");
Igaz(terv.Last().Maradek == 0m, "a végén 0 maradék");
Igaz(terv.Sum(s => s.Toke) == 1000000m, "a tőkék összege a hitel összege");
Igaz(HitelSzamolo.Terv(1000m, 10m, 1).Count == 1, "1 hónapos hitel");
Igaz(HitelSzamolo.Terv(1000000m, 12m, 12, 50000m).Count < 12, "extra törlesztéssel rövidebb");
Igaz(HitelSzamolo.OsszesKamat(HitelSzamolo.Terv(1000000m, 12m, 12, 50000m)) < HitelSzamolo.OsszesKamat(terv), "extrával kevesebb kamat");
bool kivetel = false;
try { HitelSzamolo.Terv(-5m, 5m, 12); } catch (ArgumentOutOfRangeException) { kivetel = true; }
Igaz(kivetel, "negatív összeg kivétel");
return hibak;
}
}
// ---- Form1.cs ----
private void btnSzamol_Click(object sender, EventArgs e)
{
decimal osszeg, kamat;
if (!decimal.TryParse(txtOsszeg.Text, out osszeg) || !decimal.TryParse(txtKamat.Text, out kamat))
{
tsslUzenet.Text = "Számot adj meg az összegnél és a kamatnál.";
return;
}
int honapok = (int)nudEv.Value * 12;
try
{
List<TorlesztesiSor> terv = HitelSzamolo.Terv(osszeg, kamat, honapok);
dgvTerv.DataSource = terv;
lblHavi.Text = HitelSzamolo.HaviTorleszto(osszeg, kamat, honapok).ToString("N2") + " Ft";
lblOsszes.Text = HitelSzamolo.Visszafizetendo(terv).ToString("N0") + " Ft";
lblKamat.Text = HitelSzamolo.OsszesKamat(terv).ToString("N0") + " Ft";
tsslUzenet.Text = "";
}
catch (ArgumentOutOfRangeException ex) { tsslUzenet.Text = ex.Message; }
}
private void btnTesztek_Click(object sender, EventArgs e)
{
int hibak = Tesztek.Mindet();
tsslUzenet.Text = hibak == 0 ? "Minden teszt rendben (lásd a Debug ablakot)." : hibak + " teszt hibás (lásd a Debug ablakot).";
}Mit tegyél ide: a kész hitelkalkulátor törlesztési tervvel.Javasolt fájlnév: winforms-12-7-kesz.webp · Alt szöveg: A kész hitelkalkulátor
Három önálló gyakorlófeladat
Egyszerre csak egyet csinálj: tervezd meg, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást.
1. BMI
Emeld ki a BMI-számítást és a kategória-besorolást egy BmiSzamolo osztályba. Mit teszteled?Egy lehetséges megoldás
Szamol(kg, cm), Kategoria(bmi); teszt: tipikus (70 kg, 175 cm ≈ 22,9: normál), határértékek (18,5; 25; 30), hibás bemenet (0 cm, negatív súly).
2. Jegyátlag
Az átlagszámítás kapjon Döntsd el és dokumentáld: kivétel vagy 0 (itt a 0). Teszteld az üres, az egyelemű és a vegyes listát.IEnumerable<int> bemenetet. Mi a teendő üres listánál?Egy lehetséges megoldás
3. Régi projekt
Válassz egy korábbi projektedet, keress egy hosszú Kövesd az 5 lépést (keresd, metódus, vedd ki a felületet, új osztály, hívd); futtasd a programot előtte és utána: a viselkedésnek azonosnak kell lennie.Click kezelőt, és emeld ki a logikát.Egy lehetséges megoldás
Gyakorlótár: 6 további feladat
Három szinten gyakorolhatsz: alap, közepes és kihívás. Csak a próbálkozás után nyisd le a megoldást.
Alap szint
1. Mit csinál a Form?
Sorold fel a Form három feladatát a logika kiemelése után. Beolvassa a bemenetet (TryParse), meghívja a logikát, megjeleníti az eredményt vagy a hibát.Megoldás megnyitása
2. Tiszta?
Tiszta függvény-e a A double Terulet(double a, double b), és a double Veletlen()?Megoldás megnyitása
Terulet igen (csak a paraméterektől függ); a Veletlen nem (rejtett állapot, minden hívás más).
Közepes szint
3. Rejtett függőség
Hogyan teszteled az évszakot visszaadó Írd át Evszak() függvényt, amely DateTime.Now-t használ?Megoldás megnyitása
Evszak(DateTime datum) alakra, és add át a dátumot (a Form a DateTime.Today-t).
4. Határesetek
Milyen esetek kellenek egy Minden határ két oldala (pl. 39/40, 54/55 …), a 0 és a 100, és a tartományon kívüli érték (−1, 101: kivétel).Osztalyzat(int pont) függvény teszteléséhez (0–100 pont, 5 jegy)?Megoldás megnyitása
Kihívás szint
5. Véletlen tesztelése
Hogyan teszteled a kockadobást? A Megoldás megnyitása
Random paraméterként érkezik: a teszt new Random(42) maggal reprodukálható sorozatot kap; vagy sok dobás eloszlását ellenőrzöd (minden érték 1–6 között).
6. Mikor nem kell?
Mikor felesleges a külön logikai osztály? Triviális, egysoros, nem ismétlődő műveletnél (egy Megoldás megnyitása
Text beállítása). A szétválasztás a nem triviális szabályoknál és a gyakran változó számításnál éri meg.
Gyors önellenőrzés
Válaszolj fejben, és csak utána nyisd le a választ. Az eredménye csak a paramétereitől függ, és nincs mellékhatása. Vezérlő, Kivétellel; a Form elkapja, és megjeleníti. Az idő, véletlen, fájl: a függvény belsejéből jön; paraméterként kell átadni. Tipikus esetet, határeseteket és a hibás bemenetet. Független számítás (kézi, ismert példa), nem a program kimenete. Állapot nélküli számításnál; állapotnál példányosítandó osztály.1. Mi a tiszta függvény?
2. Mi nem lehet a logikai osztályban?
MessageBox, TryParse a TextBox-ból.3. Hogyan jelzi a logika a hibás bemenetet?
4. Mi a rejtett függőség?
5. Mit teszteljünk?
6. Hol a várt érték forrása?
7. Mikor static class?
Mit tudsz már?
- A logikát kiemeled a Formból külön osztályba (Extract Method, Extract Class).
- Tiszta függvényt írsz, a függőségeket paraméterként adod át.
- A logikát tesztekkel igazolod, felület nélkül.
- A Form feladatait pontosan körülhatárolod.
Következő lecke: Felület és adatkezelés különválasztása
A mentés és betöltés kiemelése egy tároló osztályba, felület (interface) mögé, hogy a program ne függjön a fájlformátumtól.



