Grafikus C# – Windows Forms tanfolyam › 5.8 lecke

Windows Forms · 5.8 lecke · közös kezelő

Egy eseménykezelő több vezérlőelemhez

Egy számológép tíz számjegy-gombja, egy játéktábla kilenc mezője, egy űrlap öt szövegmezője: ha mindegyiknek külön eseménykezelőt írnál, a kódod tele lenne ismétlődéssel. A megoldás a közös eseménykezelő: egyetlen metódus kezeli az összes hasonló vezérlő eseményét, és a sender alapján tudja, melyik váltotta ki. Ebben a leckében megtanulod, hogyan rendeld hozzá a kezelőt sok vezérlőhöz (a tervezőben és kódból), hogyan azonosítsd a küldőt (Name, Tag, index), hogyan használd a switch utasítást, és hogyan generálj kódból egész vezérlőtáblát egyetlen kezelővel.

  • kb. 110 perc
  • közepes szint
  • közös eseménykezelő
Diáklány C# programot készít laptopon

A lecke végére

  • egyetlen kezelőt rendelsz sok vezérlőhöz a tervezőben (több vezérlő kijelölésével) és kódból is;
  • a küldőt többféleképp azonosítod, és tudod, melyik mód mikor megbízható;
  • a Tag tulajdonságban adatot tárolsz a vezérlőhöz;
  • használod a switch utasítást;
  • rekurzívan bejárod a beágyazott vezérlőket (GroupBox, Panel);
  • kódból generálsz vezérlőtáblát egy közös kezelővel, és elkerülöd a ciklusváltozó csapdáját.

Mielőtt elkezded

Ehhez a leckéhez az 5.1–5.3 lecke (események, sender, egéresemények), az 5.7 lecke (feliratkozás, leiratkozás) és az 1.4 lecke (a közös kezelő első bemutatása) szükséges. Új projektben próbáld ki a példákat.

5.7 Feliratkozás és leiratkozás →

A közös kezelő hozzárendelése

Ugyanazt a metódust több vezérlőhöz háromféleképp kötheted:

1. A tervezőben, egyenként

Jelölj ki egy vezérlőt, a Properties ablakban nyisd meg az Events nézetet (villám ikon), és a Click sor melletti legördülő listából válaszd ki a már meglévő metódust (ne kattints duplán, mert az újat hozna létre).

2. A tervezőben, egyszerre

Tartsd lenyomva a Ctrl billentyűt, és kattints sorra az összes vezérlőre (vagy húzz kijelölő keretet körülöttük). Az Events nézetben a Click sorban megadott metódus mindegyikhez hozzárendelődik. Tíz gombnál ez egyetlen lépés.

3. Kódból

A += operátorral, egyenként vagy ciklusban (lásd lent). Kódból generált vezérlőknél ez az egyetlen mód.

Form1.csC#
// egyetlen metódus a tíz számgombnak (btn0 ... btn9)
private void SzamGomb_Click(object sender, EventArgs e)
{
Button gomb = (Button)sender;
txtKijelzo.Text = txtKijelzo.Text + gomb.Text;
}

// ugyanez kódból, egy tömbbel és ciklussal
private void Form1_Load(object sender, EventArgs e)
{
Button[] szamGombok = { btn0, btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9 };

foreach (Button gomb in szamGombok)
{
gomb.Click += SzamGomb_Click;
}
}
Ugyanaz a metódusA tervezőben mindegyik vezérlő Click sora ugyanarra a névre mutat: a SzamGomb_Click névre. A név nem tartalmazza egyik vezérlő nevét sem (nincs btn5_Click), mert mindenkié.
Csak egy módon!Ha a tervezőben és a kódban is hozzárendeled, a kezelő kétszer fut (5.1, 5.7 lecke). Válassz egy helyet.
A kezelő paramétereiAz eseményeknek azonos típusúnak kell lenniük: egy Button.Click és egy Label.Click is EventHandler típusú, ezért egy metódus kezelheti mindkettőt. A Button.Click és a TextBox.KeyPress viszont más típusú, azokat nem lehet egy metódusba tenni.
ÁtnevezésHa a kezelő nevét egy konkrét vezérlőtől kapta (btn5_Click), a közös használatra nevezd át (Rename parancs, 5.1 lecke), hogy a név ne félrevezessen.

Jósolj!

