Grafikus C# – Windows Forms tanfolyam › 8.2 lecke

Windows Forms · 8.2 lecke · listaelemek

Items, SelectedItem és SelectedIndex

A listavezérlők szíve az Items gyűjtemény és a kijelölés (SelectedItem, SelectedIndex). Ebben a leckében pontosan megtanulod, mit tehetsz az elemekkel (hozzáadás, beszúrás, csere, törlés, mozgatás), hogyan tárolsz a listában saját objektumokat (nem csak szöveget), hogyan olvasod vissza őket típushelyesen, és hogyan követed a kijelölést úgy, hogy a SelectedIndexChanged esemény ne okozzon váratlan hibát.

  • kb. 90 perc
  • közepes szint
  • Items és kijelölés
Diák C# programozást gyakorol egy tanulósarokban

A lecke végére

  • az Items gyűjtemény összes fontos műveletét használod;
  • saját objektumokat teszel a listába, és típushelyesen olvasod vissza (as, is);
  • követed a kijelölést, és a törlés vagy csere után helyreállítod;
  • elemet mozgatsz fel és le (csere), és a kijelölés vele marad;
  • a SelectedIndexChanged eseményt védetten kezeled (-1, jelző);
  • felismered a gyakori hibákat (null, elcsúszó kijelölés, bejárás közbeni módosítás).

Mielőtt elkezded

Ehhez a leckéhez a 8.1 lecke (listavezérlők), a 7.2 lecke (ToString, Items[i] = elem), az 5.2 lecke (as, is) és az 5.5 lecke (jelző) szükséges.

8.1 Listavezérlők →

Az Items gyűjtemény

TagMit tesz?
Add(elem)Az elemet a lista végére teszi, a sorszámát adja vissza.
AddRange(tömb)Egyszerre több elem.
Insert(index, elem)Adott helyre szúr (rendezetlen listán).
Remove(elem), RemoveAt(index)Törlés elem vagy sorszám alapján.
Clear()Az összes elem törlése (a kijelölés -1 lesz).
Contains(elem), IndexOf(elem)Szerepel-e az elem, hanyadik.
Count, Items[i]Elemszám, elem sorszám szerint (felülírható is).
Form1.csC#
int hely = lst.Items.Add("Alma");            // hely: az új elem sorszáma
lst.SelectedIndex = hely; // az újonnan hozzáadott elem kijelölve

lst.Items.Insert(0, "Első"); // az elejére (nem rendezett listán)
lst.Items[2] = "Felülírt"; // csere

if (lst.Items.Contains("Alma")) { lst.Items.Remove("Alma"); }

lst.Items.RemoveAt(0);

for (int i = lst.Items.Count - 1; i >= 0; i--) // bejárás törléshez: visszafelé
{
if (lst.Items[i].ToString().StartsWith("X"))
{
lst.Items.RemoveAt(i);
}
}
Az Add visszatérési értékeAz Add a hozzáadott elem sorszámát adja: lst.SelectedIndex = lst.Items.Add(x); egy sorban hozzáad és kijelöl.
Remove és egyenlőségA Remove(elem) és a Contains(elem) az elem Equals metódusát használja: saját objektumnál ez alapból a hivatkozás egyezése (ugyanaz a példány), nem a tartalomé.
Items[i] = elemCseréhez és frissítéshez (7.2 lecke). Csere után a kijelölés elveszhet, ezért ellenőrizd, és szükség esetén állítsd vissza.
Törlés visszafeléHa ciklusban törölsz, visszafelé haladj: így az előre lévő sorszámok nem csúsznak el.

Jósolj!

Egy ötelemű lista elemeit előrefelé haladó for ciklussal törölöd (for (int i = 0; i < Count; i++) if (feltétel) RemoveAt(i);), és két szomszédos elem is megfelel a feltételnek. Mi történik?

Jóslat ellenőrzése

Az egyik megfelelő elem megmarad: a törlés után a következő elem a törölt helyére csúszik, a ciklus pedig már a következő sorszámra lép, így kihagyja. A visszafelé haladó ciklus ezt kiküszöböli.

Saját objektumok a listában

