Grafikus C# – Windows Forms tanfolyam › 16.1 lecke Önálló gyakorlás · 16.1 lecke · összetett feladat Ez a lecke egy teljes, összetett gyakorlófeladatot old meg lépésről lépésre: egy családi kiadáskövető program SQLite adatbázissal, táblázattal, szigorú névellenőrzéssel, törléssel és módosítással terméknév alapján, személyenkénti listával, összkiadás-grafikonnal és Info menüvel. Végig két fontos szokást gyakorlunk: a program hiba nélkül forduljon le, és az adatbázis módosítás nélkül működjön másik számítógépen is.Gyakorlófeladat: Flinstones családi kalkulátor

A lecke végére
- egy feladatszöveget követelménylistává bontasz, és minden ponthoz tudod, melyik korábbi leckéből dolgozol;
- SQLite adatbázist hozol létre kódból, a program mappájában, hogy másik gépen is működjön;
- a bevitelt két helyen korlátozod: legördülő listával és kódban is;
- paraméterezett SQL-lel törölsz és módosítasz terméknév alapján;
- egy DataGridView sorára kattintva kitöltöd a beviteli mezőket;
- személyenkénti listát és összkiadás-grafikont készítesz
SUM-mal; - végigmész az ellenőrzőlistán, mielőtt késznek tekintenéd a munkát.
Mielőtt elkezded
Ehhez a feladathoz a 3. fejezet leckéire támaszkodsz, különösen a 3.2 DB.cs és adatbázis-kapcsolat, a 3.7 paraméterezett lekérdezések és a 3.8 DataAdapter és DataTable leckékre, a 3.12 összesítésekre, valamint a 9.9 és a 9.11 grafikonos leckére. Az 1–15. fejezet anyagának ismeretében érdemes nekiállni, ezért áll ez a feladat a tanmenet végén.
A feladat
A Flinstones család új, kőkorszaki autóra gyűjt, ezért családi kiadáskövető programot kér. A program azt tárolja, hogy a négy családtag (Frédi, Béni, Irma, Vilma) melyik vásárlásnál mennyit költött, a három kategória (Élelmiszer, Háztartás, Hobbi) valamelyikében. A program Windows Forms App (.NET Framework) legyen, SQLite adatbázissal.
Két alapkövetelmény
A program legyen hiba nélkül leforduló, az adatbázisnak pedig módosítás nélkül elérhetőnek kell lennie más számítógépen is. Ha valamelyik nem teljesül, a feladat nem tekinthető késznek. Ezért ez a lecke minden lépésnél ezt a kettőt is ellenőrzi.
| # | Követelmény | Honnan tudod? |
|---|---|---|
| 1 | Az adatbázis neve FsDB, a mezők: Csaladtag, Aru, Ar, Tipus | 3.1, 3.2 |
| 2 | Csak a megadott családtag- és kategórianevek fogadhatók el | 3.7, ComboBox (1. fejezet) |
| 3 | Minden adat látszik egy DataGridView-ban | 3.4, 3.8 |
| 4 | Törlés a termék neve alapján | 3.6 |
| 5 | A termék vételárának módosítása a termék neve alapján | 3.6 |
| 6 | Személyenként kilistázni a vásárlásokat | 3.5 |
| 7 | Egy családtag kiválasztása után az összkiadása grafikonon | 3.12 (SUM, COALESCE), 9.9, 9.11 |
| 8 | Info menüpont a program készítőjének nevével | 2.9 |
| 9 | Hiba nélkül forduljon, és másik gépen is működjön | 14.7 |
A kiinduló adatok, amelyekkel a program indul (a feladat mintatáblája):
| Csaladtag | Aru | Ar | Tipus |
|---|---|---|---|
| Vilma | téliszalámi | 600 | Élelmiszer |
| Irma | mosogatószer | 1100 | Háztartás |
| Béni | adatkábel | 200 | Hobbi |
| Frédi | sportújság | 700 | Hobbi |
| Vilma | kenyér | 250 | Élelmiszer |
| Frédi | olaj | 500 | Élelmiszer |
Jósolj!
A feladat azt kéri, hogy a törlés és a módosítás a termék neve alapján történjen. Mi megy félre, ha két különböző családtag is felvisz egy „kenyér” nevű terméket?
Jóslat ellenőrzése
A név alapján törlés mindkét sort törölné, a módosítás mindkettőnek átírná az árát, így nem tudnánk, melyiket szerettük volna. Ezért a megoldásban az Aru mező egyedi (UNIQUE): ugyanazt a nevet kétszer nem lehet felvenni, és a program érthető üzenetet ad. Ezt a döntést a 2. lépésben indokoljuk részletesen.
Tervezés: mielőtt kódot írsz
Nem az nyer, aki az első percben elkezd gépelni, hanem aki 10 perc tervezéssel elkerüli az átírást. Három dolgot döntünk el előre: az adatok szerkezetét, a felület elrendezését és azt, hogyan lesz a program hordozható.
Az adatmodell: egy tábla elég
A feladat egyetlen fogalomról szól: a vásárlásról. Egy vásárlásnak négy tulajdonsága van, ezért egy Vasarlas tábla elegendő.
| Mező | Típus | Megjegyzés |
|---|---|---|
Csaladtag | TEXT, kötelező | csak Frédi, Béni, Irma vagy Vilma lehet |
Aru | TEXT, kötelező, egyedi | a törlés és a módosítás ez alapján keres |
Ar | INTEGER, kötelező | forintban, egész szám |
Tipus | TEXT, kötelező | csak Élelmiszer, Háztartás vagy Hobbi lehet |
A felület elrendezése
Rajzold le papíron, mielőtt a Designerhez vagy a kódhoz nyúlsz. Három sávot használunk: felül a bevitel, középen az összes adat, alul egymás mellett a személyenkénti lista és a grafikon.
┌─ Info | Kilépés ──────────────────────────────────────────┐
│ Családtag [▼] Áru [________] Ár [_____] Típus [▼] │
│ [Felvétel] [Ár módosítása] [Törlés] (üzenetsor) │
├───────────────────────────────────────────────────────────┤
│ DataGridView: az összes vásárlás (sorra kattintás → mezők) │
├─────────────────────────────┬─────────────────────────────┤
│ Személyenkénti vásárlások │ Családtag összkiadása │
│ [▼] [Listázás] összesen: │ [▼] [Grafikon] │
│ DataGridView │ Chart │
└─────────────────────────────┴─────────────────────────────┘A bevitel védelme két rétegben
A feladat szerint a program csak a megadott neveket és kategóriákat fogadja el. Ezt két helyen biztosítjuk:
- A felületen: a családtag és a típus
DropDownListstílusú ComboBox, így a felhasználó be sem tud gépelni más értéket. - A kódban: a felvétel előtt a program összeveti a kiválasztott értéket a megengedett nevek tömbjével. A felületi védelem megkerülhető (például később valaki átírja a vezérlőt), a kódbeli ellenőrzés nem.
Hordozhatóság: ez a döntő szempont
Három szabály a „más számítógépen is működjön” feltételhez
- Relatív útvonal: az adatbázisfájl a program mappájában van (
Application.StartupPath). Soha ne írj beC:\Users\...kezdetű útvonalat, mert másik gépen nem létezik. - Létrehozás kódból: ha a fájl vagy a tábla hiányzik, a program létrehozza és feltölti a mintaadatokkal. Így akkor is működik, ha csak az .exe-t másolták át.
- A teljes kimeneti mappa: az .exe mellett ott kell lennie az SQLite natív könyvtárainak is (a NuGet-csomag másolja a
bin\Debugmappába). A teljes mappát vidd át, ne csak az .exe-t.
Az adatbázis-réteg
Hozz létre egy új Windows Forms App (.NET Framework) projektet Flinstones néven, és telepítsd a System.Data.SQLite NuGet-csomagot, ahogy a 3.2 leckében tanultad. Ebben a feladatban az ablakot kódból építjük fel (nem a Designerrel), hogy az egész megoldás egyetlen fájlban követhető legyen. Ha a Designert szereted, ugyanezek a vezérlők ott is létrehozhatók, a logika változatlan marad.
A fájl eleje: útvonal és megengedett nevek
using System;
using System.Data;
using System.Data.SQLite;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
namespace Flinstones
{
public class FoForm : Form
{
// A szerző neve az Info menühöz: írd át a sajátodra!
private const string SzerzoNev = "Minta Elek";
// Az adatbázis a program mellett jön létre (relatív útvonal)
private static readonly string AdatbazisFajl = Path.Combine(Application.StartupPath, "FsDB.sqlite");
private static readonly string Kapcsolat = "Data Source=" + AdatbazisFajl + ";Version=3;";
private static readonly string[] Csaladtagok = { "Frédi", "Béni", "Irma", "Vilma" };
private static readonly string[] Kategoriak = { "Élelmiszer", "Háztartás", "Hobbi" };A Chart vezérlő a System.Windows.Forms.DataVisualization szerelvényben van. Ha a using System.Windows.Forms.DataVisualization.Charting; sor piros, add hozzá a hivatkozást: Solution Explorer → References → Add Reference → Assemblies → System.Windows.Forms.DataVisualization.
Segédmetódusok: egyszer megírt, sokszor használt
Az adatbázis-műveletek mindig ugyanazt a három lépést ismétlik: kapcsolat megnyitása, parancs paraméterekkel, lezárás. Ezt négy kis metódusba tesszük, és onnantól minden művelet egy sor.
private static SQLiteConnection Nyit()
{
SQLiteConnection k = new SQLiteConnection(Kapcsolat);
k.Open();
return k;
}
// A paraméterek névvel és értékkel párban érkeznek: "@nev", ertek, "@masik", masikertek
private static void ParameterekHozzaad(SQLiteCommand c, object[] ertekek)
{
for (int i = 0; i < ertekek.Length; i += 2)
{
c.Parameters.AddWithValue((string)ertekek[i], ertekek[i + 1]);
}
}
private static int Vegrehajt(string sql, params object[] ertekek)
{
using (SQLiteConnection k = Nyit())
using (SQLiteCommand c = new SQLiteCommand(sql, k))
{
ParameterekHozzaad(c, ertekek);
return c.ExecuteNonQuery();
}
}
private static object Skalar(string sql, params object[] ertekek)
{
using (SQLiteConnection k = Nyit())
using (SQLiteCommand c = new SQLiteCommand(sql, k))
{
ParameterekHozzaad(c, ertekek);
return c.ExecuteScalar();
}
}
private static DataTable Tabla(string sql, params object[] ertekek)
{
using (SQLiteConnection k = Nyit())
using (SQLiteCommand c = new SQLiteCommand(sql, k))
{
ParameterekHozzaad(c, ertekek);
using (SQLiteDataAdapter a = new SQLiteDataAdapter(c))
{
DataTable t = new DataTable();
a.Fill(t);
return t;
}
}
}COUNT(*) vagy SUM(...) eredményét.AddWithValue tölti be. Az SQL-ben mindig @nev típusú helyőrzőt írunk, így a bevitt szöveg soha nem lesz SQL-kód (3.7). A params kulcsszó miatt a metódusnak annyi értéket adhatsz át, amennyit akarsz: "@nev", ertek, "@masik", masikertek (névvel és értékkel párban).Az adatbázis létrehozása és feltöltése
Az AdatbazisElokeszit minden induláskor lefut. A CREATE TABLE IF NOT EXISTS csak akkor hoz létre táblát, ha még nincs. Ha a tábla üres, feltölti a feladat mintaadataival. A hat beszúrás egy tranzakcióban történik (3.10): vagy mind a hat bekerül, vagy egyik sem.
private static void AdatbazisElokeszit()
{
// Az Aru egyedi (kis-nagybetűtől függetlenül, az ASCII betűkre)
Vegrehajt("CREATE TABLE IF NOT EXISTS Vasarlas (" +
"Csaladtag TEXT NOT NULL, Aru TEXT NOT NULL COLLATE NOCASE UNIQUE, Ar INTEGER NOT NULL, Tipus TEXT NOT NULL)");
if (Convert.ToInt32(Skalar("SELECT COUNT(*) FROM Vasarlas")) > 0) { return; }
string[][] kezdo =
{
new string[] { "Vilma", "téliszalámi", "600", "Élelmiszer" },
new string[] { "Irma", "mosogatószer", "1100", "Háztartás" },
new string[] { "Béni", "adatkábel", "200", "Hobbi" },
new string[] { "Frédi", "sportújság", "700", "Hobbi" },
new string[] { "Vilma", "kenyér", "250", "Élelmiszer" },
new string[] { "Frédi", "olaj", "500", "Élelmiszer" }
};
using (SQLiteConnection k = Nyit())
using (SQLiteTransaction tr = k.BeginTransaction())
{
foreach (string[] sor in kezdo)
{
using (SQLiteCommand c = new SQLiteCommand("INSERT INTO Vasarlas (Csaladtag, Aru, Ar, Tipus) VALUES (@csaladtag, @aru, @ar, @tipus)", k, tr))
{
c.Parameters.AddWithValue("@csaladtag", sor[0]);
c.Parameters.AddWithValue("@aru", sor[1]);
c.Parameters.AddWithValue("@ar", int.Parse(sor[2]));
c.Parameters.AddWithValue("@tipus", sor[3]);
c.ExecuteNonQuery();
}
}
tr.Commit();
}
}Mire figyelj: a NOCASE korlátja
A COLLATE NOCASE az SQLite-ban csak az ASCII betűkre kis-nagybetű-független. Az olaj és az OLAJ ugyanaz, de a kenyér és a KENYÉR két különböző termék lesz, mert az ékezetes nagybetűt az SQLite nem vonja össze. Ehhez a feladathoz ez bőven elég; ha pontosabb egyezés kell, a felvétel előtt kisbetűsítheted a nevet a kódban (aru.ToLower()), és úgy tárolod.
Próbáld ki az adatbázist DB Browserben
Ebben a lépésben még nincs ablak, de az adatbázis-réteg már kipróbálható: a konstruktorból hívd meg az AdatbazisElokeszit() metódust, indítsd el a programot, majd nyisd meg a bin\Debug\FsDB.sqlite fájlt a DB Browser for SQLite programmal (3.1). A Browse Data fülön a hat mintasornak látszania kell. Töröld a fájlt, indítsd újra a programot: magától újra létrejön.
Mi történik, ha az útvonalat így írom: new SQLiteConnection("Data Source=FsDB.sqlite")?
Az SQLite ilyenkor az aktuális munkakönyvtárhoz képest keresi a fájlt. Visual Studióból indítva ez általában a bin\Debug mappa, de ha az .exe-t parancsikonból vagy másik helyről indítják, a munkakönyvtár más lehet, és a program új, üres adatbázist hoz létre egy váratlan helyen. Az Application.StartupPath mindig az .exe mappáját adja, ezért biztonságosabb.
A felület felépítése kódból
A projektsablon egy Form1 űrlapot hoz létre. Mi a saját FoForm osztályunkat használjuk, ezért a Program.cs-ben cseréld le az indítást:
using System;
using System.Windows.Forms;
namespace Flinstones
{
internal static class Program
{
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FoForm());
}
}
}A felhasználatlan Form1.cs fájlt törölheted a projektből, de ha benne hagyod, az sem zavar. Fontos, hogy a névtér mindenhol Flinstones legyen, különben a fordító nem találja a FoForm osztályt.
A vezérlők és a konstruktor
Először azokat a vezérlőket deklaráljuk mezőként, amelyekre később más metódusokból is hivatkozunk. A konstruktor megépíti a felületet, előkészíti az adatbázist, és megjeleníti az adatokat. A try–catch miatt egy adatbázis-hiba nem összeomlást, hanem érthető üzenetet okoz.
private ComboBox cmbCsaladtag, cmbTipus, cmbSzemely, cmbGrafSzemely;
private TextBox txtAru;
private NumericUpDown nudAr;
private DataGridView dgvAdat, dgvSzemely;
private Chart chart;
private Label lblUzenet, lblSzemelyOssz;
public FoForm()
{
Text = "Flinstones családi kalkulátor";
Size = new Size(1100, 760);
StartPosition = FormStartPosition.CenterScreen;
FeluletEpit();
try
{
AdatbazisElokeszit();
Frissit();
}
catch (Exception ex) { Hiba(ex); }
}Kis segédek a vezérlők készítéséhez
Ugyanazt a három vezérlőt (címke, gomb, táblázat) többször használjuk, ezért egy-egy rövid metódus készíti el őket. Így a felépítő kód rövid és olvasható marad.
private static Label Cimke(string szoveg)
{
return new Label { Text = szoveg, AutoSize = true, Margin = new Padding(8, 8, 2, 0) };
}
private static Button Gomb(string szoveg, EventHandler kezelo)
{
Button b = new Button { Text = szoveg, AutoSize = true, Margin = new Padding(6, 4, 2, 4) };
b.Click += kezelo;
return b;
}
private static DataGridView Racs()
{
return new DataGridView
{
Dock = DockStyle.Fill, ReadOnly = true, AllowUserToAddRows = false, AllowUserToDeleteRows = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect, MultiSelect = false,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill, RowHeadersVisible = false
};
}A teljes elrendezés
A FeluletEpit három feladatot végez: létrehozza a menüt, a három sávot, és beköti a gombok eseményeit. Az elrendezést TableLayoutPanelek adják, így az ablak átméretezésekor is rendezett marad.
private void FeluletEpit()
{
MenuStrip menu = new MenuStrip();
ToolStripMenuItem mnuInfo = new ToolStripMenuItem("Info");
ToolStripMenuItem mnuKilepes = new ToolStripMenuItem("Kilépés");
mnuInfo.Click += delegate { MessageBox.Show("A programot készítette: " + SzerzoNev, "Info", MessageBoxButtons.OK, MessageBoxIcon.Information); };
mnuKilepes.Click += delegate { Close(); };
menu.Items.Add(mnuInfo);
menu.Items.Add(mnuKilepes);
MainMenuStrip = menu;
// felső sor: adatbevitel
FlowLayoutPanel bevitel = new FlowLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, WrapContents = true };
cmbCsaladtag = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 100 };
cmbCsaladtag.Items.AddRange(Csaladtagok);
txtAru = new TextBox { Width = 140, MaxLength = 40 };
nudAr = new NumericUpDown { Minimum = 1, Maximum = 10000000, Width = 90, Increment = 50 };
cmbTipus = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 110 };
cmbTipus.Items.AddRange(Kategoriak);
bevitel.Controls.Add(Cimke("Családtag:")); bevitel.Controls.Add(cmbCsaladtag);
bevitel.Controls.Add(Cimke("Áru:")); bevitel.Controls.Add(txtAru);
bevitel.Controls.Add(Cimke("Ár (Ft):")); bevitel.Controls.Add(nudAr);
bevitel.Controls.Add(Cimke("Típus:")); bevitel.Controls.Add(cmbTipus);
bevitel.Controls.Add(Gomb("Felvétel", delegate { Felvesz(); }));
bevitel.Controls.Add(Gomb("Ár módosítása (áru neve alapján)", delegate { ArModosit(); }));
bevitel.Controls.Add(Gomb("Törlés (áru neve alapján)", delegate { Torol(); }));
lblUzenet = new Label { AutoSize = true, Margin = new Padding(8, 8, 2, 0), ForeColor = Color.DimGray };
bevitel.Controls.Add(lblUzenet);
// középső sor: az összes adat
dgvAdat = Racs();
dgvAdat.CellClick += AdatSorKattintas;
// alsó sor: személyenkénti lista és grafikon
GroupBox gbSzemely = new GroupBox { Text = "Személyenkénti vásárlások", Dock = DockStyle.Fill };
dgvSzemely = Racs();
FlowLayoutPanel szemelySor = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true };
cmbSzemely = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 100 };
cmbSzemely.Items.AddRange(Csaladtagok);
lblSzemelyOssz = new Label { AutoSize = true, Margin = new Padding(8, 8, 2, 0) };
szemelySor.Controls.Add(cmbSzemely);
szemelySor.Controls.Add(Gomb("Listázás", delegate { SzemelyListaz(); }));
szemelySor.Controls.Add(lblSzemelyOssz);
gbSzemely.Controls.Add(dgvSzemely); // előbb a Fill, utána a Top
gbSzemely.Controls.Add(szemelySor);
GroupBox gbGraf = new GroupBox { Text = "Családtag összkiadása", Dock = DockStyle.Fill };
chart = new Chart { Dock = DockStyle.Fill };
chart.ChartAreas.Add(new ChartArea("Fo"));
FlowLayoutPanel grafSor = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true };
cmbGrafSzemely = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 100 };
cmbGrafSzemely.Items.AddRange(Csaladtagok);
grafSor.Controls.Add(Cimke("Családtag:")); grafSor.Controls.Add(cmbGrafSzemely);
grafSor.Controls.Add(Gomb("Grafikon", delegate { Grafikon(); }));
gbGraf.Controls.Add(chart);
gbGraf.Controls.Add(grafSor);
TableLayoutPanel also = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 1 };
also.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
also.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
also.Controls.Add(gbSzemely, 0, 0);
also.Controls.Add(gbGraf, 1, 0);
TableLayoutPanel gyoker = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3 };
gyoker.RowStyles.Add(new RowStyle(SizeType.AutoSize));
gyoker.RowStyles.Add(new RowStyle(SizeType.Percent, 40));
gyoker.RowStyles.Add(new RowStyle(SizeType.Percent, 60));
gyoker.Controls.Add(bevitel, 0, 0);
gyoker.Controls.Add(dgvAdat, 0, 1);
gyoker.Controls.Add(also, 0, 2);
Controls.Add(gyoker); // előbb a Fill, utána a menü
Controls.Add(menu);
}Gyakori hiba: a menü eltakarja a tartalmat
A WinForms a Dock tulajdonságú vezérlőket a Controls gyűjteményben fordított sorrendben helyezi el. Ha a menüt adod hozzá először, a Fill panel a menü alá csúszik, és a felső sor elveszik. Ezért adjuk hozzá előbb a kitöltő panelt, utána a menüt (és ugyanígy a GroupBox-ban a táblázatot a felső sor előtt).
Üzenetek, hibakezelés, frissítés
A program három visszajelzési formát használ: a beviteli sor melletti színes üzenetet (zöld: sikeres, piros: hiba), a váratlan hibákat pedig MessageBox is jelzi. A Frissit betölti a teljes táblát a fő táblázatba.
private void Uzen(string szoveg, bool hiba)
{
lblUzenet.ForeColor = hiba ? Color.Firebrick : Color.DarkGreen;
lblUzenet.Text = szoveg;
}
private void Hiba(Exception ex)
{
Uzen("Hiba: " + ex.Message, true);
MessageBox.Show("Az adatbázis-művelet nem sikerült:\n" + ex.Message, "Hiba", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
private void Frissit()
{
dgvAdat.DataSource = Tabla("SELECT Csaladtag, Aru, Ar, Tipus FROM Vasarlas ORDER BY Csaladtag, Aru");
}Mérföldkő: indítsd el!
A program ebben a lépésben már lefordul és elindul: látod az ablakot, a menüt, a hat mintasort a nagy táblázatban, és üres a két alsó panel. A gombok még hiányzó metódusokra mutatnak, ezért a fordításhoz add hozzá a következő szakasz metódusait (vagy ideiglenesen üres törzzsel: private void Felvesz() { }).
Felvétel, kattintás, módosítás, törlés
Kattintás egy sorra: az adatok a mezőkbe kerülnek
Ez egy gyakori kényelmi funkció. A CellClick esemény megadja a kattintott sor indexét, abból olvassuk ki a cellák értékét. A fejlécre kattintáskor az index -1, ezt ki kell szűrni.
private void AdatSorKattintas(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0) { return; }
DataGridViewRow sor = dgvAdat.Rows[e.RowIndex];
cmbCsaladtag.SelectedItem = Convert.ToString(sor.Cells["Csaladtag"].Value);
txtAru.Text = Convert.ToString(sor.Cells["Aru"].Value);
nudAr.Value = Convert.ToDecimal(sor.Cells["Ar"].Value);
cmbTipus.SelectedItem = Convert.ToString(sor.Cells["Tipus"].Value);
}A cellákat az oszlop nevével érjük el (Cells["Aru"]), ez megegyezik az SQL SELECT oszlopneveivel. Ezért olvasható és nem törik el, ha később átrendezed az oszlopokat.
Felvétel: ellenőrzés, beszúrás, frissítés
A felvétel sorrendje mindig ugyanaz: először ellenőrzünk, és csak ha minden rendben van, írunk az adatbázisba. A duplikált terméknevet az adatbázis UNIQUE megszorítása jelzi hibával; ezt külön elkapjuk, és barátságos üzenetre cseréljük.
private void Felvesz()
{
string csaladtag = cmbCsaladtag.SelectedItem as string;
string tipus = cmbTipus.SelectedItem as string;
string aru = txtAru.Text.Trim();
// csak a megadott nevek és kategóriák fogadhatók el
if (csaladtag == null || Array.IndexOf(Csaladtagok, csaladtag) < 0) { Uzen("Válassz családtagot (Frédi, Béni, Irma, Vilma).", true); return; }
if (tipus == null || Array.IndexOf(Kategoriak, tipus) < 0) { Uzen("Válassz kategóriát (Élelmiszer, Háztartás, Hobbi).", true); return; }
if (aru.Length == 0 || aru.Length > 40) { Uzen("Az áru neve kötelező, legfeljebb 40 karakter.", true); return; }
if (nudAr.Value < 1) { Uzen("Az ár legalább 1 Ft legyen.", true); return; }
try
{
Vegrehajt("INSERT INTO Vasarlas (Csaladtag, Aru, Ar, Tipus) VALUES (@csaladtag, @aru, @ar, @tipus)",
"@csaladtag", csaladtag, "@aru", aru, "@ar", (int)nudAr.Value, "@tipus", tipus);
Frissit();
Uzen("Vásárlás felvéve.", false);
}
catch (SQLiteException ex) when (ex.ResultCode == SQLiteErrorCode.Constraint)
{
Uzen("Ilyen nevű áru már szerepel (az áru neve egyedi).", true);
}
catch (Exception ex) { Hiba(ex); }
}SelectedItem értéke null. Az as string ilyenkor nem dob kivételt, hanem null-t ad, amit az if elkap.catch csak az SQLite megszorítás-hibáknál fut le. A többi hiba a következő, általános catch-be kerül.NumericUpDown értéke decimal. Az Ar mező egész, ezért egész számmá alakítjuk.Módosítás és törlés terméknév alapján
Mindkét művelet a beviteli sor Áru mezőjét használja kulcsként (vagy kattints egy sorra, és a név magától kitöltődik). A Vegrehajt visszaadja az érintett sorok számát: ha 0, nem volt ilyen nevű termék, és ezt jelezzük, nem hallgatjuk el. A törlés előtt megerősítést kérünk, mert az nem vonható vissza.
private void ArModosit()
{
string aru = txtAru.Text.Trim();
if (aru.Length == 0) { Uzen("Add meg az áru nevét (vagy kattints egy sorra).", true); return; }
try
{
int db = Vegrehajt("UPDATE Vasarlas SET Ar = @ar WHERE Aru = @aru", "@ar", (int)nudAr.Value, "@aru", aru);
if (db == 0) { Uzen("Nincs ilyen nevű áru.", true); return; }
Frissit();
Uzen("Ár módosítva.", false);
}
catch (Exception ex) { Hiba(ex); }
}
private void Torol()
{
string aru = txtAru.Text.Trim();
if (aru.Length == 0) { Uzen("Add meg a törlendő áru nevét (vagy kattints egy sorra).", true); return; }
if (MessageBox.Show("Biztosan törlöd: " + aru + "?", "Törlés", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) { return; }
try
{
int db = Vegrehajt("DELETE FROM Vasarlas WHERE Aru = @aru", "@aru", aru);
if (db == 0) { Uzen("Nincs ilyen nevű áru.", true); return; }
Frissit();
Uzen("Törölve.", false);
}
catch (Exception ex) { Hiba(ex); }
}Jósolj!
A táblázatban a kenyér sorra kattintasz, az árat átírod 300-ra, és a „Törlés” helyett az „Ár módosítása” gombot nyomod. Mi lesz a táblázatban? És mi, ha a kattintás után a nevet Kenyér-re (nagy K-val) írod át?
Jóslat ellenőrzése
Az első esetben a kenyér ára 300 lesz, a többi sor változatlan. A második esetben is: az Aru mező COLLATE NOCASE beállítású, ezért az összehasonlítás a WHERE Aru = @aru feltételben nem érzékeny a kis- és nagybetűre (az ASCII betűkre), így a Kenyér is megtalálja a kenyér sort.
Személyenkénti lista és összkiadás-grafikon
A lista: egy családtag vásárlásai
A feladat azt kéri, hogy személyenként ki lehessen listázni a vásárlásokat. A WHERE Csaladtag = @csaladtag szűrés ezt adja. A lista alá kiírjuk az összeget is: a DataTable.Compute metódus az SQL SUM megfelelője a memóriában lévő táblán. Üres táblán a SUM értéke üres (DBNull), ezért ezt előre kezeljük.
private void SzemelyListaz()
{
string szemely = cmbSzemely.SelectedItem as string;
if (szemely == null) { Uzen("Válassz családtagot a listázáshoz.", true); return; }
try
{
DataTable t = Tabla("SELECT Aru, Ar, Tipus FROM Vasarlas WHERE Csaladtag = @csaladtag ORDER BY Aru", "@csaladtag", szemely);
dgvSzemely.DataSource = t;
string osszeg = t.Rows.Count == 0 ? "0" : Convert.ToString(t.Compute("SUM(Ar)", ""));
lblSzemelyOssz.Text = szemely + " összesen: " + osszeg + " Ft";
}
catch (Exception ex) { Hiba(ex); }
}A grafikon: mit ábrázoljunk?
A feladat: „egy családtag kiválasztása után az összkiadásának ábrázolása grafikonon”. Egyetlen szám (az összeg) nem grafikon, ezért az összkiadást kategóriák szerint bontva mutatjuk: három oszlop (Élelmiszer, Háztartás, Hobbi), a grafikon címében pedig az összeg szerepel. Így a grafikon a teljes összkiadást és a megoszlását is megmutatja.
Egy kategória összegét ez a lekérdezés adja:
SELECT COALESCE(SUM(Ar), 0)
FROM Vasarlas
WHERE Csaladtag = @csaladtag AND Tipus = @tipusA SUM üres halmazon NULL-t ad. A COALESCE(SUM(Ar), 0) ilyenkor 0-ra cseréli, ezért akkor is lesz oszlop (nulla magas), ha az adott személy egy kategóriában sem vásárolt. A hiányzó oszlop félrevezető lenne, mert az olvasó azt hinné, a kategória nem létezik.
private void Grafikon()
{
string szemely = cmbGrafSzemely.SelectedItem as string;
if (szemely == null) { Uzen("A grafikonhoz válassz családtagot.", true); return; }
try
{
chart.Series.Clear();
chart.Titles.Clear();
Series s = new Series("Kiadás") { ChartType = SeriesChartType.Column, IsValueShownAsLabel = true };
long ossz = 0;
foreach (string kategoria in Kategoriak) // mindhárom kategória látszik, akkor is, ha 0
{
object v = Skalar("SELECT COALESCE(SUM(Ar), 0) FROM Vasarlas WHERE Csaladtag = @csaladtag AND Tipus = @tipus", "@csaladtag", szemely, "@tipus", kategoria);
long osszeg = Convert.ToInt64(v);
s.Points.AddXY(kategoria, osszeg);
ossz += osszeg;
}
chart.Series.Add(s);
chart.Titles.Add(szemely + " összkiadása: " + ossz + " Ft");
chart.ChartAreas[0].AxisX.Title = "Kategória";
chart.ChartAreas[0].AxisY.Title = "Kiadás (Ft)";
Uzen("Grafikon kész.", false);
}
catch (Exception ex) { Hiba(ex); }
}SUM eredménye Skalar-ból object-ként jön; ezt számmá alakítjuk. A long nagy összegnél sem csordul túl.A mintaadatokkal ezeket az értékeket kell látnod. Ha eltér, a szűrést vagy az összegzést nézd át:
| Családtag | Élelmiszer | Háztartás | Hobbi | Cím |
|---|---|---|---|---|
| Frédi | 500 | 0 | 700 | Frédi összkiadása: 1200 Ft |
| Béni | 0 | 0 | 200 | Béni összkiadása: 200 Ft |
| Irma | 0 | 1100 | 0 | Irma összkiadása: 1100 Ft |
| Vilma | 850 | 0 | 0 | Vilma összkiadása: 850 Ft |
Mit tegyél ide: a kész program ablaka, Frédi kiválasztva a grafikonnál (három oszlop, a cím: Frédi összkiadása: 1200 Ft), a nagy táblázatban a hat mintasorral.Javasolt fájlnév: winforms-4-9-flinstones-kesz.webp · Alt szöveg: A Flinstones program ablaka a táblázattal és az összkiadás-grafikonnal
Az Info menü
Az Info menüpont a FeluletEpit elején készül: a MenuStrip két elemet kap, az Info a készítő nevét mutatja MessageBox-ban, a Kilépés a Close() metódussal zárja be az ablakot. A feladat csak a készítő nevét kéri, ezért a SzerzoNev konstanst írd át a saját nevedre. Ha Neptun-kódot és dátumot is szeretnél megjeleníteni (mint a MotoGP-feladatnál), ugyanitt bővítsd az üzenetet: DateTime.Now.ToShortDateString() adja a dátumot.
Ellenőrzés: tesztlista és a „tiszta gépes” próba
Jól dolgozik az, aki nem csak megírja, hanem végig is próbálja a programot. Ez a tesztlista kb. 10 perc, és kiszűri a leggyakoribb hibákat.
| # | Mit csinálsz? | Mit kell látnod? |
|---|---|---|
| 1 | Első indítás (nincs FsDB.sqlite) | A fájl létrejön, a táblázatban 6 sor van |
| 2 | Felvétel: Béni, „sör”, 450, Hobbi | Új sor a táblázat alján, zöld üzenet |
| 3 | Felvétel ugyanazzal a névvel („sör”) újra | Piros üzenet: ilyen nevű áru már szerepel; nincs új sor |
| 4 | Felvétel üres áru névvel / kiválasztott családtag nélkül | Piros üzenet, nem kerül be semmi |
| 5 | Kattintás a „kenyér” sorra | A négy beviteli mező kitöltődik (Vilma, kenyér, 250, Élelmiszer) |
| 6 | Ár módosítása: „kenyér” → 300 | A táblázatban 300; a módosítás indulás után is megmarad |
| 7 | Ár módosítása nem létező névre („xyz”) | Piros üzenet: nincs ilyen nevű áru |
| 8 | Törlés: „olaj” (igen) | A sor eltűnik; „nem”-re a sor megmarad |
| 9 | Listázás: Vilma | Két sor (téliszalámi, kenyér), összesen az árak összege |
| 10 | Grafikon: Frédi / Béni / Irma / Vilma | A fenti táblázat értékei, a cím tartalmazza az összeget |
| 11 | Info és Kilépés menü | Az Info a nevedet mutatja; a Kilépés bezárja az ablakot |
A „tiszta gépes” próba (hordozhatóság)
A feladat szerint az adatbázisnak módosítás nélkül működnie kell másik számítógépen. Ezt szimuláld:
- Visual Studióban válaszd a Build → Rebuild Solution parancsot. A Hibák listában 0 hibának kell lennie.
- Zárd be a programot, és másold ki a teljes
bin\Debugmappát egy másik helyre (például az Asztalra, vagy pendrive-ra). - Az új helyen töröld az
FsDB.sqlitefájlt. - Indítsd el a másolt .exe-t. A programnak magától létre kell hoznia az adatbázist, és működnie kell.
- Ha tudsz, próbáld ki egy másik gépen (osztálytárs, tanári gép), ahol nincs fent a Visual Studio.
Hiba: „Unable to load DLL SQLite.Interop.dll”
Unable to load DLL 'SQLite.Interop.dll': A megadott modul nem található.Ok: csak az .exe-t másoltad át, az SQLite natív könyvtára nélkül. Megoldás: a teljes bin\Debug mappát másold, benne az x86 és x64 almappákkal és a System.Data.SQLite.dll fájllal.
Hiba: a Chart vezérlő nem található
error CS0234: The type or namespace name 'DataVisualization' does not exist in the namespace 'System.Windows.Forms'Ok: hiányzik a szerelvény-hivatkozás. Megoldás: References → Add Reference → Assemblies → System.Windows.Forms.DataVisualization bejelölése.
Hiba: a program nem fordul le, mert a gomb metódusa hiányzik
error CS0103: The name 'Felvesz' does not exist in the current contextOk: a FeluletEpit olyan metódusra hivatkozik, amelyet még nem írtál meg. Megoldás: írd meg a metódust, vagy ideiglenesen adj hozzá üres törzset. Mielőtt késznek tekintenéd a programot, mindig legyen 0 fordítási hiba.
Hiba: „database is locked”
database is lockedOk: az adatbázist egy másik program (például DB Browser) is használja, és ott függőben van egy módosítás. Megoldás: a DB Browserben mentsd a változtatást (Write Changes), vagy zárd be, mielőtt a programmal próbálsz írni.
Ellenőrzőlista, mielőtt késznek tekinted
- Rebuild Solution után 0 hiba van a hibalistában.
- Az adatbázis neve
FsDB, a mezők neve pontosanCsaladtag,Aru,Ar,Tipus. - Az elérési út relatív (
Application.StartupPath), nincs benneC:\kezdetű rögzített útvonal. - A tiszta gépes próba sikerült: a másolt mappából, törölt adatbázissal is indul.
- Csak a megadott családtag- és kategórianevek vihetők fel.
- Minden követelmény működik: táblázat, törlés és módosítás névvel, személyenkénti lista, grafikon, Info menü.
- A
SzerzoNeva saját neved. - Azt adod le, amit a tanár kér. Ha nem derül ki, kérdezd meg előre: egyetlen .cs fájl elég-e, vagy a teljes projektmappa kell (a kimeneti mappával együtt, hogy a program a másik gépen is induljon).
Gyakorló feladatok
Egyszerre csak egyet csinálj: gondold végig, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást. Az éles helyzetben sem lesz megoldókulcs.
1. A legdrágább vásárlás
Tegyél egy „Legdrágább” gombot a bevitelsorba. Kattintásra egy üzenetablak mutassa meg, melyik áru a legdrágább az egész listán, ki vette, és mennyibe került. Ha a tábla üres, a program ezt jelezze. Az Egy lehetséges megoldás
ORDER BY Ar DESC LIMIT 1 a legnagyobb árú sort adja. A mintaadatokkal ez Irma mosogatószere, 1100 Ft. A FeluletEpit-be kerül a gomb, a többi egy új metódus:// a FeluletEpit-ben, a többi gomb mellé:
bevitel.Controls.Add(Gomb("Legdrágább", delegate { LegdragabbMutat(); }));
private void LegdragabbMutat()
{
try
{
DataTable t = Tabla("SELECT Csaladtag, Aru, Ar FROM Vasarlas ORDER BY Ar DESC LIMIT 1");
if (t.Rows.Count == 0) { Uzen("Még nincs vásárlás.", true); return; }
DataRow sor = t.Rows[0];
MessageBox.Show(sor["Aru"] + " (" + sor["Csaladtag"] + "): " + sor["Ar"] + " Ft", "Legdrágább vásárlás");
}
catch (Exception ex) { Hiba(ex); }
}
2. Kategóriánkénti összesítés
Tegyél egy „Kategóriák” gombot a személyenkénti lista mellé. Kattintásra a lista táblázata mutassa meg minden kategória összes kiadását a teljes családra, a legnagyobb összeggel kezdve. Ez a Egy lehetséges megoldás
GROUP BY klasszikus esete (3.5): a SUM kategóriánként összegez. A mintaadatokkal: Élelmiszer 1350, Háztartás 1100, Hobbi 900.// a szemelySor-hoz:
szemelySor.Controls.Add(Gomb("Kategóriák", delegate { KategoriaOsszesito(); }));
private void KategoriaOsszesito()
{
try
{
dgvSzemely.DataSource = Tabla("SELECT Tipus AS Kategoria, SUM(Ar) AS Osszeg FROM Vasarlas GROUP BY Tipus ORDER BY Osszeg DESC");
lblSzemelyOssz.Text = "Kategóriánkénti összesítés";
}
catch (Exception ex) { Hiba(ex); }
}
3. Kördiagram nulla szeletek nélkül
Írd át a grafikont kördiagramra ( A kördiagram arányokat mutat, a nulla érték értelmetlen szelet lenne, ezért csak a pozitív összegeket adjuk hozzá. Az összeg külön számolódik, így az üres eset felismerhető.Pie). A nulla értékű kategória ne kapjon szeletet, és ha a kiválasztott családtag még semmit sem vásárolt, a program írjon üzenetet az üres grafikon helyett.Egy lehetséges megoldás
private void KorGrafikon(string szemely)
{
chart.Series.Clear();
Series s = new Series("Kiadás") { ChartType = SeriesChartType.Pie, IsValueShownAsLabel = true };
long ossz = 0;
foreach (string kategoria in Kategoriak)
{
long osszeg = Convert.ToInt64(Skalar("SELECT COALESCE(SUM(Ar), 0) FROM Vasarlas WHERE Csaladtag = @csaladtag AND Tipus = @tipus", "@csaladtag", szemely, "@tipus", kategoria));
if (osszeg > 0) { s.Points.AddXY(kategoria, osszeg); }
ossz += osszeg;
}
if (ossz == 0) { Uzen(szemely + " még nem vásárolt semmit.", true); return; }
chart.Series.Add(s);
}
4. Időzített próba: a teljes feladat újra, 90 perc alatt
Ez a legfontosabb gyakorlat. Zárd be ezt a leckét, és csak a feladatszöveg (az 1. szakasz táblázata) alapján, a megoldás nézése nélkül építsd meg újra a programot, éles helyzetben is: új projekt, új adatbázis, minden követelmény. Állíts 90 perces időzítőt. Utána pontozd a munkádat. Ez nem hivatalos értékelés, csak egy segédlet, amivel őszintén megnézheted, hol tartasz: Ha 90 perc alatt nem lettél kész: jegyezd fel, melyik rész vitte el az időt (általában a Chart vagy a felület elrendezése), és azt gyakorold külön. Ismételd meg a próbát két-három nap múlva.Pontozási segédlet (saját becslés a gyakorláshoz)
Szempont Pont Hiba nélkül fordul, és a tiszta gépes próbán is működik (relatív útvonal, kódból létrejövő adatbázis) 20 Csak a megadott nevek és kategóriák fogadhatók el 15 Az összes adat látszik a táblázatban 10 Törlés és ármódosítás terméknév alapján, a nem létező név kezelésével 15 Személyenkénti lista 10 Összkiadás-grafikon a kiválasztott családtagra 15 Info menüpont a készítő nevével 5 Hibakezelés, érthető üzenetek, rendezett kód 10
A lecke végén: mit tudsz már?
- Egy feladat szövegét követelménylistává bontod, és minden ponthoz tudod a hozzá tartozó korábbi leckét.
- Az SQLite adatbázist kódból, a program mappájában hozod létre, és mintaadatokkal töltöd fel, így másik gépen is működik.
- A bevitelt két rétegben védi a program: DropDownList és kódbeli ellenőrzés.
- Paraméterezett SQL-lel törölsz és módosítasz terméknév alapján, és kezeled a „nincs ilyen név” esetet.
- A DataGridView sorára kattintva kitöltöd a beviteli mezőket.
- A
SUM, aCOALESCEés a Chart vezérlő segítségével összkiadás-grafikont készítesz. - Elvégzed a tesztlistát és a tiszta gépes próbát, mielőtt késznek tekintenéd a munkát.
Következik: MotoGP – a nehezebb gyakorlófeladat
A következő feladat ugyanezt a szerkezetet használja, de két helyen nehezebb: egy rekordot két mező együtt azonosít (rajtszám és futam), és új helyszínt is fel kell venni az eredményeivel együtt.