A kilenc gombból nyolchoz hozzárendeled a Gomb_Click kezelőt a tervezőben, a kilencediket (btn9) elfelejted. A Form1_Load eseményben pedig mind a kilencre kódból is feliratkozol (+=). Hányszor fut le a kezelő az egyes gombok megnyomására?

Jóslat ellenőrzése

A nyolc gombnál kétszer (a tervező és a kód is feliratkoztatta), a btn9-nél egyszer (csak a kód). A hiba azért nehezen vehető észre, mert minden gomb „működik”, csak néhány kétszer. A két hozzárendelési mód együttes használata kerülendő: vagy a tervezőben, vagy kódból iratkozz fel.

A küldő azonosítása: Name, Tag vagy index?

A közös kezelőnek tudnia kell, melyik vezérlő váltotta ki az eseményt. Négy mód van, mindegyiknek más az erőssége:

MódPéldaElőnyeHátránya
Név (Name)gomb.Name == "btnPlusz"Egyszerű, a tervezőben már megvan.Átnevezéskor a szövegben lévő név nem frissül, és elírást a fordító nem vesz észre.
Felirat (Text)gomb.Text == "+"Rövid, olvasható.A felirat változhat (nyelv, átírás), és nem azonosító. Számjegyeknél jó (a felirat az érték).
Tag(string)gomb.Tag == "szorzas"Megbízható: független a névtől és a felirattól, tetszőleges adatot hordozhat.Be kell állítani (tervezőben vagy kódból), és null lehet.
Index / hivatkozásArray.IndexOf(gombok, sender), sender == btnATömbből a sorszám közvetlenül adódik; a hivatkozás-összehasonlítás pontos.Csak akkor, ha a vezérlők tömbben vannak (vagy ismert nevű változók).

A Tag a leghasznosabb: minden vezérlőnek van egy Tag tulajdonsága (típusa object), amelybe bármit tehetsz: szöveget, számot, színt, egy Point értéket, akár egy saját osztály példányát. A tervezőben csak szöveget adhatsz meg, kódból bármit:

Form1.csC#
private void Form1_Load(object sender, EventArgs e)
{
// kódból bármit eltárolhatunk a Tag-ben
btnPiros.Tag = Color.Firebrick;
btnKek.Tag = Color.RoyalBlue;
btnZold.Tag = Color.SeaGreen;
}

private void SzinGomb_Click(object sender, EventArgs e)
{
Button gomb = (Button)sender;

if (gomb.Tag is Color szin)
{
pnlElonezet.BackColor = szin;
}
}
gomb.Tag is Color szinA Tag object típusú, ezért vissza kell alakítani. Az is minta ellenőriz és átalakít egyszerre, és null esetén egyszerűen nem teljesül (nincs NullReferenceException).
Egy kezelő, sok adatA gombok csak a Tag tartalmában térnek el: a kezelő kódja ugyanaz, az adat vezérlőnként más. Ez a „adatvezérelt” megközelítés lényege.
A tervezőben megadott TagA Properties ablak Tag sorába csak szöveg írható (szorzas, 5). Számként használni így a int.Parse((string)gomb.Tag) szükséges.
Mire ne használdNe a Tag-be tegyél olyan adatot, amelynek saját tulajdonsága van (a felirat a Text, a név a Name). A Tag a „plusz” adatnak való.

Jósolj!

A btnPiros Tag tulajdonságát elfelejted beállítani, és a SzinGomb_Click kezelőben Color szin = (Color)gomb.Tag; áll. Mi történik a gomb megnyomására?

Jóslat ellenőrzése

A Tag értéke null, és a (Color)null átalakítás NullReferenceException (kicsomagolási hiba) kivételt dob. Az is Color szin minta ezt megelőzi: null esetén a feltétel hamis, és a kód egyszerűen nem fut. Ezért a Tag kiolvasásakor mindig az is vagy az as a biztonságos.

A switch utasítás: sok eset, egy döntés

Ha a kezelőnek tucatnyi esetet kell megkülönböztetnie (például a számológép műveletei), az if–else if lánc hosszú és nehezen olvasható. A switch utasítás egyetlen értéket hasonlít össze több lehetséges esettel (case):

