Grafikus C# – Windows Forms tanfolyam › 6.5 lecke

Windows Forms · 6.5 lecke · füles felületek

TabControl és többlapos felületek

Amikor egy űrlapon túl sok vezérlő van, a felhasználó eltéved. A TabControl füles lapokra osztja a felületet: a beállításokat, a dokumentumokat, az űrlap szakaszait egyszerre mindig csak egy lap mutatja. Ebben a leckében megtanulod a TabControl és a TabPage használatát a tervezőben és kódból, a lapváltás eseményeit (Selecting, SelectedIndexChanged), a lapok ellenőrzésre és késleltetett betöltésre való felhasználását, a lapok kódból való hozzáadását és bezárását, és elkészíted egy többfüles jegyzettömb alapját.

  • kb. 110 perc
  • közepes szint
  • TabControl
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A lecke végére

  • felépítesz egy többlapos felületet TabControl és TabPage vezérlőkkel (tervezőben és kódból);
  • kezeled a lapfülek megjelenését (Alignment, Appearance, SizeMode, ikonok);
  • használod a lapváltás eseményeit, és a Selecting eseménnyel megakadályozod vagy ellenőrzöd a lapváltást;
  • tudod, hogy a TabPage.Visible nem rejti el a fület, és hogyan rejts el lapot helyesen;
  • lapokat adsz hozzá és távolítasz el futás közben, és kezeled az üres TabControl esetet;
  • a lap tartalmát késleltetve tölted be, és több dokumentumot kezelsz füles felületen.

Mielőtt elkezded

Ehhez a leckéhez a 6.3 lecke (tárolók, lapozó panelek), az 5.5 lecke (változás-események), az 5.7 lecke (leiratkozás és Dispose) és a 2.14 lecke (mentetlen dokumentum) szükséges. Új projektben próbáld ki a példákat.

6.4 TableLayoutPanel és FlowLayoutPanel →

TabControl és TabPage

A TabControl egy tároló, amelynek gyermekei TabPage (lap) objektumok, mindegyikhez egy fül tartozik. A lapok maguk is tárolók: a vezérlők a lapra kerülnek, nem a TabControl felületére. A tervezőben a TabControl Smart Tag menüjében (Add Tab, Remove Tab) vagy a TabPages tulajdonság szerkesztőjében (… gomb) kezeled a lapokat.

TulajdonságJelentése
TabPagesA lapok gyűjteménye (Add, Remove, Insert, indexelés).
SelectedIndex, SelectedTabA kiválasztott lap sorszáma, illetve maga a lap. Ha nincs lap, a SelectedIndex értéke -1, a SelectedTab null.
TabCountA lapok száma.
AlignmentHol vannak a fülek: Top (alap), Bottom, Left, Right.
AppearanceNormal (fülek), Buttons, FlatButtons (gombszerű fülek).
MultilineSok fül esetén több sorba törnek (különben görgethetők).
SizeModeA fülek mérete: Normal, FillToRight (kitöltik a sort), Fixed (egyforma szélesség az ItemSize szerint).
ImageList és TabPage.ImageIndexIkon a fülön (az ImageList a 1.12 leckéből).
Form1.csC#
// a TabControl kitölti az ablakot: Dock = Fill (tervezőben is beállítható)
tc.Dock = DockStyle.Fill;

// lapok elérése
TabPage elso = tc.TabPages[0];
TabPage aktualis = tc.SelectedTab;
lblInfo.Text = $"{tc.TabCount} lap, a kiválasztott: {aktualis.Text}";

// lap kiválasztása
tc.SelectedIndex = 1;
tc.SelectedTab = tabBeallitasok;

// a lap szövege, és a lapon lévő vezérlők
tabAdatok.Text = "Adatok";
foreach (Control c in tabAdatok.Controls)
{
c.Enabled = true;
}
A vezérlők a lapon vannakA txtNev szülője a tabAdatok lap, nem a TabControl. A lapon lévő vezérlők elérése a tabAdatok.Controls gyűjteményen át történik. A Dock és az Anchor a lapra vonatkozik (6.2 lecke).
A TabControl Dock = FillEgyszerű ablakoknál a TabControl kitölti az ablakot, és a lapok tartalmát a lapon belül rendezed (TableLayoutPanel, 6.4 lecke).
Ctrl+TabA TabControl beépítetten kezeli a Ctrl+Tab és Ctrl+Shift+Tab billentyűket a lapok közötti váltáshoz (ha a fókusz a TabControl-ban van).
Üres TabControlHa az összes lapot eltávolítod, a SelectedTab értéke null, és a rá hivatkozó kód NullReferenceException hibát dob. Mindig ellenőrizd a null értéket.

