Grafikus C# – Windows Forms tanfolyam › 16.1 lecke

Önálló gyakorlás · 16.1 lecke · összetett feladat

Gyakorlófeladat: Flinstones családi kalkulátor

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.

  • kb. 150 perc
  • haladó szint
  • összetett feladat
Diák C# programozást gyakorol egy tanulósarokban

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.

← Vissza a tanmenethez

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ényHonnan tudod?
1Az adatbázis neve FsDB, a mezők: Csaladtag, Aru, Ar, Tipus3.1, 3.2
2Csak a megadott családtag- és kategórianevek fogadhatók el3.7, ComboBox (1. fejezet)
3Minden adat látszik egy DataGridView-ban3.4, 3.8
4Törlés a termék neve alapján3.6
5A termék vételárának módosítása a termék neve alapján3.6
6Személyenként kilistázni a vásárlásokat3.5
7Egy családtag kiválasztása után az összkiadása grafikonon3.12 (SUM, COALESCE), 9.9, 9.11
8Info menüpont a program készítőjének nevével2.9
9Hiba nélkül forduljon, és másik gépen is működjön14.7

A kiinduló adatok, amelyekkel a program indul (a feladat mintatáblája):

CsaladtagAruArTipus
Vilmatéliszalámi600Élelmiszer
Irmamosogatószer1100Háztartás
Béniadatkábel200Hobbi
Frédisportújság700Hobbi
Vilmakenyér250Élelmiszer
Frédiolaj500É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ípusMegjegyzés
CsaladtagTEXT, kötelezőcsak Frédi, Béni, Irma vagy Vilma lehet
AruTEXT, kötelező, egyedia törlés és a módosítás ez alapján keres
ArINTEGER, kötelezőforintban, egész szám
TipusTEXT, kötelezőcsak Élelmiszer, Háztartás vagy Hobbi lehet
Miért egyedi az Aru?A feladat szerint a törlés és az ármódosítás terméknév alapján történik. Ha két sor neve megegyezhetne, nem lenne egyértelmű, melyik sorra gondolunk. Az adatbázis maga őrzi az egyediséget, így a program nem hagyhatja ki az ellenőrzést.
Miért nincs Id mező?A feladat táblázata négy mezőt ír elő, és a mezőneveknek egyezniük kell vele.

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.

Felülettervvázlat
┌─ 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:

  1. A felületen: a családtag és a típus DropDownList stílusú ComboBox, így a felhasználó be sem tud gépelni más értéket.
  2. 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 be C:\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\Debug mappá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

FoForm.cs · 1. részC#
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.

FoForm.cs · 2. részC#
        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;
}
}
}
VegrehajtINSERT, UPDATE, DELETE és CREATE végrehajtása. Visszaadja az érintett sorok számát: ebből tudjuk, hogy a törlés vagy a módosítás talált-e a névre sort.
SkalarEgyetlen értéket ad vissza, például COUNT(*) vagy SUM(...) eredményét.
TablaTöbb sort ad vissza DataTable-ben. Ezt kötjük a DataGridView DataSource tulajdonságához (3.8).
ParameterekHozzaadA paramétereket a 3.7 leckéből ismert 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.

FoForm.cs · 3. részC#
        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:

Program.csC#
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.

FoForm.cs · 4. rész (az 1. rész után)C#
        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.

FoForm.cs · 5. részC#
        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
};
}
ReadOnly = trueA táblázatban nem lehet közvetlenül szerkeszteni. A módosítás a beviteli mezőkön és a gombokon át történik, így minden változtatás az ellenőrzött kódon megy keresztül.
FullRowSelectEgy kattintás a teljes sort kijelöli. Ez kell ahhoz, hogy a sorra kattintás kitöltse a beviteli mezőket.
AllowUserToAddRows = falseEltűnik az üres „új sor” a táblázat aljáról, amit a felhasználó véletlenül kitölthetne.
AutoSizeColumnsMode = FillAz oszlopok kitöltik a rendelkezésre álló szélességet, nem lóg ki vízszintes görgetősáv.

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.

FoForm.cs · 6. részC#
        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.

FoForm.cs · 7. részC#
        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.

FoForm.cs · 8. részC#
        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.

