Grafikus C# – Windows Forms tanfolyam › 8.10 lecke Windows Forms · 8.10 · záró mini projekt A 8. fejezet záró projektje egy iskolai eszközleltár: számítógépek, padok, labdák nyilvántartása. Egyesíted mindazt, amit a fejezetben tanultál: a Mini projekt: táblázatos nyilvántartás
DataGridView-t adatobjektumokkal, a keresést, szűrést és rendezést (8.7), az ellenőrzött szerkesztést (8.8), a CSV-mentést és -betöltést (8.9), az összesítést, a helyi menüt és egy jelentés-ablakot (7. fejezet). Ez az a program-típus, amellyel az iskolai és a hobbi-nyilvántartásaid is felépíthetők.
A projekt végére
- táblázatos nyilvántartást tervezel és építesz adatobjektumokkal (
Eszkoz) ésDataGridView-val; - a főlistát és a megjelenített (szűrt, rendezett) listát szétválasztod;
- keresést, szűrést, rendezést és ellenőrzött szerkesztést kapcsolsz össze;
- CSV-be mentesz és betöltesz hibatűrően, mentetlen változtatás kezeléssel;
- összesítést (
Sum,GroupBy) és jelentés-ablakot készítesz; - végigteszteled és hozzáférhetővé teszed az alkalmazást.
Mielőtt elkezded
Ehhez a projekthez a 8.1–8.9 lecke szükséges (különösen a 8.7, 8.8 és 8.9), valamint a 7.2 (helyi menü), 7.3 (állapotsor), 7.4 (MessageBox) és 7.8 (fájlválasztók) lecke. Új Windows Forms projektben vedd fel a Settings fájlba az UtolsoMappa (string, User) beállítást.
Követelmények és pontozás
| Rész | Követelmény | Pont |
|---|---|---|
| Adat és megjelenítés (20) | Eszkoz osztály: Leltarszam, Nev, Kategoria, Helyiseg, Mennyiseg, Egysegar, Vasarlas, Allapot és számított Ertek (mennyiség × egységár). | 8 |
DataGridView kézi oszlopokkal (a kategória és az állapot ComboBox-oszlop, az érték írásvédett, ezres tagolással); a főlista (List) és a megjelenített lista (BindingList) külön; a Selejt sor szürkítve és szöveggel jelölve. | 12 | |
| Keresés, szűrés, rendezés (20) | Szöveges keresés névben és helyiségben, ékezet- és kisbetű-független. | 8 |
Kategória- és állapotszűrő (ComboBox, első elem Összes), Szűrők törlése gomb, késleltetett szűrés (Timer). | 6 | |
| Rendezés fejlécre kattintva (irányváltással, nyíllal, magyar ábécé szerint); a szűrés megtartja a rendezést. | 6 | |
| Szerkesztés (20) | Cellaszerkesztés ellenőrzéssel (CellValidating, ErrorText): név kötelező, mennyiség 0–9999, ár ≥ 0, dátum érvényes és nem jövőbeli; DataError saját üzenettel; az érték és az összesítés frissül. | 12 |
Új (automatikus leltári szám L-0001 alakban), Duplikálás, Törlés (megerősítéssel), helyi menü (Állapot almenüvel). | 8 | |
| Fájlkezelés (20) | CSV mentés és betöltés (pontosvessző, BOM, InvariantCulture, idézőjelezés); hibás sorok jelentése, adatvesztés nélkül; biztonságos mentés (.tmp, .bak). | 14 |
Mentetlen változtatás jelzése (* a címben) és megerősítés Új, Megnyitás, Kilépés előtt; utolsó mappa a Settings-ben. | 6 | |
| Összesítés és ablakok (10) | Állapotsor: 12 / 40 tétel · Összérték: 1 250 000 Ft · Selejt: 3; Jelentés ablak (modális): kategóriánként darab és érték (GroupBy). | 10 |
| Minőség (10) | Szabályok egy helyen, nincs ismétlődő kód; billentyűzetes használat és AccessibleName; hibák jellel és szöveggel; hibatűrés (üres fájl, hiányzó mappa). | 10 |
Összesen: 100 pont.
Felületterv
| Terület | Vezérlők |
|---|---|
Menü (menu) | Fájl: Új, Megnyitás..., Mentés, Mentés másként..., Kilépés; Szerkesztés: Új eszköz (Ctrl+Insert), Duplikálás, Törlés (Delete); Nézet: Szűrők törlése; Jelentés...; Súgó → Névjegy. |
Szűrősor (felül, FlowLayoutPanel) | txtKeres (Keresés névben, helyiségben), cmbKat, cmbAllapot, btnSzuroTorol. |
Táblázat (Dock = Fill, leghátul) | dgv: colLeltar (írásvédett), colNev, colKat (ComboBox), colHely, colMenny, colAr, colDatum, colAllapot (ComboBox), colErtek (írásvédett); cmsSor helyi menü. |
| Gombok | btnUj, btnDuplikal, btnTorol, btnJelentes. |
Állapotsor (ss) | tsslUzenet (Spring), tsslOsszes (darab és érték), tsslModosult. |
| Időzítő | tmrSzur (300 ms), tmrUzenet (4000 ms). |
Gondold végig!
Miért kell két lista (adatok a főlista, nezet a megjelenített), és nem elég a rácsra kötött listából törölni a szűrt sorokat?
Egy lehetséges válasz
Ha a szűrés a rácsra kötött listából eltávolítaná a nem egyező sorokat, azok véglegesen elvesznének (a szűrő törlése után sem lennének meg). A főlista őrzi az összes adatot, a megjelenített lista pedig mindig a főlistából épül újra a szűrő és a rendezés alapján. A szerkesztés mindkét listában ugyanazokat az objektumokat módosítja (hivatkozás típus), ezért nem kell másolni.
Építés lépésenként
1. lépés: adatmodell és tesztadat
Feladat: Eszkoz osztály a számított Ertek tulajdonsággal; kódból generált 15–20 tesztadat (különböző kategóriák, helyiségek, állapotok, ékezetes nevek).
Támpont: 8.6 lecke (tulajdonságok); a leltári szám automatikusan generálható ("L-" + sorszám.ToString("0000")).
2. lépés: felület és kötés
Feladat: a felületterv megépítése; AutoGenerateColumns = false, kézi oszlopok (DataPropertyName); ComboBox-oszlopok; a főlista és a BindingList kötése.
Támpont: 8.3–8.5 lecke; az érték oszlop ReadOnly, DefaultCellStyle.Format = "N0".
3. lépés: keresés, szűrés, rendezés
Feladat: Szur() metódus a főlistából (LINQ, Egyszerusit); a Rendez() a fejlécre kattintásra; a késleltetett szűrés.
Támpont: 8.7 lecke: Where, ToList, List.Sort, SortGlyphDirection.
4. lépés: szerkesztés és ellenőrzés
Feladat: CellaHiba szabályok, CellValidating, CellEndEdit (érték és összesítés frissítése), DataError.
Támpont: 8.8 lecke; az érték számított oszlop: dgv.InvalidateRow(e.RowIndex).
5. lépés: új, duplikálás, törlés, helyi menü
Feladat: az új eszköz a főlistába kerül (a szűrők törlése után kijelölve); a törlés megerősítéssel; a helyi menü Állapot almenüje közös kezelővel.
Támpont: 7.2 és 7.4 lecke (MouseDown a kijelöléshez, Button2).
6. lépés: összesítés
Feladat: az állapotsor tételszáma, összértéke (Sum) és selejt-darabszáma, a szűrt és az összes adatra egyaránt.
Támpont: 7.3 lecke; LINQ Sum, Count.
7. lépés: fájlkezelés
Feladat: CSV mentés és betöltés (CsvMezo, CsvSor), hibás sorok jelentése, adatcsere csak siker után; mentetlen változtatás kezelése; utolsó mappa a Settings-ben.
Támpont: 8.9 lecke.
8. lépés: jelentés-ablak és utómunka
Feladat: JelentesForm (modális): kategóriánként darab és érték (GroupBy); AccessibleName, billentyűzetes használat, tesztlista végigjátszása.
Támpont: 7.5–7.7 lecke (modális ablak, adatátadás konstruktorral), 6.9 lecke.
Mintamegoldás
Előbb próbáld meg önállóan!
A mintamegoldás az ellenőrzésre való. A felület a tiéd; itt a logikát adom meg. A CsvMezo és a CsvSor az 8.9 leckéből való (vedd át), az Egyszerusit a 8.7 leckéből.
Megoldás: Eszkoz.cs és JelentesForm.cs
public class Eszkoz
{
public string Leltarszam { get; set; } = "";
public string Nev { get; set; } = "";
public string Kategoria { get; set; } = "Egyéb";
public string Helyiseg { get; set; } = "";
public int Mennyiseg { get; set; } = 1;
public decimal Egysegar { get; set; }
public DateTime Vasarlas { get; set; } = DateTime.Today;
public string Allapot { get; set; } = "Jó";
public decimal Ertek { get { return Mennyiseg * Egysegar; } } // számított, írásvédett
public Eszkoz Masol()
{
return (Eszkoz)this.MemberwiseClone();
}
}
public class KategoriaOsszeg
{
public string Kategoria { get; set; } = "";
public int Tetel { get; set; }
public int Darab { get; set; }
public decimal Ertek { get; set; }
}
public partial class JelentesForm : Form
{
public JelentesForm(List<Eszkoz> adatok)
{
InitializeComponent();
dgvJelentes.AutoGenerateColumns = true;
dgvJelentes.ReadOnly = true;
dgvJelentes.DataSource = adatok
.GroupBy(e => e.Kategoria)
.Select(g => new KategoriaOsszeg { Kategoria = g.Key, Tetel = g.Count(), Darab = g.Sum(e => e.Mennyiseg), Ertek = g.Sum(e => e.Ertek) })
.OrderByDescending(k => k.Ertek)
.ToList();
lblVegosszeg.Text = "Összesen: " + adatok.Sum(e => e.Ertek).ToString("N0") + " Ft";
}
}Megoldás: Form1.cs (adat, szűrés, rendezés)
private List<Eszkoz> adatok = new List<Eszkoz>(); // a főlista: az összes adat
private BindingList<Eszkoz> nezet = new BindingList<Eszkoz>(); // a rácsra kötött, szűrt és rendezett lista
private string rendezOszlop = "Leltarszam";
private ListSortDirection irany = ListSortDirection.Ascending;
private string fajl = "";
private bool modosult = false;
private readonly string[] kategoriak = { "Informatika", "Bútor", "Sport", "Egyéb" };
private readonly string[] allapotok = { "Jó", "Javítandó", "Selejt" };
private void Form1_Load(object sender, EventArgs e)
{
colKat.Items.AddRange(kategoriak);
colAllapot.Items.AddRange(allapotok);
cmbKat.Items.Add("Összes kategória");
cmbKat.Items.AddRange(kategoriak);
cmbKat.SelectedIndex = 0;
cmbAllapot.Items.Add("Minden állapot");
cmbAllapot.Items.AddRange(allapotok);
cmbAllapot.SelectedIndex = 0;
dgv.AutoGenerateColumns = false;
dgv.DataSource = nezet;
string[] nevek = { "Laptop", "Projektor", "Pad", "Tábla", "Focilabda", "Íróasztal", "Nyomtató", "Szék" };
Random r = new Random(11);
for (int i = 0; i < 18; i++)
{
adatok.Add(new Eszkoz
{
Leltarszam = "L-" + (i + 1).ToString("0000"),
Nev = nevek[i % nevek.Length] + " " + (i / nevek.Length + 1),
Kategoria = kategoriak[i % 4],
Helyiseg = "Terem " + (100 + r.Next(1, 8)),
Mennyiseg = r.Next(1, 30),
Egysegar = r.Next(2, 300) * 1000,
Vasarlas = DateTime.Today.AddDays(-r.Next(30, 2000)),
Allapot = allapotok[r.Next(0, 3)]
});
}
Szur();
}
private static string Egyszerusit(string szoveg)
{
string bontott = (szoveg ?? "").Normalize(NormalizationForm.FormD);
StringBuilder epito = new StringBuilder();
foreach (char c in bontott)
{
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) { epito.Append(c); }
}
return epito.ToString().Normalize(NormalizationForm.FormC).ToLowerInvariant();
}
private void Szur()
{
string keres = Egyszerusit(txtKeres.Text.Trim());
IEnumerable<Eszkoz> q = adatok.Where(x => keres.Length == 0 || Egyszerusit(x.Nev + " " + x.Helyiseg).Contains(keres));
if (cmbKat.SelectedIndex > 0) { q = q.Where(x => x.Kategoria == cmbKat.Text); }
if (cmbAllapot.SelectedIndex > 0) { q = q.Where(x => x.Allapot == cmbAllapot.Text); }
List<Eszkoz> lista = q.ToList();
Rendez(lista);
nezet.RaiseListChangedEvents = false;
nezet.Clear();
foreach (Eszkoz x in lista) { nezet.Add(x); }
nezet.RaiseListChangedEvents = true;
nezet.ResetBindings();
foreach (DataGridViewColumn o in dgv.Columns)
{
o.HeaderCell.SortGlyphDirection = o.DataPropertyName == rendezOszlop ? (irany == ListSortDirection.Ascending ? SortOrder.Ascending : SortOrder.Descending) : SortOrder.None;
}
Osszegzes();
}
private void Rendez(List<Eszkoz> lista)
{
int e = irany == ListSortDirection.Ascending ? 1 : -1;
switch (rendezOszlop)
{
case "Nev": lista.Sort((a, b) => e * string.Compare(a.Nev, b.Nev, StringComparison.CurrentCultureIgnoreCase)); break;
case "Kategoria": lista.Sort((a, b) => e * string.Compare(a.Kategoria, b.Kategoria, StringComparison.CurrentCultureIgnoreCase)); break;
case "Helyiseg": lista.Sort((a, b) => e * string.Compare(a.Helyiseg, b.Helyiseg, StringComparison.CurrentCultureIgnoreCase)); break;
case "Mennyiseg": lista.Sort((a, b) => e * a.Mennyiseg.CompareTo(b.Mennyiseg)); break;
case "Egysegar": lista.Sort((a, b) => e * a.Egysegar.CompareTo(b.Egysegar)); break;
case "Vasarlas": lista.Sort((a, b) => e * a.Vasarlas.CompareTo(b.Vasarlas)); break;
case "Allapot": lista.Sort((a, b) => e * string.Compare(a.Allapot, b.Allapot, StringComparison.CurrentCultureIgnoreCase)); break;
case "Ertek": lista.Sort((a, b) => e * a.Ertek.CompareTo(b.Ertek)); break;
default: lista.Sort((a, b) => e * string.Compare(a.Leltarszam, b.Leltarszam, StringComparison.Ordinal)); break;
}
}
private void dgv_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
string oszlop = dgv.Columns[e.ColumnIndex].DataPropertyName;
if (oszlop == rendezOszlop)
{
irany = irany == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
}
else
{
rendezOszlop = oszlop;
irany = ListSortDirection.Ascending;
}
Szur();
}
private void Szuro_Valtozott(object sender, EventArgs e)
{
tmrSzur.Stop();
tmrSzur.Start();
}
private void tmrSzur_Tick(object sender, EventArgs e)
{
tmrSzur.Stop();
Szur();
}
private void btnSzuroTorol_Click(object sender, EventArgs e)
{
txtKeres.Clear();
cmbKat.SelectedIndex = 0;
cmbAllapot.SelectedIndex = 0;
Szur();
}
private void Osszegzes()
{
decimal ertek = nezet.Sum(x => x.Ertek);
int selejt = adatok.Count(x => x.Allapot == "Selejt");
tsslOsszes.Text = nezet.Count + " / " + adatok.Count + " tétel · Összérték: " + ertek.ToString("N0") + " Ft · Selejt: " + selejt;
}Megoldás: Form1.cs (szerkesztés, ellenőrzés, műveletek)
private string CellaHiba(string oszlop, string szoveg)
{
szoveg = (szoveg ?? "").Trim();
switch (oszlop)
{
case "colNev":
return szoveg.Length == 0 ? "A név megadása kötelező." : (szoveg.Length > 50 ? "A név legfeljebb 50 karakter lehet." : "");
case "colMenny":
int db;
if (!int.TryParse(szoveg, NumberStyles.Integer, CultureInfo.CurrentCulture, out db)) { return "A mennyiség egész szám legyen (Esc: visszavonás)."; }
return db < 0 || db > 9999 ? "A mennyiség 0 és 9999 között lehet." : "";
case "colAr":
decimal ar;
if (!decimal.TryParse(szoveg, NumberStyles.Number, CultureInfo.CurrentCulture, out ar)) { return "Az egységár szám legyen (Esc: visszavonás)."; }
return ar < 0 ? "Az egységár nem lehet negatív." : "";
case "colDatum":
DateTime d;
if (!DateTime.TryParse(szoveg, CultureInfo.CurrentCulture, DateTimeStyles.None, out d)) { return "A dátum alakja éééé. hh. nn. legyen (Esc: visszavonás)."; }
return d.Date > DateTime.Today ? "A vásárlás dátuma nem lehet a jövőben." : "";
}
return "";
}
private void dgv_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
string oszlop = dgv.Columns[e.ColumnIndex].Name;
string hiba = CellaHiba(oszlop, Convert.ToString(e.FormattedValue));
dgv.Rows[e.RowIndex].Cells[e.ColumnIndex].ErrorText = hiba;
if (hiba.Length > 0 && oszlop != "colNev")
{
e.Cancel = true;
}
}
private void dgv_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
dgv.Rows[e.RowIndex].Cells[e.ColumnIndex].ErrorText = "";
dgv.InvalidateRow(e.RowIndex); // a számított érték frissítése
Modosult();
Osszegzes();
}
private void dgv_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
tsslUzenet.Text = "Érvénytelen érték a(z) " + (e.RowIndex + 1) + ". sorban. Javítsd, vagy nyomj Esc-et.";
e.ThrowException = false;
}
private void dgv_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
Eszkoz x = e.RowIndex >= 0 && e.RowIndex < nezet.Count ? nezet[e.RowIndex] : null;
if (x != null && x.Allapot == "Selejt")
{
e.CellStyle.ForeColor = Color.DimGray; // nem csak színnel: az Állapot oszlop szövege is „Selejt”
}
}
private void Modosult()
{
if (!modosult) { modosult = true; CimFrissit(); }
}
private void CimFrissit()
{
string nev = fajl.Length == 0 ? "Névtelen" : Path.GetFileName(fajl);
this.Text = nev + (modosult ? "*" : "") + " – Eszközleltár";
tsslModosult.Text = modosult ? "● Módosítva" : "✔ Mentve";
}
private string KovetkezoSzam()
{
int max = 0;
foreach (Eszkoz x in adatok)
{
int n;
if (x.Leltarszam.StartsWith("L-") && int.TryParse(x.Leltarszam.Substring(2), out n) && n > max) { max = n; }
}
return "L-" + (max + 1).ToString("0000");
}
private void KijelolEszkoz(Eszkoz x)
{
int i = nezet.IndexOf(x);
if (i >= 0)
{
dgv.ClearSelection();
dgv.CurrentCell = dgv.Rows[i].Cells["colNev"];
}
}
private void btnUj_Click(object sender, EventArgs e)
{
Eszkoz uj = new Eszkoz { Leltarszam = KovetkezoSzam(), Nev = "Új eszköz" };
adatok.Add(uj);
btnSzuroTorol.PerformClick(); // az új sor biztosan látszik
KijelolEszkoz(uj);
Modosult();
dgv.BeginEdit(true);
}
private Eszkoz Aktualis()
{
return dgv.CurrentRow == null ? null : dgv.CurrentRow.DataBoundItem as Eszkoz;
}
private void btnDuplikal_Click(object sender, EventArgs e)
{
Eszkoz x = Aktualis();
if (x == null) { return; }
Eszkoz m = x.Masol();
m.Leltarszam = KovetkezoSzam();
adatok.Add(m);
Szur();
KijelolEszkoz(m);
Modosult();
}
private void btnTorol_Click(object sender, EventArgs e)
{
List<Eszkoz> torlendo = new List<Eszkoz>();
foreach (DataGridViewRow r in dgv.SelectedRows)
{
Eszkoz x = r.DataBoundItem as Eszkoz;
if (x != null) { torlendo.Add(x); }
}
if (torlendo.Count == 0) { return; }
if (MessageBox.Show(this, torlendo.Count + " tétel törlése. Biztosan?", "Törlés", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes)
{
foreach (Eszkoz x in torlendo) { adatok.Remove(x); }
Szur();
Modosult();
}
}
private void Allapot_Click(object sender, EventArgs e) // a helyi menü Állapot almenüje: a Tag az állapot
{
string uj = ((ToolStripMenuItem)sender).Tag.ToString();
foreach (DataGridViewRow r in dgv.SelectedRows)
{
Eszkoz x = r.DataBoundItem as Eszkoz;
if (x != null) { x.Allapot = uj; }
}
Szur();
Modosult();
}
private void dgv_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
DataGridView.HitTestInfo h = dgv.HitTest(e.X, e.Y);
if (h.RowIndex >= 0 && !dgv.Rows[h.RowIndex].Selected)
{
dgv.ClearSelection();
dgv.Rows[h.RowIndex].Selected = true;
}
}
}
private void btnJelentes_Click(object sender, EventArgs e)
{
using (JelentesForm ablak = new JelentesForm(adatok))
{
ablak.ShowDialog(this);
}
}Megoldás: Form1.cs (CSV és fájlkezelés)
private const string Fejlec = "Leltarszam;Nev;Kategoria;Helyiseg;Mennyiseg;Egysegar;Vasarlas;Allapot";
private void CsvMent(string utvonal)
{
List<string> sorok = new List<string> { Fejlec };
foreach (Eszkoz x in adatok)
{
sorok.Add(CsvMezo(x.Leltarszam) + ";" + CsvMezo(x.Nev) + ";" + CsvMezo(x.Kategoria) + ";" + CsvMezo(x.Helyiseg) + ";"
+ x.Mennyiseg.ToString(CultureInfo.InvariantCulture) + ";" + x.Egysegar.ToString(CultureInfo.InvariantCulture) + ";"
+ x.Vasarlas.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ";" + CsvMezo(x.Allapot));
}
File.WriteAllLines(utvonal, sorok, new UTF8Encoding(true));
}
private List<Eszkoz> CsvBeolvas(string utvonal, List<string> hibak)
{
List<Eszkoz> uj = new List<Eszkoz>();
string[] sorok = File.ReadAllLines(utvonal, Encoding.UTF8);
if (sorok.Length == 0 || sorok[0].Trim() != Fejlec)
{
hibak.Add("A fájl nem eszközleltár (a fejléc nem egyezik).");
return uj;
}
for (int i = 1; i < sorok.Length; i++)
{
if (sorok[i].Trim().Length == 0) { continue; }
List<string> m = CsvSor(sorok[i]);
int db;
decimal ar;
DateTime datum;
if (m.Count != 8) { hibak.Add((i + 1) + ". sor: 8 mező kellene, " + m.Count + " van."); }
else if (!int.TryParse(m[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out db) || db < 0) { hibak.Add((i + 1) + ". sor: érvénytelen mennyiség."); }
else if (!decimal.TryParse(m[5], NumberStyles.Number, CultureInfo.InvariantCulture, out ar) || ar < 0) { hibak.Add((i + 1) + ". sor: érvénytelen egységár."); }
else if (!DateTime.TryParseExact(m[6], "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out datum)) { hibak.Add((i + 1) + ". sor: érvénytelen dátum."); }
else
{
uj.Add(new Eszkoz { Leltarszam = m[0].Trim(), Nev = m[1].Trim(), Kategoria = m[2].Trim(), Helyiseg = m[3].Trim(), Mennyiseg = db, Egysegar = ar, Vasarlas = datum, Allapot = m[7].Trim() });
}
}
return uj;
}
private bool Ment(bool maskent)
{
string ut = fajl;
if (maskent || ut.Length == 0)
{
sfd.FileName = ut.Length == 0 ? "leltar" : Path.GetFileName(ut);
if (sfd.ShowDialog(this) != DialogResult.OK) { return false; }
ut = sfd.FileName;
}
try
{
string ideiglenes = ut + ".tmp";
CsvMent(ideiglenes);
if (File.Exists(ut)) { File.Copy(ut, ut + ".bak", true); File.Delete(ut); }
File.Move(ideiglenes, ut);
fajl = ut;
modosult = false;
CimFrissit();
Properties.Settings.Default.UtolsoMappa = Path.GetDirectoryName(ut);
Properties.Settings.Default.Save();
tsslUzenet.Text = "Mentve: " + Path.GetFileName(ut);
return true;
}
catch (Exception ex)
{
MessageBox.Show(this, "A mentés nem sikerült." + Environment.NewLine + ex.Message, "Mentés", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
private bool Megnyit(string utvonal)
{
List<string> hibak = new List<string>();
List<Eszkoz> uj;
try { uj = CsvBeolvas(utvonal, hibak); }
catch (IOException ex)
{
MessageBox.Show(this, "A fájl nem olvasható: " + ex.Message, "Megnyitás", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if (hibak.Count > 0)
{
string szoveg = hibak.Count + " hibás sor van, az első: " + hibak[0] + Environment.NewLine + "A hibás sorok nélkül (" + uj.Count + " tétel) betöltsem?";
if (uj.Count == 0 || MessageBox.Show(this, szoveg, "Megnyitás", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes)
{
return false;
}
}
adatok = uj; // csak a teljes feldolgozás után cserélünk
fajl = utvonal;
modosult = false;
CimFrissit();
Szur();
return true;
}
private bool Folytathat()
{
if (!modosult) { return true; }
switch (MessageBox.Show(this, "Mented a változtatásokat?", "Eszközleltár", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning))
{
case DialogResult.Yes: return Ment(false);
case DialogResult.No: return true;
default: return false;
}
}
private void miUj_Click(object sender, EventArgs e)
{
if (Folytathat())
{
adatok = new List<Eszkoz>();
fajl = "";
modosult = false;
CimFrissit();
Szur();
}
}
private void miMegnyit_Click(object sender, EventArgs e)
{
if (Folytathat() && ofd.ShowDialog(this) == DialogResult.OK)
{
Megnyit(ofd.FileName);
}
}
private void miMent_Click(object sender, EventArgs e) { Ment(false); }
private void miMentMaskent_Click(object sender, EventArgs e) { Ment(true); }
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing && !Folytathat())
{
e.Cancel = true;
}
}Mit tegyél ide: az eszközleltár a szűrősorral, a táblázattal (rendezési nyíl, hibás cella ikonja), az állapotsorral és a jelentés-ablakkal.Javasolt fájlnév: winforms-8-10-kesz.webp · Alt szöveg: Az eszközleltár program és a jelentés
Tesztlista
| # | Teszt | Várt eredmény |
|---|---|---|
| 1 | Indítás: a tesztadat betöltődik. | 18 tétel, az állapotsor tételszám és összérték helyes. |
| 2 | Keresés: fuzet, FÜZET, terem 103. | Ékezet- és kisbetű-független találat névben és helyiségben. |
| 3 | Kategória- és állapotszűrő együtt a kereséssel; Szűrők törlése. | Az állapotsor 12 / 18 tétel alakú; törlés után 18 / 18. |
| 4 | Rendezés fejlécre kattintva (kétszer), majd szűrés. | Irányváltás, nyíl, a szűrés megtartja a rendezést; a magyar ábécé szerinti sorrend. |
| 5 | Betű az Mennyiség cellába (beillesztéssel is). | Piros ikon, nem lép tovább; Esc visszavon. |
| 6 | Üres név; negatív ár; jövőbeli dátum. | Érthető hibaüzenet a cellán. |
| 7 | Mennyiség módosítása. | Az érték oszlop és az összérték azonnal frissül. |
| 8 | Új eszköz szűrt nézetben. | A szűrők törlődnek, az új sor látszik, kijelölt, szerkesztésben; a leltári szám következő sorszámú. |
| 9 | Duplikálás, majd Törlés (több sor), Nem és Igen. | A Nem nem töröl; az Igen a főlistából is töröl (szűrő törlése után sem látszik). |
| 10 | Helyi menü Állapot → Selejt több kijelölt soron; jobb kattintás nem kijelölt soron. | Az állapot változik, szürkítve és szöveggel jelölve; a jobb kattintás a sort jelöli ki. |
| 11 | Mentés CSV-be, megnyitás Excelben. | Ékezetek és oszlopok helyesek; az ár és a dátum jól látszik. |
| 12 | Mentés, majd a fájl megnyitása a programban. | Az adatok azonosak; nincs * a címben. |
| 13 | Hibás CSV (2 hibás sor, rossz mezőszám, rossz dátum). | A jelentés megmondja a sorszámot; Nem esetén az adat érintetlen. |
| 14 | Üres fájl, rossz fejléc. | Érthető üzenet, a program tovább használható. |
| 15 | Szerkesztés után Új, Megnyitás, kilépés: Igen, Nem, Mégse. | Mentés + folytatás, elvetés + folytatás, maradás. |
| 16 | Írásvédett fájlba mentés. | Hibaüzenet, az eredeti fájl épen marad, nem lép tovább. |
| 17 | Jelentés ablak. | Kategóriánként darab és érték, összesen sor; a főablak a jelentés alatt le van tiltva. |
| 18 | Billentyűzet-, nagyított betűméretes teszt (125%, 150%). | Minden elérhető, a szűrősor és a táblázat rendezett marad. |
Gyakori hibák és önértékelés
Ezeken szoktak elcsúszni
Gondold végig, nálad előfordul-e valamelyik.
1. A szűrő törlése után hiányoznak tételek
A szűrt sorokat a rácsra kötött listából töröltem. A főlista (Ok és javítás
adatok) és a megjelenített lista (nezet) különbözik. A szűrés mindig a főlistából épít új nézetet, és soha nem töröl a főlistából.
2. A törlés a nézetből töröl, a tétel visszajön
A törölt tétel a következő szűréskor újra megjelenik. A tételt a megjelenített listából töröltem. A törlés a főlistából (Ok és javítás
adatok.Remove) történjen, utána Szur().
3. Az érték oszlop nem frissül szerkesztés után
A mennyiség módosítása után az érték régi. A számított tulajdonságnak nincs értesítése (nem Ok és javítás
INotifyPropertyChanged). A CellEndEdit eseményben dgv.InvalidateRow, vagy valósítsd meg az INotifyPropertyChanged felületet (8.6 lecke).
4. A jobb kattintás rossz sort érint
A helyi menü a korábban kijelölt sorra hat. A Ok és javítás
MouseDown eseményben a HitTest alapján jelöld ki a sort (7.2 lecke), ha az még nincs kijelölve.
5. Az új sor nem látszik szűrt nézetben
Az új eszköz hozzáadása után nem jelenik meg. A szűrő kizárja az új tételt. Töröld a szűrőket (Ok és javítás
btnSzuroTorol.PerformClick()) az új tétel felvétele után, és jelöld ki.
6. A betöltés elveszi az adatot hibás fájlnál
A hibás CSV megnyitása után a lista üres. Új listába olvass, és a főlistát a sikeres feldolgozás után cseréld (Ok és javítás
adatok = uj), 8.9 lecke.
Az értékeléshez ellenőrizd az alábbi szempontokat is:
CellaHiba, Szur, Rendez); nincs ismétlődő kód.Bővítési ötletek, ha kész vagy
Ha maradt időd, próbáld ki:
- Több feltétel szerinti rendezés: Shift + fejléc-kattintás másodlagos rendezési kulccsal.
- Előzmények (visszavonás): verem a módosításokról (
Stack, 7.7 és 8.8 lecke ötlete). - Kép az eszközhöz: PictureBox az aktuális sor képével (9. fejezet).
- Nyomtatható leltárív: a szűrt lista nyomtatása előnézettel (2.15 lecke).
- Adatbázis: a CSV helyett SQLite (3. fejezet) a nagyobb adatmennyiséghez.
- Egy kód, több nézet: a szűrés és összesítés logikáját külön osztályba (12. fejezet).
Gyakorlótár: 6 további feladat
Három szinten gyakorolhatsz: alap, közepes és kihívás. A kész programot bővíted. Csak a próbálkozás után nyisd le a megoldást.
Alap szint
1. Új oszlop
Vegyél fel Megjegyzés oszlopot. Mely helyeken kell módosítanod a programot? (1) az Megoldás megnyitása
Eszkoz osztály új tulajdonsága; (2) a rács új oszlopa (DataPropertyName); (3) a Rendez új ága (ha rendezhető); (4) a CSV fejléce, mentése és betöltése (a mezőszám 9, a régi fájlok kezelése); (5) a Masol automatikusan viszi (MemberwiseClone).
2. Átlagár az állapotsorban
Az állapotsor mutassa a megjelenített tételek átlagos egységárát is. Mire kell vigyáznod üres lista esetén? Az Megoldás megnyitása
Average üres sorozatnál kivételt dob: előbb nezet.Count > 0 ellenőrzés, vagy DefaultIfEmpty(0).decimal atlag = nezet.Count == 0 ? 0 : nezet.Average(x => x.Egysegar);
Közepes szint
3. Lejárt garancia jelölése
A 5 évnél régebben vásárolt eszközök dátum-cellája legyen narancssárga, és a sor végén jelenjen meg a Régi felirat (ne csak szín). Hogyan? A Megoldás megnyitása
CellFormatting eseményben a Vásárlás oszlopnál a háttér színe; a szöveges jelzéshez egy külön Megjegyzés oszlop vagy az eszköztipp (ToolTipText).if (dgv.Columns[e.ColumnIndex].Name == "colDatum" && x != null && x.Vasarlas < DateTime.Today.AddYears(-5))
{
e.CellStyle.BackColor = Color.Moccasin;
dgv.Rows[e.RowIndex].Cells[e.ColumnIndex].ToolTipText = "Régi: 5 évnél régebben vásárolt.";
}
4. Kijelöltek összértéke
A kijelölt sorok összértéke jelenjen meg az állapotsorban, a kijelölés változásakor. Melyik eseményt használod? A Megoldás megnyitása
SelectionChanged eseményt (dgv.SelectedRows elemei).private void dgv_SelectionChanged(object sender, EventArgs e)
{
decimal ossz = 0;
foreach (DataGridViewRow r in dgv.SelectedRows)
{
Eszkoz x = r.DataBoundItem as Eszkoz;
if (x != null) { ossz += x.Ertek; }
}
tsslKijelolt.Text = dgv.SelectedRows.Count + " kijelölve: " + ossz.ToString("N0") + " Ft";
}
Kihívás szint
5. Szűrő mentése a beállításokba
A program emlékezzen az utolsó szűrőre és rendezésre ( A keresőszöveget, a kategória és az állapot szövegét, a rendezési oszlopot és irányt. Visszaállításkor ellenőrizni kell, hogy a kategória még létezik-e (Settings). Mit tárolsz, és hogyan állítod vissza biztonságosan?Megoldás megnyitása
Items.Contains), különben az Összes marad.
6. Adatok és felület szétválasztása
Tedd a szűrést, rendezést, összesítést és CSV-kezelést egy Például LeltarModell osztályba (nincs felület). Milyen metódusokat adsz neki, és hogyan használja a Form1?Megoldás megnyitása
Szur(string keres, string kat, string allapot, string oszlop, bool novekvo) (List<Eszkoz>-et ad), Osszertek(List<Eszkoz>), CsvMent, CsvBeolvas. A Form1 csak a vezérlők értékét adja át, és megjeleníti az eredményt. Így a modell ablak nélkül tesztelhető, és más felület (konzol, webes) is használhatja (12. fejezet).public class LeltarModell
{
public List<Eszkoz> Adatok = new List<Eszkoz>();
public List<Eszkoz> Szur(string keres, string kategoria, string allapot)
{
IEnumerable<Eszkoz> q = Adatok;
if (keres.Length > 0) { q = q.Where(x => (x.Nev + " " + x.Helyiseg).IndexOf(keres, StringComparison.CurrentCultureIgnoreCase) >= 0); }
if (kategoria.Length > 0) { q = q.Where(x => x.Kategoria == kategoria); }
if (allapot.Length > 0) { q = q.Where(x => x.Allapot == allapot); }
return q.ToList();
}
public decimal Osszertek(List<Eszkoz> lista)
{
return lista.Sum(x => x.Ertek);
}
}
Gyors önellenőrzés
Válaszolj fejben, és csak utána nyisd le a választ. A szűrés ne töröljön adatot: a főlista őrzi az összeset, a nézet mindig abból épül. Típushibánál (nem szám); a tartalmi hibát elég jelezni ( Új listába olvasunk, a hibákat gyűjtjük, és a főlistát csak siker után cseréljük. A tétel a következő szűréskor visszajön: a törlés a főlistából történjen.1. Miért két lista a főlista és a megjelenített lista?
2. Hogyan rendezel a magyar ábécé szerint?
string.Compare(..., StringComparison.CurrentCultureIgnoreCase) a List.Sort lambdában.3. Hogyan frissül a számított érték szerkesztéskor?
dgv.InvalidateRow a CellEndEdit eseményben (vagy INotifyPropertyChanged).4. Mikor kényszerítsd a felhasználót a cellában?
ErrorText), és mentés előtt újra ellenőrizni.5. Miért nem veszít adatot a hibás CSV betöltése?
6. Hogyan számolod a kategóriánkénti összegeket?
GroupBy(e => e.Kategoria), majd Count és Sum csoportonként.7. Miért ne a nézetből törölj?
Mit tudsz már? (a 8. fejezet végén)
- Adatobjektumokat jelenítesz meg és szerkesztesz
DataGridView-val, adatkötéssel. - Keresést, szűrést és rendezést építesz (ékezetfüggetlenül, magyar ábécé szerint).
- Cella- és sorszinten ellenőrzöl, és a szabályokat egy helyen tartod.
- CSV-be mentesz és betöltesz hibatűrően, adatvesztés nélkül.
- Összesítesz (
Sum,GroupBy), jelentés-ablakot készítesz, és teljes nyilvántartó programot építettél.
Következő fejezet: képek, rajzolás és grafika
A 8. fejezet kész. A 9. fejezetben a képek megjelenítése és a saját rajzolás (GDI+) következik: PictureBox, Paint, Graphics, egéres rajzolás.