Form1.csC#
private void MuveletGomb_Click(object sender, EventArgs e)
{
Button gomb = (Button)sender;
double a = (double)nudA.Value;
double b = (double)nudB.Value;
double eredmeny = 0;

switch (gomb.Tag.ToString()) // a Tag: osszead, kivon, szoroz, oszt
{
case "osszead":
eredmeny = a + b;
break;

case "kivon":
eredmeny = a - b;
break;

case "szoroz":
eredmeny = a * b;
break;

case "oszt":
if (b == 0)
{
lblEredmeny.Text = "Nullával nem lehet osztani";
return;
}

eredmeny = a / b;
break;

default:
lblEredmeny.Text = "Ismeretlen művelet";
return;
}

lblEredmeny.Text = eredmeny.ToString();
}
switch (érték)Az értéket (itt szöveget) egyszer kiértékeli, és az első egyező case ágat hajtja végre.
breakMinden ágat break (vagy return) zár le. Enélkül a C# fordítási hibát jelez (nem „esik át” a következő ágra, mint más nyelvekben).
defaultAz az ág, amely akkor fut, ha egyik case sem egyezik. Érdemes mindig megadni: a váratlan értéket így kezeled.
Több érték, egy ágEgymás alá írt case címkék ugyanazt az ágat adják: case "a": case "b": ... break;

Jósolj!

A switch egyik ágából (case "kivon":) kitörlöd a break; sort. Lefordul-e a program?

Jóslat ellenőrzése

Nem fordul le. A C# nem engedi, hogy egy ág (amely tartalmaz utasítást) a break, return vagy más kilépés nélkül véget érjen: CS0163: Az irányítás nem eshet át az egyik case címkéről a másikra. Így a más nyelvekben gyakori „átesés” hiba itt nem fordulhat elő.

Vezérlők bejárása: ciklus és rekurzió

Gyakori feladat, hogy minden adott típusú vezérlőhöz ugyanazt a kezelőt kell kötni (például az összes szövegmező kiemelése, ha fókuszba kerül). A this.Controls gyűjtemény viszont csak az ablak közvetlen gyermekeit tartalmazza: a GroupBox vagy Panel belsejében lévő vezérlők nem szerepelnek benne. Ezeket rekurzióval (a metódus önmagát hívja) lehet bejárni:

Form1.csC#
// minden TextBox kiemelése fókuszba kerüléskor, a beágyazottaké is
private void Form1_Load(object sender, EventArgs e)
{
Feliratkozas(this);
}

private void Feliratkozas(Control szulo)
{
foreach (Control gyermek in szulo.Controls)
{
if (gyermek is TextBox mezo)
{
mezo.Enter += Mezo_Enter;
mezo.Leave += Mezo_Leave;
}

if (gyermek.Controls.Count > 0)
{
Feliratkozas(gyermek); // rekurzió: a gyermek gyermekei
}
}
}

private void Mezo_Enter(object sender, EventArgs e)
{
((TextBox)sender).BackColor = Color.LightYellow;
}

private void Mezo_Leave(object sender, EventArgs e)
{
((TextBox)sender).BackColor = SystemColors.Window;
}
RekurzióA Feliratkozas a vezérlő gyermekeit járja be, és ha egy gyermeknek is vannak gyermekei, önmagát hívja rájuk. Így a tetszőleges mélységű beágyazás is bejárható.
gyermek is TextBox mezoTípusellenőrzés és átalakítás egyben (5.2 lecke). Csak a szövegmezőkhöz köt kezelőt.
Controls.Count > 0Csak akkor lépünk a gyermekbe, ha van mit bejárni. (Nélküle is működne, de így kevesebb felesleges hívás van.)
Enter és LeaveNem azonos a Click eseménnyel: akkor futnak, amikor a vezérlő fókuszt kap, illetve elveszíti, billentyűzettel is.

Jósolj!

Az ablakon három TextBox van, kettő egy GroupBox-ban. A Form1_Load-ban ezt írod: foreach (Control c in this.Controls) { if (c is TextBox) { ... } } (rekurzió nélkül). Hány szövegmezőre köt kezelőt?

Jóslat ellenőrzése

Csak egyre: az ablak közvetlen gyermeke. A GroupBox belsejében lévő két szövegmező a GroupBox.Controls gyűjteményben van, és az ablak Controls gyűjteménye nem tartalmazza őket. A GroupBox maga egy Control, így a ciklus látja, de a tartalmába csak a rekurzió lép be.

Kódból generált tábla egyetlen kezelővel