Jósolj!

A TabControlban három lap van, a harmadik van kiválasztva. A kódból tc.TabPages.RemoveAt(2);-t hívsz. Melyik lap lesz kiválasztva, és mi a SelectedIndex értéke?

Jóslat ellenőrzése

Az utolsó lap eltávolítása után a kiválasztás általában az előző lapra kerül (a második lap, SelectedIndex = 1). Nem szabad azonban a viselkedésre építeni: egy lap bezárása után mindig állítsd be a kiválasztást kifejezetten, és ellenőrizd, hogy maradt-e lap (tc.TabCount > 0).

A lapváltás eseményei

Lapváltáskor több esemény fut, mindegyik más pillanatban:

EseményMikor?Mire való?
DeselectingA régi lap elhagyása előtt.Ellenőrzés: elhagyható-e a lap (e.Cancel).
SelectingAz új lap kiválasztása előtt.Ellenőrzés: megnyitható-e a lap (e.Cancel, e.TabPage).
DeselectedA régi lap elhagyása után.Takarítás, mentés.
SelectedAz új lap kiválasztása után.Az új lap frissítése.
SelectedIndexChangedA kiválasztott lap sorszáma megváltozott.Egyszerű reagálás a váltásra (a leggyakoribb).
Form1.csC#
// 1. egyszerű reagálás a váltásra
private void tc_SelectedIndexChanged(object sender, EventArgs e)
{
if (tc.SelectedTab == null)
{
lblAllapot.Text = "Nincs megnyitott lap.";
return;
}

lblAllapot.Text = "Aktív lap: " + tc.SelectedTab.Text;
}

// 2. a lap elhagyásának megakadályozása érvénytelen adatnál
private void tc_Deselecting(object sender, TabControlCancelEventArgs e)
{
if (e.TabPage == tabAdatok && txtNev.Text.Trim().Length == 0)
{
MessageBox.Show("Előbb add meg a nevet!");
e.Cancel = true; // a lap nem váltódik
}
}

// 3. egy lap letiltása: nem választható ki
private void tc_Selecting(object sender, TabControlCancelEventArgs e)
{
if (e.TabPage == tabHaladó && !chkHaladó.Checked)
{
e.Cancel = true;
}
}
TabControlCancelEventArgsAz e.TabPage a lap, amelyről szó van, az e.TabPageIndex a sorszáma, az e.Cancel = true megszakítja a váltást. A Selecting és a Deselecting eseményeknél van.
Selecting vagy Deselecting?A Deselecting a régi lap elhagyását, a Selecting az új lap megnyitását ellenőrzi. Az „előbb töltsd ki az adatokat” szabály a Deselecting, a „ez a lap még nem elérhető” a Selecting esemény dolga.
Ne váltsd a lapot a Selecting eseménybenA Selecting kezelőben a SelectedIndex átállítása újabb Selecting eseményt vált ki, és végtelen visszacsatolás lehet. Csak a Cancel értéket állítsd.
A Selected esemény a frissítésreA lap adatainak betöltésére a Selected (vagy a SelectedIndexChanged) a jó hely: ilyenkor már az új lap látszik.

Jósolj!

A tc_SelectedIndexChanged kezelőben a tc.SelectedTab.Text értéket írod ki. Az utolsó lapot bezárod a programból (tc.TabPages.Remove(lap)). Mi történik?

Jóslat ellenőrzése

Az utolsó lap eltávolítása után a SelectedTab értéke null, és a SelectedIndexChanged eseményben a .Text lekérdezése NullReferenceException kivételt dob. A kezelőben ezért mindig ellenőrizd a SelectedTab == null esetet.

Lapok kezelése kódból

Form1.csC#
// új lap hozzáadása és kiválasztása
TabPage lap = new TabPage("Új lap");

TextBox szoveg = new TextBox();
szoveg.Multiline = true;
szoveg.Dock = DockStyle.Fill;
lap.Controls.Add(szoveg);

tc.TabPages.Add(lap);
tc.SelectedTab = lap;

// lap eltávolítása és felszabadítása
tc.TabPages.Remove(lap);
lap.Dispose(); // a lapon lévő vezérlőkkel együtt