Az Items elemei object típusúak: bármit tárolhat, a megjelenített szöveget a ToString() adja. Szöveg helyett így valódi adatobjektumokat teszel a listába, és a kijelölésnél az egész objektumot megkapod vissza:

Form1.csC#
public class Szam
{
public string Cim = "";
public int Masodperc = 0;

public override string ToString()
{
return Cim + " (" + Masodperc / 60 + ":" + (Masodperc % 60).ToString("00") + ")";
}
}

lst.Items.Add(new Szam { Cim = "Esti dal", Masodperc = 215 });

// visszaolvasás: típuskényszerítéssel; az 'as' null-t ad, ha nincs kijelölés
Szam kijelolt = lst.SelectedItem as Szam;

if (kijelolt != null)
{
txtCim.Text = kijelolt.Cim;
nudHossz.Value = kijelolt.Masodperc;
}

// az összes elem feldolgozása típushelyesen
int ossz = 0;

foreach (object o in lst.Items)
{
ossz += ((Szam)o).Masodperc;
}
as és nullA lst.SelectedItem as Szam null értéket ad, ha nincs kijelölés (vagy az elem más típusú): a null ellenőrzés védi a kódot.
Hivatkozás a listábanA listában az objektum hivatkozása van: az as Szam ugyanazt a példányt adja, a módosítása közvetlenül a listaelemet módosítja. A lista szövege viszont nem frissül (Items[i] = elem kell).
ToStringA megjelenített szöveget a ToString() adja. A DisplayMember (8.5 lecke) ennek rugalmasabb alternatívája.
Foreach és típusA foreach (Szam s in lst.Items) alakban a ciklusváltozó típusa is megadható: a C# típuskényszerítést végez, és hibát dob, ha valamelyik elem más típusú.

Jósolj!

A lista egy Szam objektumot tartalmaz, a kijelölt elem módosítása után (kijelolt.Cim = "Új cím";) a lista szövege nem változik. Miért, és mi a javítás?

Jóslat ellenőrzése

Az objektum módosult, de a lista a szöveget a hozzáadáskor (vagy a legutóbbi rajzoláskor) kérte le, és nem tud a változásról. A javítás: lst.Items[lst.SelectedIndex] = kijelolt; (ugyanaz az objektum visszaírva újrarajzoltatja), vagy az adatkötés (BindingList, 8.4 lecke), amely magától frissül.

A kijelölés követése

Form1.csC#
private bool betoltes = false;

private void lst_SelectedIndexChanged(object sender, EventArgs e)
{
if (betoltes)
{
return; // programozott változás közben nem reagálunk
}

Szam sz = lst.SelectedItem as Szam;

// a -1 (nincs kijelölés) is érvényes eset: a gombokat tiltjuk, a mezőket ürítjük
btnTorol.Enabled = sz != null;
btnFel.Enabled = lst.SelectedIndex > 0;
btnLe.Enabled = lst.SelectedIndex >= 0 && lst.SelectedIndex < lst.Items.Count - 1;

txtCim.Text = sz == null ? "" : sz.Cim;
}

// törlés után a szomszéd elem jelölése, hogy a felhasználó folytathassa
private void btnTorol_Click(object sender, EventArgs e)
{
int i = lst.SelectedIndex;

if (i < 0) { return; }

lst.Items.RemoveAt(i);

if (lst.Items.Count > 0)
{
lst.SelectedIndex = Math.Min(i, lst.Items.Count - 1);
}
}
-1 is esetA SelectedIndexChanged lefut, ha a kijelölés megszűnik (Clear, törlés): a kezelőnek a -1 és a null esetet is kezelnie kell, különben NullReferenceException keletkezik.
Jelző a tömeges műveletnélSok elem feltöltésekor vagy törlésekor a kezelő sokszor fut. A betoltes jelzővel kihagyhatod, és a művelet végén egyszer frissítesz (5.5 lecke).
Törlés utáni kijelölésA törlés után a legközelebbi elem kijelölése megőrzi a munkafolyamatot (nem kell újra rákattintani). A Math.Min védi a listavégi esetet.
Gombok állapotaA kijelöléstől függő gombok (Törlés, Fel, Le) engedélyezettsége a SelectedIndexChanged eseményben frissül, és minden módosítás után is meg kell hívni.

Jósolj!