Ha a vezérlők száma sok vagy a kiosztásuk szabályos (játéktábla, billentyűzet, naptár), kódból hozzuk létre őket egy ciklusban, és mindegyikhez ugyanazt a kezelőt kötjük. A hely adatát a Tag tárolja, így a kezelő tudja, melyik mezőről van szó:

Form1.cs (pnlTabla: Panel)C#
private Button[,] mezok = new Button[3, 3];

private void Form1_Load(object sender, EventArgs e)
{
for (int sor = 0; sor < 3; sor++)
{
for (int oszlop = 0; oszlop < 3; oszlop++)
{
Button mezo = new Button();
mezo.Size = new Size(80, 80);
mezo.Location = new Point(oszlop * 84, sor * 84);
mezo.Font = new Font("Segoe UI", 28, FontStyle.Bold);
mezo.Tag = new Point(oszlop, sor); // a mező helye a táblán
mezo.Click += Mezo_Click; // ugyanaz a kezelő mind a kilencnek

mezok[sor, oszlop] = mezo;
pnlTabla.Controls.Add(mezo);
}
}
}

private void Mezo_Click(object sender, EventArgs e)
{
Button mezo = (Button)sender;

if (mezo.Tag is Point hely)
{
lblInfo.Text = $"Sor: {hely.Y}, oszlop: {hely.X}";
}
}

A ciklusváltozó és a lambda

Az 5.1 lecke Gyakorlótárában láttad: ha lambdával kötöd a kezelőt, és a lambda a ciklusváltozót használja, minden vezérlő ugyanazt az értéket látja. A közös kezelő és a Tag ezt a problémát elkerüli (nincs lambda). Ha mégis lambdát használsz, helyi másolat kell:

Form1.csC#
for (int i = 0; i < 5; i++)
{
Button gomb = new Button();

// HIBÁS: gomb.Click += (s, ev) => MessageBox.Show(i.ToString()); // mindegyik 5-öt mutat!

int szam = i; // helyi másolat minden körben
gomb.Click += (s, ev) => MessageBox.Show(szam.ToString());

pnlGombok.Controls.Add(gomb);
}

Jósolj!

A Mezo_Click kezelőben a Point hely = (Point)mezo.Tag; sor áll, de a ciklusban elfelejted beállítani a Tag tulajdonságot. Mi történik az első kattintásra?

Jóslat ellenőrzése

A Tag null, és a (Point)null átalakítás NullReferenceException kivételt dob. Az is Point hely minta ilyenkor hamis, és a kód nem fut le: nincs hiba, de nem is történik semmi, ami hibakeresésnél szintén félrevezető lehet. Ezért a Tag beállítása a létrehozás ciklusban kötelező lépés.

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. NullReferenceException a Tag használatakor

string muvelet = gomb.Tag.ToString();   // a Tag null
Hiba és javítás

A Tag nincs beállítva, így null. Állítsd be a tervezőben vagy kódból, és olvasd ki biztonságosan: string muvelet = gomb.Tag as string; majd if (muvelet != null), vagy gomb.Tag?.ToString().

2. InvalidCastException a kezelőben

A közös kezelőt egy Button és egy Label is használja, és a (Button)sender hibát dob a címkénél.

Ok és javítás

A sender nem minden esetben Button. Használj is mintát (if (sender is Button gomb)) vagy a közös Control típust (5.2 lecke).

3. A switch nem talál egyezést átnevezés után

switch (gomb.Name)
{
case "btnOsszead": /* ... */ break; // a gombot átnevezted btnPlusz-ra
}
Hiba és javítás

A szövegben szereplő név nem frissül az átnevezéskor, és a fordító nem jelez hibát: a default ág fut. Használj Tag értéket (amely független a névtől), vagy a vezérlőt tömbben tárold, és az indexet vizsgáld.

4. Minden gomb ugyanazt az értéket adja

for (int i = 0; i < 5; i++)
{
Button g = new Button();
g.Click += (s, e) => MessageBox.Show(i.ToString()); // mind 5-öt mutat
}
Hiba és javítás

A lambda a for ciklus egyetlen i változóját látja, amelynek értéke a ciklus végén 5. Készíts helyi másolatot a ciklustörzsben (int szam = i;), vagy használj közös kezelőt és Tag értéket.

5. A kezelő nem kötődik a panelen lévő mezőkhöz