// lap elrejtése: a TabPage.Visible NEM működik, el kell távolítani, és eltárolni
TabPage rejtett = tabHaladó;
tc.TabPages.Remove(rejtett); // a hivatkozást a mezőben megőrizzük

// visszarakás (például az eredeti helyére)
tc.TabPages.Insert(2, rejtett);

A TabPage.Visible nem rejti el a fület

A TabPage.Visible = false beállításnak nincs hatása a fülre: a lapfül továbbra is látszik, és a lap kiválasztható. Lap elrejtésére az a megoldás, hogy a lapot eltávolítod a TabPages gyűjteményből (Remove), és a hivatkozást egy mezőben megőrzöd, hogy később visszatehesd (Insert vagy Add). Egy lapot elég letiltani, ha csak a tartalmát akarod elérhetetlenné tenni (Enabled = false, de a fül kiválasztható marad).

Késleltetett betöltés: csak akkor töltsd, ha megnyitják

Ha egy lap tartalma lassan töltődik (adatbázis, nagy fájl), ne töltsd be induláskor az összes lapét: elég a lap első megnyitásakor. Egy jelző (a lap Tag tulajdonságában vagy egy halmazban) megjegyzi, hogy betöltötted-e már:

Form1.csC#
private readonly HashSet<TabPage> betoltott = new HashSet<TabPage>();

private void tc_Selected(object sender, TabControlEventArgs e)
{
if (e.TabPage == null || betoltott.Contains(e.TabPage))
{
return; // már betöltöttük
}

betoltott.Add(e.TabPage);

if (e.TabPage == tabStatisztika)
{
StatisztikaBetolt(); // a lassú lekérdezés csak egyszer fut
}
}

Jósolj!

A tabStatisztika lapon egy DataGridView van, amelyet a Form1_Load eseményben töltesz fel. A lap nincs kiválasztva induláskor. Mit tapasztalsz a lap első megnyitásakor?

Jóslat ellenőrzése

A TabControl a nem látható lapok vezérlőit csak a lap első megnyitásakor „hozza létre teljesen” (az ablakkezelőjük később jön létre). A tapasztalat szerint a nem látható lapon lévő vezérlők adatkötése és egyes beállításai ezért késve vagy váratlanul viselkedhetnek, ezért a feltöltésüket gyakran biztonságosabb a lap megnyitásakor (Selected) végezni. A pontos viselkedés a vezérlőtől függ, ezért a saját programodban próbáld ki.

Megjelenés: fülek, ikonok, saját rajz

A lapfülek kinézetét az alábbiakkal módosíthatod:

Form1.csC#
// fülek a bal oldalon, egyforma méretben
tc.Alignment = TabAlignment.Left;
tc.SizeMode = TabSizeMode.Fixed;
tc.ItemSize = new Size(40, 120); // bal oldali fülnél a méret felcserélődik: magasság x szélesség

// ikonok a füleken
tc.ImageList = imlIkonok;
tabAdatok.ImageIndex = 0;
tabBeallitasok.ImageIndex = 1;

// saját rajzolású fülek: a mentetlen lap szövege piros
tc.DrawMode = TabDrawMode.OwnerDrawFixed;