A SelectedIndexChanged kezelő a ((Szam)lst.SelectedItem).Cim sort tartalmazza ellenőrzés nélkül, és a Törlés gomb az utolsó elemet törli. Mi történik?

Jóslat ellenőrzése

Az utolsó elem törlése után a kijelölés megszűnik (-1), az esemény lefut, és a SelectedItem null: NullReferenceException. A kezelőnek mindig kezelnie kell a nincs-kijelölés esetét.

Elem mozgatása és cseréje

A felhasználók gyakran szeretnének egy elemet feljebb vagy lejjebb tenni (lejátszási lista, prioritások). A ListBoxnak nincs „csere” művelete: az elemet kivesszük, és az új helyre beszúrjuk, a kijelölést pedig mi állítjuk vissza:

Form1.csC#
private void Mozgat(int irany)   // -1: fel, +1: le
{
int i = lst.SelectedIndex;
int uj = i + irany;

if (i < 0 || uj < 0 || uj >= lst.Items.Count)
{
return;
}

object elem = lst.Items[i];

betoltes = true; // a közbülső kijelölésváltozások ne fussanak
lst.Items.RemoveAt(i);
lst.Items.Insert(uj, elem);
lst.SelectedIndex = uj; // a kijelölés követi az elemet
betoltes = false;

Frissit(); // gombok állapota, összesítő
}

private void btnFel_Click(object sender, EventArgs e) { Mozgat(-1); }
private void btnLe_Click(object sender, EventArgs e) { Mozgat(1); }
Kivétel és beszúrásAz elemet előbb elmentjük egy változóba, utána töröljük, és az új helyre szúrjuk. A rendezett (Sorted) listán a mozgatás értelmetlen, mert a sorrendet a rendezés adja.
HatárokAz első elemet nem lehet feljebb, az utolsót nem lehet lejjebb vinni. A gombok engedélyezettsége a határokat követi.
Csere helybenKét szomszédos elem cseréje egyszerűbb: object t = Items[i]; Items[i] = Items[i2]; Items[i2] = t;, de a kijelölés ilyenkor az eredeti helyen marad, ezért itt is be kell állítani.
Adatkötésnél másHa a lista adatkötött (BindingList), a mozgatás az adatgyűjteményben történik, és a lista követi (8.4 lecke).

Jósolj!

A Mozgat metódusból kihagyod a lst.SelectedIndex = uj; sort. Mit tapasztal a felhasználó a Fel gomb megnyomásakor?

Jóslat ellenőrzése

Az elem feljebb kerül, de a kijelölés elvész (vagy egy másik elemen marad): a felhasználó nem tudja a Fel gombot ismételten nyomni az elem követésével. A kijelölés visszaállítása a mozgatás része.

📷 Képernyőkép helye

Mit tegyél ide: a lejátszási lista: elemek, mellettük a Fel, Le, Törlés gombok és az összes hossz felirat.Javasolt fájlnév: winforms-8-2-lista.webp · Alt szöveg: Átrendezhető lejátszási lista

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. InvalidCastException a listaelemnél

Szam sz = (Szam)lst.SelectedItem;   // az elem szöveg, vagy null
Hiba és javítás

A kijelölt elem nem Szam típusú (például szöveg került a listába), vagy nincs kijelölés. Használj as és null ellenőrzést, és csak egy típust tegyél a listába.

2. A törlés kihagy egy elemet

A feltételnek megfelelő szomszédos elemek közül az egyik megmarad.

Ok és javítás

Előrefelé haladó for ciklussal töröltél. Járj visszafelé (for (int i = Count - 1; i >= 0; i--)).

3. A módosított adat nem látszik a listában

Az objektum mezőjét átírtad, a lista szövege régi.

Ok és javítás

A lista nem értesül a változásról. Írd vissza az elemet a helyére (Items[i] = elem), vagy használj adatkötést (8.4 lecke).

4. NullReferenceException az utolsó elem törlése után

A SelectedIndexChanged kezelő összeomlik.

Ok és javítás

A kezelő a -1 (nincs kijelölés) esetet nem kezeli. Ellenőrizd a SelectedItem != null feltételt.

5. A Remove nem talál egy elemet

lst.Items.Remove(new Szam { Cim = "Esti dal", Masodperc = 215 });   // új példány
Hiba és javítás