Az összes szövegmezőre ráírtad a ciklust, de a GroupBox belsejében lévők kimaradnak.

Ok és javítás

A this.Controls csak a közvetlen gyermekeket tartalmazza. Járd be rekurzívan a beágyazott vezérlőket (Feliratkozas(Control szulo)), ahogy a leckében látod.

6. A kezelő kétszer fut a számgombokra

A számgombok eseménykezelője minden megnyomásra kétszer ad hozzá számjegyet.

Ok és javítás

A gombokat a tervezőben is és a Form1_Load ciklusban is feliratkoztattad. Válassz egy módot, és töröld a másikat (a Designer.cs fájlban a Click += sorokat az Events nézetben törölheted a metódus nevének kitörlésével).

Mini projekt: amőba

Készíts kétszemélyes amőba (tic-tac-toe) játékot, amelynek mezői kódból jönnek létre egyetlen közös kezeléssel:

  1. Az ablakon: egy pnlTabla panel (260 × 260), lblSoron (ki következik), lblEredmeny (pontállás: X: 2 | O: 1 | Döntetlen: 0), btnUj (új játék).
  2. A Form1_Load egy 3 × 3-as tömbbe (Button[,]) hozza létre a kilenc mezőt, mindegyik Tag tulajdonságába a helyét (Point) teszi, és mindegyikhez a Mezo_Click közös kezelőt köti.
  3. A Mezo_Click az üres mezőre X vagy O jelet tesz (felváltva), a foglalt mezőre kattintást figyelmen kívül hagyja.
  4. Minden lépés után ellenőrzi a nyerést (sorok, oszlopok, átlók) és a döntetlent (9 lépés). Nyerésnél kiemeli a nyerő mezőket, letiltja a táblát, és növeli a pontszámot.
  5. Az Új játék gomb törli a táblát (a pontszám marad), és az X kezd.
  6. Teszteld: minden nyerő vonal, döntetlen, foglalt mezőre kattintás, új játék nyerés után, a pontállás.
Megoldás megnyitása (próbáld ki előbb önállóan!)

A nyerés ellenőrzése egy Egyezik segédmetódusra épül, amely három mezőről megállapítja, hogy azonos, nem üres jelet hordoznak-e. A játék állapotát három mező őrzi: a soron következő jel, a lépések száma és a vége-jelző.

Form1.csC#
private Button[,] mezok = new Button[3, 3];
private bool xKovetkezik = true;
private int lepesek = 0;
private bool vege = false;
private int xPont = 0;
private int oPont = 0;
private int dontetlen = 0;

private void Form1_Load(object sender, EventArgs e)
{
for (int sor = 0; sor < 3; sor++)
{
for (int oszlop = 0; oszlop < 3; oszlop++)
{
Button mezo = new Button();
mezo.Size = new Size(80, 80);
mezo.Location = new Point(oszlop * 84, sor * 84);
mezo.Font = new Font("Segoe UI", 28, FontStyle.Bold);
mezo.Tag = new Point(oszlop, sor);
mezo.Click += Mezo_Click;

mezok[sor, oszlop] = mezo;
pnlTabla.Controls.Add(mezo);
}
}

Frissit();
}

private void Mezo_Click(object sender, EventArgs e)
{
Button mezo = (Button)sender;

if (vege || mezo.Text != "")
{
return; // vége a játéknak, vagy foglalt mező
}

mezo.Text = xKovetkezik ? "X" : "O";
mezo.ForeColor = xKovetkezik ? Color.RoyalBlue : Color.Firebrick;
lepesek = lepesek + 1;

if (Nyert())
{
vege = true;

if (xKovetkezik)
{
xPont = xPont + 1;
}
else
{
oPont = oPont + 1;
}

lblSoron.Text = (xKovetkezik ? "X" : "O") + " nyert!";
}
else if (lepesek == 9)
{
vege = true;
dontetlen = dontetlen + 1;
lblSoron.Text = "Döntetlen.";
}
else
{
xKovetkezik = !xKovetkezik;
lblSoron.Text = "Soron: " + (xKovetkezik ? "X" : "O");
}

Frissit();
}

private bool Egyezik(Button a, Button b, Button c)
{
if (a.Text != "" && a.Text == b.Text && b.Text == c.Text)
{
a.BackColor = Color.LightGreen;
b.BackColor = Color.LightGreen;
c.BackColor = Color.LightGreen;
return true;
}

return false;
}