private void tc_DrawItem(object sender, DrawItemEventArgs e)
{
TabPage lap = tc.TabPages[e.Index];
bool kivalasztott = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
Color hatter = kivalasztott ? Color.White : Color.FromArgb(235, 235, 240);
Color szin = lap.Text.EndsWith("*") ? Color.Firebrick : Color.Black;

using (SolidBrush ecset = new SolidBrush(hatter))
{
e.Graphics.FillRectangle(ecset, e.Bounds);
}

TextRenderer.DrawText(e.Graphics, lap.Text, e.Font, e.Bounds, szin,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
}
Bal oldali fülekA Left és Right elhelyezésnél az ItemSize szélessége és magassága a forgatás miatt felcserélődik: először a (rövidebb) magasság, utána a (hosszabb) szélesség adja a fülek vízszintes kiterjedését. A pontos értéket kísérletezéssel állítsd be.
OwnerDrawFixedA DrawMode értékével a fülek rajzolása a te kezedbe kerül: a DrawItem eseményben kell megrajzolni minden fület. Az e.Bounds a fül területe, az e.State az állapota (kiválasztott).
TextRenderer.DrawTextA Graphics.DrawString helyett a TextRenderer a Windows vezérlők betűmegjelenítését használja, ezért a szöveg ugyanúgy néz ki, mint a beépített fülek szövege.
Rajzolás után újrarajzolásHa a lap szövege változik (* a végén), a tc.Invalidate() hívás kényszeríti a fülek újrarajzolását.

Jósolj!

Beállítod az OwnerDrawFixed módot, de a DrawItem eseményt nem kötöd hozzá (vagy az üres). Mit látsz a füleken?

Jóslat ellenőrzése

A fülek üresen jelennek meg (vagy a rajzolás nélküli háttérrel): a rendszer a rajzolást teljesen rád bízza, és ha nem rajzolsz semmit, a fül sem mutat feliratot. A saját rajzolás a feliratot is neked kell megrajzolnod.

📷 Képernyőkép helye

Mit tegyél ide: egy többfüles ablak, amelynek egyik füle piros szöveggel (mentetlen) jelenik meg, mellette a normál fülek.Javasolt fájlnév: winforms-6-5-tabok.webp · Alt szöveg: Füles felület saját rajzolású fülekkel

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 lap nem tűnik el

tabHaladó.Visible = false;   // a fül továbbra is látszik
Hiba és javítás

A TabPage.Visible nem rejti el a fület. Távolítsd el a lapot a TabPages gyűjteményből, és a hivatkozást őrizd meg a visszarakáshoz.

2. NullReferenceException az utolsó lap bezárása után

lblCim.Text = tc.SelectedTab.Text;   // ha nincs lap, a SelectedTab null
Hiba és javítás

Ellenőrizd a SelectedTab != null feltételt, vagy a TabCount > 0 értéket, mielőtt használod.

3. A lassú lap minden megnyitáskor újratölt

A statisztika lap minden váltásnál másodpercekig tölt.

Ok és javítás

A betöltés a SelectedIndexChanged eseményben fut, jelző nélkül. Jegyezd meg, hogy betöltötted-e már a lapot (HashSet<TabPage> vagy jelző), vagy csak adatváltozáskor frissítsd.

4. Végtelen ciklus a Selecting eseményben

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

A SelectedIndex átállítása újabb Selecting eseményt vált ki. Csak az e.Cancel = true értéket állítsd, így a lap nem váltódik, és a kiválasztás a régi lapon marad.

5. A bezárt lap vezérlői tovább élnek

tc.TabPages.Remove(lap);   // a Dispose hiányzik
Hiba és javítás

A Remove csak kiveszi a lapot a gyűjteményből, de nem szabadítja fel. Hívd a lap.Dispose() metódust (a lapon lévő vezérlőket is felszabadítja), különösen sok dinamikus lapnál (5.7 lecke).

6. A vezérlő nem a lapon, hanem a TabControlon van

A vezérlő minden lapon látszik, vagy a lapváltáskor nem váltódik.

Ok és javítás

A vezérlő a TabControl fölé, nem a lapra került (a tervezőben a lap fölé húztad, de nem a lapra ejtetted). A Document Outline ablakban látható a hierarchia. Húzd a vezérlőt a lapra, vagy kódból: vezerlo.Parent = lap;.

Mini projekt: többfüles jegyzettömb

Készíts jegyzettömböt, amelyben minden dokumentum külön lapon van:

  1. Az ablak: MenuStrip (Új, Megnyitás, Mentés, Bezárás), tc (TabControl, Dock = Fill), alul egy lblAllapot (karakterszám).
  2. Egy kis Dokumentum osztály tárolja egy lap adatait: a szerkesztőt (RichTextBox), a lapot, az útvonalat, a nevet és a módosított jelzőt.
  3. Az Új (Ctrl+T) új lapot nyit egy üres szerkesztővel (Névtelen 1, Névtelen 2…). A Megnyitás (Ctrl+O) szövegfájlt tölt új lapra (a fájl neve a lap szövege).
  4. A szerkesztés módosítottnak jelöli a dokumentumot: a lap szövege *-ot kap, a fül piros színű (saját rajzolás).
  5. A Mentés (Ctrl+S) elmenti az aktuális lapot (új dokumentumnál SaveFileDialog), és törli a csillagot.
  6. A Bezárás (Ctrl+W, vagy a fül középső egérgombbal kattintva) a lapot bezárja; ha módosított, rákérdez (Igen/Nem/Mégse, 2.14 lecke). A lap felszabadítása (Dispose) és a lista tisztítása kötelező.
  7. Az ablak bezárásakor, ha van módosított dokumentum, a program rákérdez (FormClosing).
  8. Teszteld: több lap, szerkesztés, mentés, bezárás mentetlenül (mindhárom válasz), az utolsó lap bezárása, a billentyűk, a középső gomb.
Megoldás megnyitása (próbáld ki előbb önállóan!)

A lapok és dokumentumok párosítását a lap Tag tulajdonsága adja (egy Dokumentum objektum). A lambda feliratkozás a szerkesztő TextChanged eseményére kerül, miután a kezdeti szöveg már beállt, így a betöltés nem számít módosításnak.

Form1.cs (using System.IO; using System.Collections.Generic;)C#
private class Dokumentum
{
public RichTextBox Szerkeszto;
public TabPage Lap;
public string Utvonal = "";
public string Nev = "";
public bool Modosult = false;
}

private int szamlalo = 0;

private void Form1_Load(object sender, EventArgs e)
{
tc.DrawMode = TabDrawMode.OwnerDrawFixed;
UjDokumentum("", "");
}

private void CimFrissit(Dokumentum d)
{
d.Lap.Text = d.Nev + (d.Modosult ? "*" : "");
tc.Invalidate();
}

private Dokumentum UjDokumentum(string utvonal, string tartalom)
{
Dokumentum d = new Dokumentum();
d.Utvonal = utvonal;
d.Nev = utvonal.Length == 0 ? "Névtelen " + (++szamlalo) : Path.GetFileName(utvonal);

d.Szerkeszto = new RichTextBox();
d.Szerkeszto.Dock = DockStyle.Fill;
d.Szerkeszto.BorderStyle = BorderStyle.None;
d.Szerkeszto.Font = new Font("Consolas", 11);
d.Szerkeszto.Text = tartalom; // előbb a szöveg, utána a figyelés

d.Lap = new TabPage(d.Nev);
d.Lap.Tag = d;
d.Lap.Controls.Add(d.Szerkeszto);

d.Szerkeszto.TextChanged += (s, ev) =>
{
if (!d.Modosult)
{
d.Modosult = true;
CimFrissit(d);
}

Statisztika();
};

tc.TabPages.Add(d.Lap);
tc.SelectedTab = d.Lap;
d.Szerkeszto.Focus();

return d;
}

private Dokumentum Aktualis()
{
return tc.SelectedTab == null ? null : (Dokumentum)tc.SelectedTab.Tag;
}

private void Statisztika()
{
Dokumentum d = Aktualis();
lblAllapot.Text = d == null ? "Nincs megnyitott dokumentum." : "Karakterek: " + d.Szerkeszto.TextLength;
}

private void tc_SelectedIndexChanged(object sender, EventArgs e)
{
Statisztika();
}

private bool Ment(Dokumentum d)
{
if (d.Utvonal.Length == 0)
{
sfdMentes.FileName = d.Nev;

if (sfdMentes.ShowDialog() != DialogResult.OK)
{
return false;
}

d.Utvonal = sfdMentes.FileName;
d.Nev = Path.GetFileName(d.Utvonal);
}

try
{
File.WriteAllText(d.Utvonal, d.Szerkeszto.Text);
d.Modosult = false;
CimFrissit(d);
return true;
}
catch (Exception ex)
{
MessageBox.Show("A mentés nem sikerült: " + ex.Message);
return false;
}
}

private bool Bezar(Dokumentum d)
{
if (d.Modosult)
{
tc.SelectedTab = d.Lap;
DialogResult valasz = MessageBox.Show("Mented a változtatásokat: " + d.Nev + "?", "Bezárás", MessageBoxButtons.YesNoCancel);

if (valasz == DialogResult.Cancel || (valasz == DialogResult.Yes && !Ment(d)))
{
return false;
}
}

tc.TabPages.Remove(d.Lap);
d.Lap.Dispose();
Statisztika();
return true;
}

private void miUj_Click(object sender, EventArgs e)
{
UjDokumentum("", "");
}

private void miMegnyit_Click(object sender, EventArgs e)
{
if (ofdMegnyit.ShowDialog() != DialogResult.OK)
{
return;
}

try
{
UjDokumentum(ofdMegnyit.FileName, File.ReadAllText(ofdMegnyit.FileName));
}
catch (Exception ex)
{
MessageBox.Show("A fájl nem nyitható meg: " + ex.Message);
}
}

private void miMent_Click(object sender, EventArgs e)
{
Dokumentum d = Aktualis();

if (d != null)
{
Ment(d);
}
}

private void miBezar_Click(object sender, EventArgs e)
{
Dokumentum d = Aktualis();

if (d != null)
{
Bezar(d);
}
}

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == (Keys.Control | Keys.T)) { miUj_Click(this, EventArgs.Empty); return true; }
if (keyData == (Keys.Control | Keys.O)) { miMegnyit_Click(this, EventArgs.Empty); return true; }
if (keyData == (Keys.Control | Keys.S)) { miMent_Click(this, EventArgs.Empty); return true; }
if (keyData == (Keys.Control | Keys.W)) { miBezar_Click(this, EventArgs.Empty); return true; }

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