FoForm.cs · 9. részC#
        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); }
}
as stringHa nincs kijelölt elem, a SelectedItem értéke null. Az as string ilyenkor nem dob kivételt, hanem null-t ad, amit az if elkap.
Array.IndexOf(...) < 0A kódbeli ellenőrzés: ha a név nincs a megengedettek között, az eredmény negatív, és a felvétel elutasítva.
catch ... whenSzűrt kivételkezelés (C# 6): ez a 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.
(int)nudAr.ValueA 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.

FoForm.cs · 10. részC#
        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.

FoForm.cs · 11. részC#
        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:

SQLösszegzés
SELECT COALESCE(SUM(Ar), 0)
FROM Vasarlas
WHERE Csaladtag = @csaladtag AND Tipus = @tipus

A 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.

FoForm.cs · 12. részC#
        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); }
}
Series.Clear és Titles.ClearMinden kattintás előtt töröljük az előző sorozatot és címet, különben a grafikonok egymásra rakódnának.
SeriesChartType.ColumnOszlopdiagram. Kategóriák összehasonlítására ez a legolvashatóbb típus.
IsValueShownAsLabelAz oszlopok fölé kiírja az értéket, így nem kell a tengelyről leolvasni.
Convert.ToInt64A 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ÉlelmiszerHáztartásHobbiCím
Frédi5000700Frédi összkiadása: 1200 Ft
Béni00200Béni összkiadása: 200 Ft
Irma011000Irma összkiadása: 1100 Ft
Vilma85000Vilma összkiadása: 850 Ft
📷 Képernyőkép helye

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?
1Első indítás (nincs FsDB.sqlite)A fájl létrejön, a táblázatban 6 sor van
2Felvétel: Béni, „sör”, 450, HobbiÚj sor a táblázat alján, zöld üzenet
3Felvétel ugyanazzal a névvel („sör”) újraPiros üzenet: ilyen nevű áru már szerepel; nincs új sor
4Felvétel üres áru névvel / kiválasztott családtag nélkülPiros üzenet, nem kerül be semmi
5Kattintás a „kenyér” sorraA négy beviteli mező kitöltődik (Vilma, kenyér, 250, Élelmiszer)
6Ár módosítása: „kenyér” → 300A 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
8Törlés: „olaj” (igen)A sor eltűnik; „nem”-re a sor megmarad
9Listázás: VilmaKét sor (téliszalámi, kenyér), összesen az árak összege
10Grafikon: Frédi / Béni / Irma / VilmaA fenti táblázat értékei, a cím tartalmazza az összeget
11Info é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:

  1. Visual Studióban válaszd a Build → Rebuild Solution parancsot. A Hibák listában 0 hibának kell lennie.
  2. Zárd be a programot, és másold ki a teljes bin\Debug mappát egy másik helyre (például az Asztalra, vagy pendrive-ra).
  3. Az új helyen töröld az FsDB.sqlite fájlt.
  4. 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.
  5. 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 context

Ok: 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 locked

Ok: 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 pontosan Csaladtag, Aru, Ar, Tipus.
  • Az elérési út relatív (Application.StartupPath), nincs benne C:\ 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 SzerzoNev a 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.

Egy lehetséges megoldás

Az 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:

MegoldásC#
// 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.

Egy lehetséges megoldás

Ez a GROUP BY klasszikus esete (3.5): a SUM kategóriánként összegez. A mintaadatokkal: Élelmiszer 1350, Háztartás 1100, Hobbi 900.

MegoldásC#
// 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 (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

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ő.

MegoldásC#
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.

Pontozási segédlet (saját becslés a gyakorláshoz)

Ez nem hivatalos értékelés, csak egy segédlet, amivel őszintén megnézheted, hol tartasz:

SzempontPont
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 el15
Az összes adat látszik a táblázatban10
Törlés és ármódosítás terméknév alapján, a nem létező név kezelésével15
Személyenkénti lista10
Összkiadás-grafikon a kiválasztott családtagra15
Info menüpont a készítő nevével5
Hibakezelés, érthető üzenetek, rendezett kód10

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.

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, a COALESCE é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.

Tovább a MotoGP-feladatra