private bool Nyert()
{
for (int i = 0; i < 3; i++)
{
if (Egyezik(mezok[i, 0], mezok[i, 1], mezok[i, 2]))
{
return true;
}

if (Egyezik(mezok[0, i], mezok[1, i], mezok[2, i]))
{
return true;
}
}

return Egyezik(mezok[0, 0], mezok[1, 1], mezok[2, 2]) || Egyezik(mezok[0, 2], mezok[1, 1], mezok[2, 0]);
}

private void Frissit()
{
lblEredmeny.Text = $"X: {xPont} | O: {oPont} | Döntetlen: {dontetlen}";
}

private void btnUj_Click(object sender, EventArgs e)
{
foreach (Button mezo in mezok)
{
mezo.Text = "";
mezo.BackColor = SystemColors.Control;
}

xKovetkezik = true;
lepesek = 0;
vege = false;
lblSoron.Text = "Soron: X";
}
📷 Képernyőkép helye

Mit tegyél ide: egy befejezett játék: a három nyerő mező zölden, a „X nyert!” felirat és a pontállás.Javasolt fájlnév: winforms-5-8-kesz.webp · Alt szöveg: Az amőba játék egy nyerő állással

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. Színpaletta egy kezelővel

Hat színes Panel (vagy gomb) palettát alkot. Egy közös kezelő a kattintott szín értékét a pnlKijelolt panel háttérszínére állítja. Hol tárolod a színt, és hogyan állítod be?

Egy lehetséges megoldás

A színt a panel saját BackColor tulajdonsága tárolja, ezért a kezelőnek nem is kell külön adat. A közös kezelő a sender hátterét másolja. Ha a paletta elemei színtelenek lennének (például képek), a Tag tárolná a színt.

Form1.csC#
private void Paletta_Click(object sender, EventArgs e)
{
Control elem = (Control)sender;
pnlKijelolt.BackColor = elem.BackColor;
lblSzin.Text = "Kiválasztott szín: " + elem.BackColor.Name;
}

2. Hőmérséklet-átváltó választógombokkal

Három RadioButton (Celsius, Fahrenheit, Kelvin) a kimeneti mértékegységet választja. A bemenet mindig Celsius (nudCelsius). Egyetlen CheckedChanged kezelő és a Tag használatával számold át az értéket, és az lblEredmeny-ben jelenítsd meg.

Egy lehetséges megoldás

A RadioButton.CheckedChanged kétszer fut (az új és a régi gombra), ezért csak a bejelölt gombbal foglalkozunk (rb.Checked). A Tag a mértékegység kódját tartalmazza (C, F, K). Az érték változásakor is újra kell számolni, ezért a számolás külön metódus.

Form1.csC#
private void Szamol()
{
double c = (double)nudCelsius.Value;
string egyseg = "C";

foreach (Control vezerlo in grpEgyseg.Controls)
{
RadioButton rb = vezerlo as RadioButton;

if (rb != null && rb.Checked)
{
egyseg = rb.Tag.ToString();
}
}

switch (egyseg)
{
case "F":
lblEredmeny.Text = (c * 9 / 5 + 32).ToString("0.##") + " °F";
break;

case "K":
lblEredmeny.Text = (c + 273.15).ToString("0.##") + " K";
break;

default:
lblEredmeny.Text = c.ToString("0.##") + " °C";
break;
}
}

private void Egyseg_CheckedChanged(object sender, EventArgs e)
{
RadioButton rb = (RadioButton)sender;

if (rb.Checked)
{
Szamol();
}
}

private void nudCelsius_ValueChanged(object sender, EventArgs e)
{
Szamol();
}

3. Mentés gomb engedélyezése több mezőből

Az űrlapon öt szövegmező van (két GroupBox-ban elosztva). A Mentés gomb csak akkor aktív, ha mindegyik nem üres. Egy közös TextChanged kezelőt kell használnod, a beágyazott mezőket is bejárva.

Egy lehetséges megoldás

A rekurzív bejárás a mezőkhöz köti a kezelőt (egyszer, a Load-ban). Az ellenőrzés egy másik rekurzív metódus (VanUres), amely igazat ad, ha bármelyik szövegmező üres. A gomb állapotát a közös kezelő frissíti.