private void tc_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Middle)
{
return;
}

for (int i = 0; i < tc.TabCount; i++)
{
if (tc.GetTabRect(i).Contains(e.Location))
{
Bezar((Dokumentum)tc.TabPages[i].Tag);
break;
}
}
}

private void tc_DrawItem(object sender, DrawItemEventArgs e)
{
TabPage lap = tc.TabPages[e.Index];
bool kivalasztott = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
Color hatter = kivalasztott ? Color.White : Color.FromArgb(235, 235, 240);
Color szin = lap.Text.EndsWith("*") ? Color.Firebrick : Color.Black;

using (SolidBrush ecset = new SolidBrush(hatter))
{
e.Graphics.FillRectangle(ecset, e.Bounds);
}

TextRenderer.DrawText(e.Graphics, lap.Text, e.Font, e.Bounds, szin, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
}

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

for (int i = tc.TabCount - 1; i >= 0; i--)
{
if (!Bezar((Dokumentum)tc.TabPages[i].Tag))
{
e.Cancel = true; // a felhasználó a Mégse gombot választotta
return;
}
}
}
📷 Képernyőkép helye

Mit tegyél ide: a többfüles jegyzettömb három lappal, az egyik piros fülű (mentetlen), alul a karakterszám.Javasolt fájlnév: winforms-6-5-kesz.webp · Alt szöveg: A többfüles jegyzettömb

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. Beállítóablak füles felülettel