A Remove hivatkozás szerint egyeztet: az új példány nem azonos a listában lévővel. Add meg a listában lévő példányt (SelectedItem), vagy keresd meg tartalom alapján, és úgy töröld.

6. A kijelölés ugrál mozgatás közben

A felhasználó nem tudja követni az elemet.

Ok és javítás

A kivétel és beszúrás közben a kijelölés megváltozik, és a kezelő közben lefut. Használj betoltes jelzőt, és a végén állítsd be az új sorszámot.

Mini projekt: lejátszási lista

Készíts átrendezhető lejátszási listát:

  1. Az ablak: lst (ListBox, Szam objektumok), txtCim, nudPerc és nudMp (hossz), gombok: Hozzáad, Módosít, Töröl, Fel, Le, Duplikál; lblOssz (5 szám, 18:42).
  2. Kijelölésre a mezők feltöltődnek a szám adataival; kijelölés nélkül ürülnek, és a kijelölést igénylő gombok le vannak tiltva.
  3. A Módosít a kijelölt objektumot módosítja, és visszaírja a helyére; a Duplikál másolatot (új példány) tesz közvetlenül az eredeti alá.
  4. A Töröl megerősítést kér (7.4 lecke), a törlés után a szomszéd elem jelölt.
  5. A Fel és Le mozgatja az elemet, a kijelölés vele marad, a határoknál a gomb tiltott.
  6. A lblOssz minden változás után frissül.
  7. Teszteld: hozzáadás, módosítás (a lista szövege frissül), törlés az utolsó elemre, mozgatás a határokon, duplikálás, üres lista.
Megoldás megnyitása (próbáld ki előbb önállóan!)

A Frissit() metódus számolja az összesítést és az engedélyezéseket; minden módosítás után meghívjuk. A betoltes jelző a mozgatás és a mezők feltöltése közben védi a kezelőt.

Form1.csC#
public class Szam
{
public string Cim = "";
public int Masodperc = 0;

public Szam Masol()
{
return (Szam)this.MemberwiseClone();
}

public override string ToString()
{
return Cim + " (" + Masodperc / 60 + ":" + (Masodperc % 60).ToString("00") + ")";
}
}

private bool betoltes = false;

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

private int Hossz()
{
return (int)nudPerc.Value * 60 + (int)nudMp.Value;
}

private void Frissit()
{
int i = lst.SelectedIndex;
bool van = i >= 0;

btnModosit.Enabled = van;
btnTorol.Enabled = van;
btnDuplikal.Enabled = van;
btnFel.Enabled = i > 0;
btnLe.Enabled = van && i < lst.Items.Count - 1;

int ossz = 0;

foreach (Szam s in lst.Items)
{
ossz += s.Masodperc;
}

lblOssz.Text = lst.Items.Count + " szám, " + ossz / 60 + ":" + (ossz % 60).ToString("00");
}

private void lst_SelectedIndexChanged(object sender, EventArgs e)
{
if (betoltes) { return; }

Szam sz = lst.SelectedItem as Szam;

betoltes = true;
txtCim.Text = sz == null ? "" : sz.Cim;
nudPerc.Value = sz == null ? 0 : Math.Min(nudPerc.Maximum, sz.Masodperc / 60);
nudMp.Value = sz == null ? 0 : sz.Masodperc % 60;
betoltes = false;

Frissit();
}

private void btnHozzaad_Click(object sender, EventArgs e)
{
if (txtCim.Text.Trim().Length == 0) { return; }

Szam sz = new Szam { Cim = txtCim.Text.Trim(), Masodperc = Hossz() };
lst.SelectedIndex = lst.Items.Add(sz);
}

private void btnModosit_Click(object sender, EventArgs e)
{
Szam sz = lst.SelectedItem as Szam;

if (sz == null) { return; }

sz.Cim = txtCim.Text.Trim();
sz.Masodperc = Hossz();

betoltes = true;
lst.Items[lst.SelectedIndex] = sz; // a lista szövegének frissítése
betoltes = false;

Frissit();
}

private void btnDuplikal_Click(object sender, EventArgs e)
{
Szam sz = lst.SelectedItem as Szam;

if (sz != null)
{
lst.Items.Insert(lst.SelectedIndex + 1, sz.Masol());
Frissit();
}
}