Form1.csC#
private void Kot(Control szulo)
{
foreach (Control gyermek in szulo.Controls)
{
if (gyermek is TextBox mezo)
{
mezo.TextChanged += Mezo_TextChanged;
}

Kot(gyermek);
}
}

private bool VanUres(Control szulo)
{
foreach (Control gyermek in szulo.Controls)
{
if (gyermek is TextBox mezo && mezo.Text.Trim().Length == 0)
{
return true;
}

if (VanUres(gyermek))
{
return true;
}
}

return false;
}

private void Mezo_TextChanged(object sender, EventArgs e)
{
btnMent.Enabled = !VanUres(this);
}

private void Form1_Load(object sender, EventArgs e)
{
Kot(this);
btnMent.Enabled = !VanUres(this);
}

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. A Tag használata szövegként

Három gombot (Kicsi, Közepes, Nagy) egy közös kezelő köt. A tervezőben a Tag értékük 10, 16, 24. A kezelő a lblPelda betűméretét ennek megfelelően állítsa. Hogyan alakítod számmá a Tag értékét?

Megoldás megnyitása

A tervezőben megadott Tag szöveg (string), ezért int.Parse (vagy TryParse) kell. A TryParse megvédi a programot egy elgépelt értéktől.

Form1.csC#
private void Meret_Click(object sender, EventArgs e)
{
Button gomb = (Button)sender;
int meret;

if (gomb.Tag != null && int.TryParse(gomb.Tag.ToString(), out meret))
{
lblPelda.Font = new Font(lblPelda.Font.FontFamily, meret);
}
}

2. Tervezői kijelölés

Magyarázd el lépésről lépésre, hogyan rendelsz hozzá egy meglévő Gomb_Click metódust öt gombhoz a tervezőben anélkül, hogy öt külön alkalommal állítanád.

Megoldás megnyitása

(1) Kattints az első gombra, tartsd lenyomva a Ctrl billentyűt, és kattints a másik négyre is (vagy húzz kijelölő keretet körülöttük). (2) A Properties ablakban válts az Events (villám) nézetre. (3) A Click sor melletti legördülő listából válaszd ki a Gomb_Click metódust (a meglévő metódust válaszd ki, ne duplán kattints). (4) A hozzárendelés mind az öt gombra érvényes lesz.

Közepes szint

3. Az összes Label átszínezése

Egy gomb az ablak összes Label vezérlőjének (a GroupBox-ban lévőknek is) a betűszínét pirosra állítja. Hogyan járod be?

Megoldás megnyitása

Rekurzív metódus, amely a gyermekeket bejárja, a Label típusúaknál átállítja a színt, és minden gyermekre meghívja önmagát.

Form1.csC#
private void Szinez(Control szulo, Color szin)
{
foreach (Control gyermek in szulo.Controls)
{
if (gyermek is Label cimke)
{
cimke.ForeColor = szin;
}

Szinez(gyermek, szin);
}
}

private void btnSzinez_Click(object sender, EventArgs e)
{
Szinez(this, Color.Firebrick);
}

4. Számológép műveletei switch-csel

Négy műveletgomb (+, -, ×, ÷) közös kezelője a gombok felirata alapján számol két NumericUpDown értékével. Mi a különbség a felirat és a Tag használata között?

Megoldás megnyitása

A felirat (Text) rövid és olvasható, de ha a felirat megváltozik (például × helyett *), a switch elromlik. A Tag független a megjelenéstől. Kis programban a felirat is elfogadható, nagyobb programban a Tag a biztosabb.

Form1.csC#
private void Muvelet_Click(object sender, EventArgs e)
{
double a = (double)nudA.Value;
double b = (double)nudB.Value;
double eredmeny;

switch (((Button)sender).Text)
{
case "+": eredmeny = a + b; break;
case "-": eredmeny = a - b; break;
case "×": eredmeny = a * b; break;
case "÷":
if (b == 0) { lblEredmeny.Text = "Nullával nem lehet osztani"; return; }
eredmeny = a / b;
break;
default: return;
}

lblEredmeny.Text = eredmeny.ToString();
}

Kihívás szint

5. Menüpontok közös kezelővel

A Nézet → Betűméret menü három pontja (miKicsi, miKozepes, miNagy) közös kezelővel állítja a txtSzoveg betűméretét. Miért nem alakíthatod a sender paramétert Control típusra?