Készíts beállítóablakot Általános, Megjelenés és Hálózat lappal és OK, Mégse, Alkalmaz gombbal (a TabControl alatt, Anchor = Bottom, Right). Mi a TabControl és a gombok viszonya?

Egy lehetséges megoldás

A gombok nem a lapon vannak, hanem az ablakon, a TabControl alatt, ezért minden lapnál láthatók. A TabControl Anchor értéke négy irány, a magassága a gombsor fölött végződik. Az Alkalmaz gomb csak módosításkor aktív.

Form1.csC#
private bool valtozas = false;

private void Form1_Load(object sender, EventArgs e)
{
btnAlkalmaz.Enabled = false;

// minden módosítható vezérlő jelzi a változást
chkIndit.CheckedChanged += Valtozott;
txtProxy.TextChanged += Valtozott;
cmbTema.SelectedIndexChanged += Valtozott;
}

private void Valtozott(object sender, EventArgs e)
{
valtozas = true;
btnAlkalmaz.Enabled = true;
}

private void btnAlkalmaz_Click(object sender, EventArgs e)
{
// beállítások mentése
valtozas = false;
btnAlkalmaz.Enabled = false;
}

private void btnOk_Click(object sender, EventArgs e)
{
if (valtozas)
{
btnAlkalmaz_Click(sender, e);
}

this.Close();
}

2. Ellenőrzés lapelhagyáskor

Egy regisztrációs űrlap Adatok, Jelszó és Összegzés lapjából a Jelszó lapra csak akkor lehet átlépni, ha a név és az e-mail ki van töltve. Melyik eseményt használod, és mit írsz az üzenetben?

Egy lehetséges megoldás

A Deselecting esemény a régi lap (Adatok) elhagyását ellenőrzi. Hibánál a hibás mezőre teszi a fókuszt, hogy a felhasználó tudja, mit kell javítania.

Form1.csC#
private void tc_Deselecting(object sender, TabControlCancelEventArgs e)
{
if (e.TabPage != tabAdatok)
{
return;
}

if (txtNev.Text.Trim().Length == 0)
{
MessageBox.Show("A név kötelező.");
e.Cancel = true;
txtNev.Focus();
}
else if (!txtEmail.Text.Contains("@"))
{
MessageBox.Show("Az e-mail címnek tartalmaznia kell a @ jelet.");
e.Cancel = true;
txtEmail.Focus();
}
}