private void btnTorol_Click(object sender, EventArgs e)
{
int i = lst.SelectedIndex;

if (i < 0 || MessageBox.Show(this, "Törlöd a kijelölt számot?", "Törlés", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes)
{
return;
}

lst.Items.RemoveAt(i);

if (lst.Items.Count > 0)
{
lst.SelectedIndex = Math.Min(i, lst.Items.Count - 1);
}

Frissit();
}

private void Mozgat(int irany)
{
int i = lst.SelectedIndex;
int uj = i + irany;

if (i < 0 || uj < 0 || uj >= lst.Items.Count) { return; }

object elem = lst.Items[i];

betoltes = true;
lst.Items.RemoveAt(i);
lst.Items.Insert(uj, elem);
lst.SelectedIndex = uj;
betoltes = false;

Frissit();
}

private void btnFel_Click(object sender, EventArgs e) { Mozgat(-1); }
private void btnLe_Click(object sender, EventArgs e) { Mozgat(1); }

Három önálló gyakorlófeladat

Egyszerre csak egyet csinálj, és csak utána nyisd ki a megoldást.

1. Első és utolsó

Két gomb (Az elejére, A végére) a kijelölt elemet a lista elejére vagy végére viszi. Hogyan?

Egy lehetséges megoldás

Kivétel, majd beszúrás a 0 vagy Count helyre, kijelölés beállítása.

Form1.csC#
private void Szel(bool elejere)
{
int i = lst.SelectedIndex;

if (i < 0) { return; }

object elem = lst.Items[i];
lst.Items.RemoveAt(i);

int uj = elejere ? 0 : lst.Items.Count;
lst.Items.Insert(uj, elem);
lst.SelectedIndex = uj;
}

2. Keresés tartalom szerint

A Keres gomb kijelöli azt a számot, amelynek a címe megegyezik a beírt szöveggel. Miért nem jó a Items.IndexOf(new Szam{...})?

Egy lehetséges megoldás

Az IndexOf hivatkozás szerint keres, az új példány nem egyezik. Ciklus kell a tartalom összehasonlítására.

Form1.csC#
for (int i = 0; i < lst.Items.Count; i++)
{
if (((Szam)lst.Items[i]).Cim.Equals(txtKeres.Text.Trim(), StringComparison.CurrentCultureIgnoreCase))
{
lst.SelectedIndex = i;
return;
}
}

lblUzenet.Text = "Nincs ilyen című szám.";

3. Több kijelölt elem mozgatása

MultiExtended listán a Fel gomb az összes kijelölt elemet egy hellyel feljebb viszi. Mire kell vigyáznod?

Egy lehetséges megoldás

A sorszámokat előbb egy listába másold, és felülről lefelé dolgozz; ha az első elem már a lista elején van, nem mozgatható semmi.

Form1.csC#
List<int> indexek = new List<int>();

foreach (int i in lst.SelectedIndices) { indexek.Add(i); }

if (indexek.Count == 0 || indexek[0] == 0) { return; }

lst.BeginUpdate();

foreach (int i in indexek)
{
object elem = lst.Items[i];
lst.Items.RemoveAt(i);
lst.Items.Insert(i - 1, elem);
}

lst.ClearSelected();

foreach (int i in indexek) { lst.SetSelected(i - 1, true); }

lst.EndUpdate();

Gyakorlótár: 6 további feladat

Alap, közepes és kihívás szinten gyakorolhatsz. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Add visszatérési értéke

Mire használod az Add által visszaadott számot?

Megoldás megnyitása

Az új elem sorszáma: egy sorban hozzáadhatod és kijelölheted (lst.SelectedIndex = lst.Items.Add(x);).

2. as vagy kényszerítés?

Mikor jobb az as, mikor a (Szam) kényszerítés?

Megoldás megnyitása

Az as null-t ad (nincs kijelölés, más típus): védett kódhoz. A (Szam) kivételt dob, ha nem megfelelő: ott használd, ahol a típus biztosan helyes (például a listát te töltöd, és minden elem Szam).

Közepes szint

3. Összes elem szövegben

Készíts szöveget, amely a lista elemeit vesszővel elválasztva sorolja fel.

Megoldás megnyitása

string.Join és a ToString(): a Items elemei object típusúak, ezért tömbbe kell másolni.

Form1.csC#
List<string> szovegek = new List<string>();

foreach (object o in lst.Items)
{
szovegek.Add(o.ToString());
}

string vege = string.Join(", ", szovegek);

4. A lista rendezése tartalom szerint

A Rendezés hossz szerint gomb a listát a számok hossza szerint rendezi. A Sorted nem jó, miért?

Megoldás megnyitása

A Sorted a ToString() szövege szerint rendez (ábécé), nem a hossz szerint. A számokat egy List<Szam> listába másolod, rendezed (Sort lambdával), és újratöltöd.

Form1.csC#
List<Szam> tomb = new List<Szam>();

foreach (Szam s in lst.Items) { tomb.Add(s); }

tomb.Sort((a, b) => a.Masodperc.CompareTo(b.Masodperc));

lst.BeginUpdate();
lst.Items.Clear();
foreach (Szam s in tomb) { lst.Items.Add(s); }
lst.EndUpdate();

Kihívás szint

5. Visszavonás

Az utolsó törlés visszavonható. Hogyan őrzöd az elemet és a helyét?

Megoldás megnyitása

Egy Stack veremre teszed a törölt elemet és a sorszámát; a visszavonás kiveszi, és visszaszúrja.

Form1.csC#
private readonly Stack<KeyValuePair<int, object>> torolt = new Stack<KeyValuePair<int, object>>();

private void TorolVisszavonhatoan(int i)
{
torolt.Push(new KeyValuePair<int, object>(i, lst.Items[i]));
lst.Items.RemoveAt(i);
}

private void btnVisszavon_Click(object sender, EventArgs e)
{
if (torolt.Count > 0)
{
KeyValuePair<int, object> t = torolt.Pop();
lst.Items.Insert(Math.Min(t.Key, lst.Items.Count), t.Value);
lst.SelectedIndex = Math.Min(t.Key, lst.Items.Count - 1);
}
}

6. Egyenlőség tartalom szerint

A Szam osztály Equals metódusa a címet és a hosszt hasonlítja. Mi változik a Contains és Remove viselkedésében?

Megoldás megnyitása

Ezek innentől tartalom szerint egyeztetnek, a Contains(new Szam{...}) megtalálja az azonos adatú elemet. Az Equals felülírásakor a GetHashCode felülírása is kötelező szokás.

Szam.csC#
public override bool Equals(object obj)
{
Szam masik = obj as Szam;

return masik != null && masik.Cim == Cim && masik.Masodperc == Masodperc;
}

public override int GetHashCode()
{
return Cim.GetHashCode() ^ Masodperc;
}

Gyors önellenőrzés

1. Mit ad vissza az Items.Add?

Az új elem sorszámát.

2. Hogyan törölsz bejárás közben?

Visszafelé haladó for ciklussal.

3. Mit ad a SelectedItem kijelölés nélkül?

null (a SelectedIndex -1).

4. Miért nem frissül a lista szövege az objektum módosítása után?

A lista nem tud a változásról: írd vissza az elemet (Items[i] = elem), vagy használj adatkötést.

5. Hogyan mozgatsz elemet?

Kivétel (RemoveAt), beszúrás az új helyre (Insert), a kijelölés beállítása.

6. Miért kell a -1 esetet kezelni?

A SelectedIndexChanged törlésnél és Clear-nél is lefut, ilyenkor nincs kijelölt elem.

7. Hogyan egyeztet a Remove és a Contains?

Az Equals szerint: saját objektumnál alapból hivatkozás szerint.

Mit tudsz már?

  • Az Items gyűjtemény összes műveletét használod.
  • Saját objektumokat tárolsz a listában, és típushelyesen olvasod vissza.
  • Követed a kijelölést, és védetten kezeled a SelectedIndexChanged eseményt.
  • Elemet mozgatsz és cserélsz úgy, hogy a kijelölés vele marad.

Következő lecke: DataGridView – oszlopok, sorok és cellák

A táblázat: oszlopok, sorok és cellák, szerkesztés, kijelölés és a leggyakoribb beállítások.