Megoldás megnyitása

A menüpont (ToolStripMenuItem) nem Control (5.2 lecke). A közös őse a ToolStripItem, amelynek van Tag tulajdonsága, és a sender erre (vagy ToolStripMenuItem-re) alakítható.

Form1.csC#
private void Betumeret_Click(object sender, EventArgs e)
{
ToolStripMenuItem pont = (ToolStripMenuItem)sender;
int meret;

if (pont.Tag != null && int.TryParse(pont.Tag.ToString(), out meret))
{
txtSzoveg.Font = new Font(txtSzoveg.Font.FontFamily, meret);

// a jelölés: csak az aktuális pont kapja a pipát
miKicsi.Checked = pont == miKicsi;
miKozepes.Checked = pont == miKozepes;
miNagy.Checked = pont == miNagy;
}
}

6. Billentyűzet kódból

Hozz létre kódból egy telefonos billentyűzetet (4 sor × 3 oszlop: 1 … 9, *, 0, #) egyetlen közös kezelővel, amely a nyomott gombot egy txtSzam mezőhöz fűzi. Hol tárolod a gombok feliratát?

Megoldás megnyitása

A feliratokat egy szöveg tömb tartalmazza, és a gomb Text tulajdonságába kerülnek. A közös kezelő a Text értéket fűzi hozzá, így nincs szükség külön Tag-re. A sor és oszlop az index osztásából és maradékából adódik.

Form1.csC#
private void Form1_Load(object sender, EventArgs e)
{
string[] feliratok = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#" };

for (int i = 0; i < feliratok.Length; i++)
{
Button gomb = new Button();
gomb.Text = feliratok[i];
gomb.Size = new Size(60, 50);
gomb.Location = new Point((i % 3) * 64, (i / 3) * 54); // oszlop: maradék, sor: egész osztás
gomb.Click += Billentyu_Click;

pnlBillentyuk.Controls.Add(gomb);
}
}

private void Billentyu_Click(object sender, EventArgs e)
{
txtSzam.Text = txtSzam.Text + ((Button)sender).Text;
}

Gyors önellenőrzés

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

1. Hogyan rendelsz egy kezelőt több vezérlőhöz a tervezőben?

Kijelölöd a vezérlőket (Ctrl + kattintás), és az Events nézetben a meglévő metódust választod ki az eseményhez.

2. Miért ne használd a tervezőt és a kódot egyszerre ugyanarra az eseményre?

Mert a kezelő kétszer iratkozik fel, és kétszer fut le.

3. Mikor a legmegbízhatóbb a Tag a Name vagy a Text helyett?

Ha az azonosítás nem függhet a névtől vagy a felirattól (átnevezés, nyelv). A Tag bármilyen adatot hordozhat.

4. Miért kell a switch ágait break vagy return zárja?

A C# nem engedi az átesést az egyik ágból a másikba (CS0163), így a hiba nem fordulhat elő.

5. Miért nem elég a this.Controls ciklus a beágyazott vezérlőkhöz?

Csak a közvetlen gyermekeket tartalmazza. A GroupBox vagy Panel belsejét rekurzióval kell bejárni.

6. Mi a ciklusváltozó és a lambda csapdája?

A lambda egyetlen ciklusváltozót lát, amelynek végső értéke minden vezérlőnél azonos. Helyi másolat (int szam = i;) vagy közös kezelő + Tag oldja meg.

7. Mit tárolhatsz a Tag-ben?

Bármilyen objektumot: szöveget, számot, színt, Point értéket, saját osztály példányát. A tervezőben csak szöveget.

Mit tudsz már?

  • Egyetlen kezelőt rendelsz sok vezérlőhöz (tervezőben, kódból), és a küldőt azonosítod.
  • A Tag tulajdonságban adatot tárolsz, és biztonságosan olvasod ki.
  • Használod a switch utasítást és a rekurzív bejárást.
  • Kódból generálsz vezérlőtáblát közös kezelővel, és kerülöd a ciklusváltozó csapdáját.
  • Elkészítettél egy kész játékot (amőba) a közös kezelő mintájával.

Következő lecke: mini projekt – billentyűzettel vezérelt program

Az 5. fejezet záró projektjében az eseményekről tanultak mind egy programban találkoznak: billentyűzet, egér, változás-események és életciklus.