3. Lap bezárása a fülön lévő ✕ jelre kattintva

A fülek szövege végén ✕ látszik (a lap szövege: Névtelen 1 ✕). A ✕ jelre kattintva a lap bezárul. Hogyan állapítod meg, hogy a jelre kattintottak?

Egy lehetséges megoldás

A MouseUp eseményben megkeressük a kattintott fület (GetTabRect), és ellenőrizzük, hogy a kattintás a fül jobb szélső sávjában történt-e (például a jobb szélétől 20 képpontra). Pontosabb megoldás a fül saját rajzolása és a jel területének kiszámítása.

Form1.csC#
private void tc_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left)
{
return;
}

for (int i = 0; i < tc.TabCount; i++)
{
Rectangle ful = tc.GetTabRect(i);
Rectangle xTerulet = new Rectangle(ful.Right - 22, ful.Top, 22, ful.Height); // a jobb szélső sáv

if (xTerulet.Contains(e.Location))
{
TabPage lap = tc.TabPages[i];
tc.TabPages.Remove(lap);
lap.Dispose();
return;
}
}
}

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. Új lap kódból

Egy gomb új lapot ad a tc vezérlőhöz Lap N szöveggel (N a lap sorszáma), és kiválasztja. Mit kell tenned, hogy a lap tartalmazzon egy Label vezérlőt?

Megoldás megnyitása

A vezérlőt a lap Controls gyűjteményéhez kell adni (nem a TabControl-hoz).

Form1.csC#
private void btnUjLap_Click(object sender, EventArgs e)
{
TabPage lap = new TabPage("Lap " + (tc.TabCount + 1));

Label cimke = new Label();
cimke.Text = "Ez a(z) " + lap.Text + " tartalma.";
cimke.AutoSize = true;
cimke.Location = new Point(12, 12);

lap.Controls.Add(cimke);
tc.TabPages.Add(lap);
tc.SelectedTab = lap;
}

2. Az aktív lap neve

Az ablak címsorában mindig az aktív lap neve szerepel (Szerkesztő – Beállítások). Melyik eseményt használod, és mire kell vigyázni?

Megoldás megnyitása

A SelectedIndexChanged eseményt. A SelectedTab lehet null (nincs lap), ezért ellenőrizni kell. Induláskor is be kell állítani a címet (a Load eseményben).

Form1.csC#
private void CimFrissit()
{
this.Text = tc.SelectedTab == null ? "Szerkesztő" : "Szerkesztő – " + tc.SelectedTab.Text;
}

private void tc_SelectedIndexChanged(object sender, EventArgs e)
{
CimFrissit();
}

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

Közepes szint

3. Lapok ki- és bekapcsolása

A Haladó lap csak akkor látszik, ha a chkHalado jelölőnégyzet be van jelölve. Hogyan oldod meg, hogy a lap tartalma megmaradjon a ki- és bekapcsolás között, és a lap az eredeti helyére (az utolsó helyre) kerüljön vissza?

Megoldás megnyitása

A lap hivatkozása megmarad a tabHalado mezőben (a tervező létrehozta), ezért a tartalom sem vész el az eltávolításkor. A visszarakás az Add (a végére) vagy az Insert (adott helyre). Ellenőrizni kell, hogy a lap nincs-e már a gyűjteményben (Contains).

Form1.csC#
private void chkHalado_CheckedChanged(object sender, EventArgs e)
{
if (chkHalado.Checked)
{
if (!tc.TabPages.Contains(tabHalado))
{
tc.TabPages.Add(tabHalado);
}
}
else
{
tc.TabPages.Remove(tabHalado); // a hivatkozás megmarad a tabHalado mezőben
}
}

4. Késleltetett betöltés ellenőrzése

Szimuláld a lassú betöltést a tabStatisztika lapon (Thread.Sleep(1500) a betöltő metódusban), és igazold, hogy a betöltés csak egyszer fut. Hogyan jelzed a felhasználónak a várakozást?

Megoldás megnyitása

A betöltés előtt a kurzor WaitCursor, utána visszaáll. A jelző biztosítja az egyszeri futást. (A lassú művelet természetesen lefagyasztja az ablakot, ezt a 11. fejezet oldja meg.)

Form1.csC#
private bool statBetoltve = false;

private void tc_Selected(object sender, TabControlEventArgs e)
{
if (e.TabPage != tabStatisztika || statBetoltve)
{
return;
}

this.Cursor = Cursors.WaitCursor;

try
{
System.Threading.Thread.Sleep(1500); // a lassú betöltés szimulációja
lblStat.Text = "Betöltve: " + DateTime.Now.ToString("HH:mm:ss");
statBetoltve = true;
}
finally
{
this.Cursor = Cursors.Default;
}
}

Kihívás szint

5. Ctrl+1 … Ctrl+9 gyorsbillentyűk

A Ctrl+1 az első, a Ctrl+2 a második lapot választja ki (legfeljebb a kilencedikig). Hol kezeled, és mi történik, ha nincs annyi lap?

Megoldás megnyitása

A ProcessCmdKey metódusban (5.4 lecke). A Keys.D1 + i a számbillentyűket adja. Ha a sorszám túl nagy, a billentyűt nem nyeljük el (a többi kezelő kaphat esélyt), vagy figyelmen kívül hagyjuk.

Form1.csC#
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
for (int i = 0; i < 9; i++)
{
Keys billentyu = Keys.Control | (Keys)((int)Keys.D1 + i);

if (keyData == billentyu)
{
if (i < tc.TabCount)
{
tc.SelectedIndex = i;
}

return true;
}
}

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

6. Kijelölt lap kiemelése színnel

Saját rajzolással a kiválasztott fül háttere legyen lila, a szövege fehér; a többi fül világosszürke, sötét szöveggel. Mire kell figyelni a rajzolás végén?

Megoldás megnyitása

A DrawItem eseményben az e.State jelzi a kiválasztott fület. A kiválasztás változásakor a fülek újrarajzolása automatikus, de a lap szövegének módosításánál a tc.Invalidate() hívás kell.

Form1.csC#
private void tc_DrawItem(object sender, DrawItemEventArgs e)
{
bool kivalasztott = (e.State & DrawItemState.Selected) == DrawItemState.Selected;

Color hatter = kivalasztott ? Color.FromArgb(104, 64, 165) : Color.FromArgb(238, 238, 242);
Color szoveg = kivalasztott ? Color.White : Color.FromArgb(40, 40, 50);

using (SolidBrush ecset = new SolidBrush(hatter))
{
e.Graphics.FillRectangle(ecset, e.Bounds);
}

TextRenderer.DrawText(e.Graphics, tc.TabPages[e.Index].Text, e.Font, e.Bounds, szoveg,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
}

Gyors önellenőrzés

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

1. Hova kerülnek a vezérlők a TabControlban?

A TabPage lapokra: azok a lapok Controls gyűjteményében vannak, nem a TabControlban.

2. Hogyan rejtesz el egy lapot?

El kell távolítani a TabPages gyűjteményből (Remove), és a hivatkozást megőrizni a visszarakáshoz. A TabPage.Visible nem rejti el a fület.

3. Mi a különbség a Selecting és a Deselecting között?

A Deselecting a régi lap elhagyását, a Selecting az új lap megnyitását ellenőrzi; mindkettő megszakítható (e.Cancel).

4. Mi a SelectedTab értéke üres TabControlnál?

null (és a SelectedIndex -1), ezért mindig ellenőrizd.

5. Miért kell a Dispose lap eltávolítása után?

A Remove csak kiveszi a lapot, a lap és a vezérlői nem szabadulnak fel; sok dinamikus lapnál szivárgás keletkezhet.

6. Hogyan töltöd a lap tartalmát csak az első megnyitáskor?

A Selected eseményben, egy jelzővel (vagy HashSet-tel), amely megjegyzi, hogy a lapot betöltötted-e.

7. Mit kell rajzolnod az OwnerDrawFixed módban?

Mindent: a fül hátterét és a feliratát is a DrawItem eseményben. A rendszer nem rajzol semmit helyetted.

Mit tudsz már?

  • Többlapos felületet építesz TabControl és TabPage segítségével.
  • Kezeled a lapváltás eseményeit, és ellenőrzéssel megakadályozod a váltást.
  • Lapokat adsz hozzá, rejtesz el és távolítasz el kódból, a Dispose alkalmazásával.
  • Késleltetve töltöd be a lapok tartalmát, és saját rajzolású füleket készítesz.
  • Felismered a gyakori hibákat (Visible, null lap, végtelen Selecting).

Következő lecke: TabIndex, fókusz és billentyűzetes navigáció

Egér nélkül is használható felület: a tabulátorsorrend, a fókusz kezelése és a gyorsbillentyűk.