Grafikus C# – Windows Forms tanfolyam › 16.2 lecke Önálló gyakorlás · 16.2 lecke · összetett feladat Ez a feladat ugyanarra a vázra épül, mint a Flinstones-feladat (16.1), de két helyen nehezebb. Egy eredményt két mező együtt azonosít (rajtszám és futam), és fel kell tudni venni új helyszínt az összes versenyző eredményével, amely rögtön megjelenik a táblázatban és a grafikonon. Ebben a leckében lépésről lépésre megépítjük a programot, és közben két fontos szokást gyakorlunk: a program hiba nélkül forduljon, és az adatbázis módosítás nélkül működjön másik gépen is.Gyakorlófeladat: MotoGP versenyeredmények

A lecke végére
- összetett elsődleges kulccsal (
Nr+Futam) tervezel táblát, és érted, miért kell; - a versenyzőket objektumként tárolod a legördülő listában, és a csapat magától kitöltődik;
- két mező alapján módosítasz, egy mező alapján törölsz;
GROUP BYésSUMalapján csapatonként összesítesz, és ezt grafikonon mutatod;- egy párbeszédablakkal új helyszínt veszel fel hat versenyző eredményével, tranzakcióban;
- az Info menüben a készítő nevét és a dátumot jeleníted meg;
- végigmész a tesztlistán és az ellenőrzőlistán.
Mielőtt elkezded
Érdemes először a 16.1 Flinstones-leckét megcsinálnod: az adatbázis-segédmetódusok (Nyit, ParameterekHozzaad, Vegrehajt, Skalar, Tabla) és a felület segédei (Cimke, Gomb, Racs, Uzen, Hiba) ugyanazok, ezért itt nem ismételjük meg őket. Átmásolhatod a 16.1-ből.
A feladat
A MotoGP vezetői olyan programot kérnek, amelyben rögzíthető, hogy a rajtszámmal rendelkező versenyzők és csapataik hány pontot gyűjtöttek az egyes futamokon. A versenyzők: Marquez (93, Honda), Rossi (46, Yamaha), Rins (42, Suzuki), Vinales (12, Yamaha), Mir (33, Suzuki), Espargaro (44, Honda). A csapatok: Honda, Yamaha, Suzuki. A két induló helyszín: Olasz Nagydíj és Portugál Nagydíj.
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.
| # | Követelmény | Honnan tudod? |
|---|---|---|
| 1 | Az adatbázis neve MotoGPDB, a mezők: Nr, Pilota, Istallo, Futam, Pont | 3.1, 3.2 |
| 2 | Csak a megadott versenyzők, csapatok és helyszínnevek fogadhatók el; legyen adatfelvitel | 3.7, 3.6 |
| 3 | Minden adat látszik egy DataGridView-ban | 3.8 |
| 4 | Egy tetszőleges sorra kattintva az adatok megjelennek a beviteli mezőkben | 3.9 |
| 5 | Törlés a versenyző rajtszáma alapján | 3.6 |
| 6 | A pontszám módosítása a rajtszám és a helyszín neve alapján | 3.6 |
| 7 | Személyenként kilistázni az elért pontszámokat egy DataGridView-ban | 3.5 |
| 8 | Egy csapat és egy helyszín kiválasztása után a csapat összpontszáma az adott helyszínen grafikonon | 3.5 (GROUP BY), grafikonos lecke |
| 9 | Új helyszín felvétele a versenyzők eredményeivel; megjelenik a táblázatban és a grafikonon is | 3.10, dialógusablak (7. fejezet) |
| 10 | Info menü: név + dátum; Kilépés menü | 2.9 |
| 11 | Hiba nélkül forduljon, és másik gépen is működjön | 14.7 |
A feladat mintatáblája (a program ezekkel az adatokkal indul; a sorrend a programban versenyzőnként csoportosított, de a tartalom azonos):
| Nr | Pilota | Istallo | Futam | Pont |
|---|---|---|---|---|
| 33 | Mir | Suzuki | Olasz Nagydíj | 12 |
| 46 | Rossi | Yamaha | Olasz Nagydíj | 25 |
| 42 | Rins | Suzuki | Olasz Nagydíj | 8 |
| 12 | Vinales | Yamaha | Olasz Nagydíj | 15 |
| 93 | Marquez | Honda | Olasz Nagydíj | 18 |
| 44 | Espargaro | Honda | Olasz Nagydíj | 10 |
| 93 | Marquez | Honda | Portugál Nagydíj | 15 |
| 46 | Rossi | Yamaha | Portugál Nagydíj | 10 |
| 42 | Rins | Suzuki | Portugál Nagydíj | 25 |
| 44 | Espargaro | Honda | Portugál Nagydíj | 18 |
| 33 | Mir | Suzuki | Portugál Nagydíj | 8 |
| 12 | Vinales | Yamaha | Portugál Nagydíj | 12 |
Jósolj!
Nézd meg a mintatáblát. Melyik mező vagy mezőpár azonosít egy sort egyértelműen? Elég-e a rajtszám (Nr)? Elég-e a helyszín (Futam)?
Jóslat ellenőrzése
Egyik sem elég külön: a 93-as rajtszám két sorban szerepel (egy-egy futamon), és az Olasz Nagydíj hat sorban. Egy sort csak a pár azonosít: ki (Nr) és hol (Futam). Ezt összetett elsődleges kulcsnak hívjuk, és az adatbázis megtiltja, hogy ugyanaz a pár kétszer szerepeljen. A 2. szakaszban ez lesz a tábla tervének alapja.
Tervezés: tábla, kulcs, versenyző-osztály
A feladat egyetlen táblát kér, öt mezővel. A mezőtípusokat az adatok döntik el:
| Mező | Típus | Mit tárol? |
|---|---|---|
Nr | INTEGER | a versenyző rajtszáma (93, 46, ...) |
Pilota | TEXT | a versenyző neve |
Istallo | TEXT | a csapat neve (Honda, Yamaha, Suzuki) |
Futam | TEXT | a helyszín neve |
Pont | INTEGER | az adott futamon szerzett pont |
A kulcs az első szakasz jóslata szerint a (Nr, Futam) pár. SQL-ben így mondjuk meg:
CREATE TABLE IF NOT EXISTS Eredmeny (
Nr INTEGER NOT NULL,
Pilota TEXT NOT NULL,
Istallo TEXT NOT NULL,
Futam TEXT NOT NULL,
Pont INTEGER NOT NULL,
PRIMARY KEY (Nr, Futam)
)A versenyzőkhöz kell egy kis osztály is. Azért érdemes külön osztályt írni, mert a legördülő listába objektumot teszünk, és a lista a ToString() szövegét mutatja:
public class Versenyzo
{
public int Nr { get; private set; }
public string Pilota { get; private set; }
public string Istallo { get; private set; }
public Versenyzo(int nr, string pilota, string istallo) { Nr = nr; Pilota = pilota; Istallo = istallo; }
public override string ToString() { return Nr + " - " + Pilota + " (" + Istallo + ")"; }
}Miért objektum, és nem szöveg?
Ha a listában csak a szöveg lenne, a kiválasztás után vissza kellene bontanod a rajtszámot és a csapatot. Objektummal egyetlen sor elég: Versenyzo v = cmbVersenyzo.SelectedItem as Versenyzo; után v.Nr és v.Istallo kéznél van. Az as azt is megoldja, hogy ha semmi sincs kiválasztva, v értéke null lesz, és nem dob hibát.
Az adatbázis létrehozása és feltöltése
Az adatbázis fájlja a program mellé kerül, relatív útvonalon. Ez a második alapkövetelmény megoldása: nincs bekódolva C:\Users\... útvonal, és ha a fájl még nincs meg, a program maga hozza létre és tölti fel. A kapcsolat részleteit a 14.7 lecke magyarázza.
private const string SzerzoNev = "Minta Elek"; // írd át a sajátodra!
private static readonly string AdatbazisFajl = Path.Combine(Application.StartupPath, "MotoGPDB.sqlite");
private static readonly string Kapcsolat = "Data Source=" + AdatbazisFajl + ";Version=3;";
private static readonly Versenyzo[] Versenyzok = new Versenyzo[]
{
new Versenyzo(93, "Marquez", "Honda"),
new Versenyzo(46, "Rossi", "Yamaha"),
new Versenyzo(42, "Rins", "Suzuki"),
new Versenyzo(12, "Vinales", "Yamaha"),
new Versenyzo(33, "Mir", "Suzuki"),
new Versenyzo(44, "Espargaro", "Honda")
};
private static readonly string[] Csapatok = { "Honda", "Yamaha", "Suzuki" };
private static readonly string[] KezdoFutamok = { "Olasz Nagydíj", "Portugál Nagydíj" };
// futamonként hat pont, a Versenyzok sorrendjében: 93, 46, 42, 12, 33, 44
private static readonly int[] KezdoPontok =
{
18, 25, 8, 15, 12, 10, // Olasz Nagydíj
15, 10, 25, 12, 8, 18 // Portugál Nagydíj
};A KezdoPontok egyetlen tömb, amelyben futamonként egymás után áll a hat pont, a versenyzők sorrendjében. Az első hat szám az olasz futamé, a második hat a portugálé. Az f. futam v. versenyzőjének pontja ezért a KezdoPontok[f * Versenyzok.Length + v] helyen van: a futam sorszámát megszorozzuk a versenyzők számával (6), és hozzáadjuk a versenyző sorszámát.
private static void AdatbazisElokeszit()
{
Vegrehajt("CREATE TABLE IF NOT EXISTS Eredmeny (" +
"Nr INTEGER NOT NULL, Pilota TEXT NOT NULL, Istallo TEXT NOT NULL, Futam TEXT NOT NULL, Pont INTEGER NOT NULL, " +
"PRIMARY KEY (Nr, Futam))");
if (Convert.ToInt32(Skalar("SELECT COUNT(*) FROM Eredmeny")) > 0) { return; }
using (SQLiteConnection k = Nyit())
using (SQLiteTransaction tr = k.BeginTransaction())
{
for (int f = 0; f < KezdoFutamok.Length; f++)
{
for (int v = 0; v < Versenyzok.Length; v++) { EredmenyBeszur(k, tr, Versenyzok[v], KezdoFutamok[f], KezdoPontok[f * Versenyzok.Length + v]); }
}
tr.Commit();
}
}
private static void EredmenyBeszur(SQLiteConnection k, SQLiteTransaction tr, Versenyzo v, string futam, int pont)
{
using (SQLiteCommand c = new SQLiteCommand("INSERT INTO Eredmeny (Nr, Pilota, Istallo, Futam, Pont) VALUES (@nr, @pilota, @istallo, @futam, @pont)", k, tr))
{
c.Parameters.AddWithValue("@nr", v.Nr);
c.Parameters.AddWithValue("@pilota", v.Pilota);
c.Parameters.AddWithValue("@istallo", v.Istallo);
c.Parameters.AddWithValue("@futam", futam);
c.Parameters.AddWithValue("@pont", pont);
c.ExecuteNonQuery();
}
}Jósolj!
Mi történik, ha a program mellől letörlöd a MotoGPDB.sqlite fájlt, aztán újra elindítod?
Jóslat ellenőrzése
Az SQLite a hiányzó fájlt az első megnyitáskor létrehozza (üresen). Az AdatbazisElokeszit megcsinálja a táblát, a COUNT(*) nulla, tehát újra betöltődik a 12 kezdősor. Pontosan ezért működik a program másik gépen is, ahol még nincs adatbázis. A saját módosításaid viszont elvesznek a törléssel, mert azok a fájlban voltak.
A felület felépítése kódból
A felületet itt is kódból építjük, mert így a teljes program egyetlen fájl, és nem kell a Designer-rel bajlódni. A kép három sávból áll: felül az adatbevitel, középen az összes adat, alul balra a személyenkénti lista, jobbra a grafikon.
4.1 Első rész: a menü és az adatbevitel
private void FeluletEpit()
{
MenuStrip menu = new MenuStrip();
ToolStripMenuItem mnuInfo = new ToolStripMenuItem("Info");
ToolStripMenuItem mnuKilepes = new ToolStripMenuItem("Kilépés");
mnuInfo.Click += delegate { MessageBox.Show("Készítő: " + SzerzoNev + "\nDátum: " + DateTime.Now.ToString("yyyy.MM.dd."), "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 };
cmbVersenyzo = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 190 };
cmbVersenyzo.Items.AddRange(Versenyzok);
cmbVersenyzo.SelectedIndexChanged += delegate { Versenyzo v = cmbVersenyzo.SelectedItem as Versenyzo; txtCsapat.Text = v == null ? "" : v.Istallo; };
txtCsapat = new TextBox { ReadOnly = true, Width = 80 };
cmbFutam = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 150 };
nudPont = new NumericUpDown { Minimum = 0, Maximum = 25, Width = 60 };
bevitel.Controls.Add(Cimke("Versenyző (rajtszám):")); bevitel.Controls.Add(cmbVersenyzo);
bevitel.Controls.Add(Cimke("Csapat:")); bevitel.Controls.Add(txtCsapat);
bevitel.Controls.Add(Cimke("Futam:")); bevitel.Controls.Add(cmbFutam);
bevitel.Controls.Add(Cimke("Pont:")); bevitel.Controls.Add(nudPont);
bevitel.Controls.Add(Gomb("Felvétel", delegate { Felvesz(); }));
bevitel.Controls.Add(Gomb("Pont módosítása (rajtszám + futam)", delegate { Modosit(); }));
bevitel.Controls.Add(Gomb("Törlés (rajtszám alapján)", delegate { Torol(); }));
bevitel.Controls.Add(Gomb("Új helyszín...", delegate { UjHelyszin(); }));
lblUzenet = new Label { AutoSize = true, Margin = new Padding(8, 8, 2, 0), ForeColor = Color.DimGray };
bevitel.Controls.Add(lblUzenet);
// ... folytatódik
}ReadOnly, kézzel nem írható át, így nem lehet Marquez mellé Yamahát írni.4.2 Második rész: táblázat, személyenkénti lista és grafikon
// 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 pontszámok", Dock = DockStyle.Fill };
dgvSzemely = Racs();
FlowLayoutPanel szemelySor = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true };
cmbSzemely = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 190 };
cmbSzemely.Items.AddRange(Versenyzok);
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 = "Csapat összpontszáma helyszínenként", Dock = DockStyle.Fill };
chart = new Chart { Dock = DockStyle.Fill };
chart.ChartAreas.Add(new ChartArea("Fo"));
FlowLayoutPanel grafSor = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true };
cmbCsapat = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 100 };
cmbCsapat.Items.AddRange(Csapatok);
cmbGrafFutam = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 150 };
grafSor.Controls.Add(Cimke("Csapat:")); grafSor.Controls.Add(cmbCsapat);
grafSor.Controls.Add(Cimke("Helyszín:")); grafSor.Controls.Add(cmbGrafFutam);
grafSor.Controls.Add(Gomb("Grafikon", delegate { Grafikon(); }));
gbGraf.Controls.Add(chart);
gbGraf.Controls.Add(grafSor);A sorrend számít: előbb a Fill, utána a Top
A WinForms a Dock-ra állított elemeket fordított sorrendben helyezi el. Ha a DockStyle.Top sort adod hozzá először, a nagy Fill elem eltakarja. Ezért előbb a kitöltő elemet (táblázat, grafikon), utána a felső sort adjuk a dobozhoz. Ugyanez igaz az ablakra: előbb a tartalom, utána a menü.
4.3 Harmadik rész: elrendezés, és a két segédmetódus
TableLayoutPanel also = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 1 };
also.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 45));
also.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 55));
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, 38));
gyoker.RowStyles.Add(new RowStyle(SizeType.Percent, 62));
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);
}
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);
}A TableLayoutPanel a területet sorokra és oszlopokra osztja. A gyökérben három sor van: az első AutoSize (annyi, amennyi a beviteli sor kell), a másik kettő a maradék 38, illetve 62 százaléka. Így az ablak átméretezésekor is szépen elfér minden.
Az Uzen az állapotsorba írja a visszajelzést (zölddel a sikert, pirossal a hibát). A Hiba minden adatbázis-hibát két helyen jelez: az üzenetsorban és egy ablakban. Így a program nem fagy le és nem omlik össze, hanem érthetően szól.
Állj meg és próbáld ki
A Cimke, Gomb és Racs segédeket a 4.9 leckéből másold be, és az üres Felvesz, Modosit, Torol, UjHelyszin, SzemelyListaz, Grafikon metódusokat ideiglenesen írd meg üres törzzsel. Indítsd el: az ablaknak már most látszania kell a menüvel, a sávokkal és a legördülő listákkal. Ha a lista tele van, de a Futam lista üres, az rendben van, azt a következő szakaszban töltjük fel.
Betöltés, felvétel, módosítás, törlés, lista
5.1 A táblázat és a futamlista betöltése
private void Frissit()
{
dgvAdat.DataSource = Tabla("SELECT Nr, Pilota, Istallo, Futam, Pont FROM Eredmeny ORDER BY Futam, Nr");
}
private void FutamokBetolt()
{
string elozo = cmbFutam.SelectedItem as string;
string elozoGraf = cmbGrafFutam.SelectedItem as string;
DataTable t = Tabla("SELECT Futam FROM Eredmeny GROUP BY Futam ORDER BY Futam");
cmbFutam.Items.Clear();
cmbGrafFutam.Items.Clear();
foreach (DataRow r in t.Rows)
{
cmbFutam.Items.Add(Convert.ToString(r["Futam"]));
cmbGrafFutam.Items.Add(Convert.ToString(r["Futam"]));
}
if (elozo != null) { cmbFutam.SelectedItem = elozo; }
if (elozoGraf != null) { cmbGrafFutam.SelectedItem = elozoGraf; }
}
// kattintás egy sorra: az adatok a beviteli mezőkbe kerülnek
private void AdatSorKattintas(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0) { return; } // a fejlécre kattintottak
DataGridViewRow sor = dgvAdat.Rows[e.RowIndex];
int nr = Convert.ToInt32(sor.Cells["Nr"].Value);
foreach (Versenyzo v in Versenyzok)
{
if (v.Nr == nr) { cmbVersenyzo.SelectedItem = v; }
}
cmbFutam.SelectedItem = Convert.ToString(sor.Cells["Futam"].Value);
nudPont.Value = Convert.ToDecimal(sor.Cells["Pont"].Value);
}5.2 Felvétel
private void Felvesz()
{
Versenyzo v = cmbVersenyzo.SelectedItem as Versenyzo;
string futam = cmbFutam.SelectedItem as string;
if (v == null) { Uzen("Válassz versenyzőt.", true); return; }
if (futam == null) { Uzen("Válassz futamot.", true); return; }
try
{
if (Convert.ToInt32(Skalar("SELECT COUNT(*) FROM Eredmeny WHERE Nr = @nr AND Futam = @futam", "@nr", v.Nr, "@futam", futam)) > 0)
{
Uzen("Ennek a versenyzőnek ezen a futamon már van eredménye (használd a módosítást).", true);
return;
}
using (SQLiteConnection k = Nyit())
using (SQLiteTransaction tr = k.BeginTransaction())
{
EredmenyBeszur(k, tr, v, futam, (int)nudPont.Value);
tr.Commit();
}
Frissit();
Uzen("Eredmény felvéve.", false);
}
catch (Exception ex) { Hiba(ex); }
}Az ellenőrzés sorrendje a lényeg: először a bemenetet nézzük (van-e kiválasztott versenyző és futam), aztán az adatbázist (nincs-e már ilyen pár), és csak utána írunk. A COUNT(*) előzetes vizsgálat barátságos üzenetet ad. Ha ezt kihagynád, a kulcs akkor is megvédené a táblát, de a felhasználó egy kezelhetetlen kivételszöveget látna.
5.3 Módosítás két mező alapján
private void Modosit()
{
Versenyzo v = cmbVersenyzo.SelectedItem as Versenyzo;
string futam = cmbFutam.SelectedItem as string;
if (v == null || futam == null) { Uzen("Válaszd ki a versenyzőt (rajtszám) és a futamot.", true); return; }
try
{
int db = Vegrehajt("UPDATE Eredmeny SET Pont = @pont WHERE Nr = @nr AND Futam = @futam", "@pont", (int)nudPont.Value, "@nr", v.Nr, "@futam", futam);
if (db == 0) { Uzen("Nincs ilyen eredmény (rajtszám + futam).", true); return; }
Frissit();
Uzen("Pontszám módosítva.", false);
}
catch (Exception ex) { Hiba(ex); }
}A leggyakoribb hiba: elmarad az AND
Ha a WHERE Nr = @nr után kihagyod az AND Futam = @futam részt, a versenyző összes futamának pontja átíródik ugyanarra az értékre. Az UPDATE és a DELETE mindig annyi sort érint, ahányra a feltétel igaz. Ezért kell a kulcs mindkét mezője.
Az ExecuteNonQuery visszaadja az érintett sorok számát. Ha ez nulla, nem volt mit módosítani, és ezt jelezzük, ahelyett hogy sikert hazudnánk.
5.4 Törlés rajtszám alapján
private void Torol()
{
Versenyzo v = cmbVersenyzo.SelectedItem as Versenyzo;
if (v == null) { Uzen("Válaszd ki a törlendő versenyzőt (rajtszám).", true); return; }
if (MessageBox.Show("Biztosan törlöd a(z) " + v.Nr + " rajtszámú versenyző összes eredményét?", "Törlés", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) { return; }
try
{
int db = Vegrehajt("DELETE FROM Eredmeny WHERE Nr = @nr", "@nr", v.Nr);
Frissit();
Uzen(db == 0 ? "Ehhez a rajtszámhoz nincs adat." : db + " sor törölve.", db == 0);
}
catch (Exception ex) { Hiba(ex); }
}A törlés rajtszám alapján megy, tehát a versenyző minden futamának sora eltűnik (itt kettő). Mivel ez nem visszavonható, megerősítést kérünk. A figyelmeztetés szövege kimondja, mit fog törölni.
Ha törölted, de kell az adat
Tesztelés közben könnyű kitörölni a mintaadatokat. Állítsd le a programot, töröld a MotoGPDB.sqlite fájlt a bin\Debug mappából, és indítsd újra: a 3. szakasz kódja újra létrehozza a 12 kezdősort.
5.5 Személyenkénti lista
private void SzemelyListaz()
{
Versenyzo v = cmbSzemely.SelectedItem as Versenyzo;
if (v == null) { Uzen("Válassz versenyzőt a listázáshoz.", true); return; }
try
{
DataTable t = Tabla("SELECT Futam, Pont FROM Eredmeny WHERE Nr = @nr ORDER BY Futam", "@nr", v.Nr);
dgvSzemely.DataSource = t;
lblSzemelyOssz.Text = "Összesen: " + Convert.ToString(t.Compute("SUM(Pont)", "")) + " pont";
}
catch (Exception ex) { Hiba(ex); }
}A DataTable.Compute("SUM(Pont)", "") a már betöltött táblából számolja ki az összeget, nem kell hozzá újabb SQL-lekérdezés. Ha a versenyzőnek nincs sora (mert töröltük), az eredmény üres szöveg, ezért Convert.ToString-et használunk, ami ilyenkor sem dob hibát.
Új helyszín: párbeszédablak és tranzakció
Ez a feladat legnehezebb része. Az új helyszínhez egyszerre kell megadni a nevet és mind a hat versenyző pontját. Ehhez külön kis ablakot (párbeszédablakot) írunk, amelyből a főablak kiolvassa az adatokat.
public class UjHelyszinForm : Form
{
private TextBox txtNev;
private NumericUpDown[] pontok;
public string HelyszinNev { get { return txtNev.Text.Trim(); } }
public int Pont(int index) { return (int)pontok[index].Value; }
public UjHelyszinForm(Versenyzo[] versenyzok)
{
Text = "Új helyszín felvétele";
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false; MinimizeBox = false; ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterParent;
ClientSize = new Size(360, 60 + versenyzok.Length * 32 + 60);
Controls.Add(new Label { Text = "Helyszín neve:", Left = 12, Top = 16, AutoSize = true });
txtNev = new TextBox { Left = 120, Top = 12, Width = 220, MaxLength = 40 };
Controls.Add(txtNev);
pontok = new NumericUpDown[versenyzok.Length];
for (int i = 0; i < versenyzok.Length; i++)
{
int y = 56 + i * 32;
Controls.Add(new Label { Text = versenyzok[i].ToString(), Left = 12, Top = y + 3, Width = 200 });
pontok[i] = new NumericUpDown { Left = 220, Top = y, Width = 80, Minimum = 0, Maximum = 25 };
Controls.Add(pontok[i]);
}
Button ok = new Button { Text = "Mentés", DialogResult = DialogResult.OK, Left = 160, Top = ClientSize.Height - 44, Width = 90 };
Button megse = new Button { Text = "Mégse", DialogResult = DialogResult.Cancel, Left = 260, Top = ClientSize.Height - 44, Width = 90 };
Controls.Add(ok); Controls.Add(megse);
AcceptButton = ok; CancelButton = megse;
}
}ShowDialog ezt az értéket adja vissza. Nem kell külön Close() sem eseménykezelő.private-ok, kívülről csak ez a két kapu látszik.A főablak oldali kód:
private void UjHelyszin()
{
using (UjHelyszinForm f = new UjHelyszinForm(Versenyzok))
{
if (f.ShowDialog(this) != DialogResult.OK) { return; }
string nev = f.HelyszinNev;
if (nev.Length == 0 || nev.Length > 40) { Uzen("A helyszín neve kötelező, legfeljebb 40 karakter.", true); return; }
try
{
if (Convert.ToInt32(Skalar("SELECT COUNT(*) FROM Eredmeny WHERE Futam = @futam", "@futam", nev)) > 0)
{
Uzen("Ilyen nevű helyszín már van.", true);
return;
}
using (SQLiteConnection k = Nyit())
using (SQLiteTransaction tr = k.BeginTransaction())
{
for (int i = 0; i < Versenyzok.Length; i++) { EredmenyBeszur(k, tr, Versenyzok[i], nev, f.Pont(i)); }
tr.Commit(); // mind a hat sor együtt kerül be, vagy egyik sem
}
FutamokBetolt();
Frissit();
Uzen("Új helyszín felvéve: " + nev, false);
}
catch (Exception ex) { Hiba(ex); }
}
}Jósolj!
Mit lát a felhasználó, ha az „Új helyszín...” ablakban nem ír nevet, és megnyomja a Mentés gombot? Mi történik a táblázattal?
Jóslat ellenőrzése
Az ablak bezárul (az OK gomb ezt teszi), de a nev.Length == 0 feltétel miatt a főablak piros üzenetet ír: „A helyszín neve kötelező, legfeljebb 40 karakter.”, és az adatbázishoz hozzá sem nyúl. A táblázat változatlan marad. A név ellenőrzése tehát a főablakban történik, az ablak csak adatot gyűjt.
Miért tranzakció?
A hat beszúrás együtt egy eredmény. Ha a harmadiknál valami elromlik (például megtelik a lemez), a tranzakció nem véglegesít semmit, és nem marad a helyszín félig felvéve. A Commit előtt a változások csak „ideiglenesek”. Részletek: 3.10 lecke.
Ellenőrizd
- Vegyél fel „Spanyol Nagydíj” néven egy helyszínt tetszőleges pontokkal: megjelenik a táblázat alján hat új sorban.
- A Futam lista és a grafikon helyszínlistája is tartalmazza az új nevet, újraindítás nélkül.
- Próbáld meg ugyanazzal a névvel újra: piros üzenetet kell kapnod.
- A Mégse gomb nem változtat semmit.
Csapat összpontja grafikonon, és az Info menü
A feladat: egy csapatot és egy helyszínt választva a csapat összpontszámát kell grafikonon megmutatni. Mi a csapat minden helyszínét kirajzoljuk oszlopokként, és a kiválasztottat más színnel emeljük ki, így az összehasonlítás is látszik.
private void Grafikon()
{
string csapat = cmbCsapat.SelectedItem as string;
string futam = cmbGrafFutam.SelectedItem as string;
if (csapat == null || futam == null) { Uzen("A grafikonhoz válassz csapatot és helyszínt is.", true); return; }
try
{
DataTable t = Tabla("SELECT Futam, SUM(Pont) AS Osszeg FROM Eredmeny WHERE Istallo = @csapat GROUP BY Futam ORDER BY Futam", "@csapat", csapat);
chart.Series.Clear();
chart.Titles.Clear();
Series s = new Series("Összpont") { ChartType = SeriesChartType.Column, IsValueShownAsLabel = true };
int kivalasztottOsszeg = 0;
foreach (DataRow r in t.Rows)
{
string f = Convert.ToString(r["Futam"]);
int osszeg = Convert.ToInt32(r["Osszeg"]);
int idx = s.Points.AddXY(f, osszeg);
s.Points[idx].Color = f == futam ? Color.OrangeRed : Color.SlateBlue; // a kiválasztott helyszín kiemelve
if (f == futam) { kivalasztottOsszeg = osszeg; }
}
chart.Series.Add(s);
chart.Titles.Add(csapat + " - " + futam + ": " + kivalasztottOsszeg + " pont");
chart.ChartAreas[0].AxisX.Title = "Helyszín";
chart.ChartAreas[0].AxisY.Title = "A csapat összpontja";
Uzen("Grafikon kész.", false);
}
catch (Exception ex) { Hiba(ex); }
}Jósolj!
A mintaadatokkal mekkora lesz a Honda összpontja az Olasz és a Portugál Nagydíjon? Mekkora a Yamahaé és a Suzukié?
Jóslat ellenőrzése
Honda (Marquez és Espargaro): Olasz 18 + 10 = 28, Portugál 15 + 18 = 33.
Yamaha (Rossi és Vinales): Olasz 25 + 15 = 40, Portugál 10 + 12 = 22.
Suzuki (Rins és Mir): Olasz 8 + 12 = 20, Portugál 25 + 8 = 33.
Az Info és a Kilépés menü
A menüt már a 4. szakaszban megépítettük. Az Info a nevedet és a mai dátumot mutatja, a Kilépés bezárja az ablakot. A konstanst írd át a sajátodra:
private const string SzerzoNev = "Minta Elek"; // ← a te nevedA dátumot a DateTime.Now.ToString("yyyy.MM.dd.") adja, ezért nem kell kézzel átírni. A menüre kattintva egy üzenetablak jön fel, bővebben a 2.9 leckében.
Ellenőrzés: tesztek, hordozhatóság, gyakori hibák
A program akkor jó, ha nemcsak a szép úton megy végig, hanem a rossz bemenetet is kezeli. Próbáld ki sorban, és pipáld ki:
| # | Teszt | Elvárt eredmény |
|---|---|---|
| 1 | Első indítás (nincs .sqlite fájl) | A program elindul, a táblázatban 12 sor van |
| 2 | Felvétel versenyző nélkül | Piros üzenet: „Válassz versenyzőt.” |
| 3 | Marquez, Olasz Nagydíj, 20 pont felvétele | Piros üzenet: már van eredménye (nem duplázódik) |
| 4 | Marquez, Olasz Nagydíj, módosítás 22 pontra | Csak az a sor változik, a portugál 15 marad |
| 5 | Kattintás egy táblázatsorra | A versenyző, csapat, futam és pont a mezőkbe kerül |
| 6 | Rins listázása a személyenkénti listában | Olasz 8, Portugál 25, Összesen: 33 pont |
| 7 | Honda + Olasz Nagydíj a grafikonon | Két oszlop: 28 és 33, a cím „Honda - Olasz Nagydíj: 28 pont” |
| 8 | Új helyszín felvétele | 6 új sor a táblázatban, a két listában és a grafikon helyszínlistájában is ott a név |
| 9 | Ugyanaz a helyszínnév még egyszer | Piros üzenet, a táblázat nem változik |
| 10 | Törlés Mir (33) rajtszámra | Megerősítés után az összes sora eltűnik (2 vagy több) |
| 11 | Info menü | A név és a mai dátum |
| 12 | Kilépés menü | Az ablak bezárul |
Hordozhatósági teszt (ezt mindig csináld meg!)
Ez a leglényegesebb próba. Zárd be a programot, másold a teljes bin\Debug mappát egy másik helyre (például az asztalra, vagy egy pendrive-ra), és onnan indítsd el az .exe-t. Ha elindul és működik, a másik gépen is menni fog. Ha hibát ad, valószínűleg valahol abszolút útvonal van a kódban, vagy hiányzik a System.Data.SQLite DLL a mappából. Adhatod a teljes mappát, nem csak az .exe-t.
A leggyakoribb hibák
1. Az UPDATE vagy a DELETE feltétele kimarad
Hiányzik a WHERE (vagy az AND Futam = @futam), és az egész tábla vagy a versenyző minden sora megváltozik.Hiba nélkül lefut, de minden sor ugyanazt a pontot kapja
2. A fejlécre kattintás hibát dob
A CellClick eseménykezelőből kimarad a if (e.RowIndex < 0) { return; }.ArgumentOutOfRangeException: Az index kívül esik a tartományon
3. A Dock sorrend fordított
A felső sort vagy a menüt adtad hozzá előbb, mint a kitöltő elemet, ezért a táblázat vagy a grafikon felső része eltűnik a menü alatt.Nincs hibaüzenet: a felület egyszerűen „levágott” lesz
4. Az adatbázis útvonala bekódolt
A kapcsolati szövegben C:\Users\... áll. Egy másik gépen ez a mappa nem létezik, ezért a program nem indul el.SQLiteException: unable to open database file
5. Összefűzött SQL-szöveg
Szövegként összefűzött SQL ("... WHERE Nr = " + nr) helyett mindig paramétert használj, mert a szigorú ellenőrzés lényege, hogy a program rossz bemenettel is helyesen működjön.Egy aposztróf a névben elrontja az egész parancsot
Ellenőrzőlista, mielőtt késznek tekinted
- Tiszta indításnál (törölt .sqlite fájllal) is elindul a program.
- Az adatbázis-útvonal relatív (
Application.StartupPath). - A
bin\Debugmásolata másik mappából is fut. - Az adatbázis és a mezők neve az előírt:
MotoGPDB,Nr,Pilota,Istallo,Futam,Pont. - Csak a megadott versenyzők, csapatok és helyszínek választhatók.
- Az Info menüben a saját neved és a mai dátum áll.
- Minden adatbázis-művelet
try/catch-ben van. - Az SQL paraméterezett, nincs összefűzött szöveg.
Önálló gyakorlás
Az alábbi feladatok ugyanarra a programra épülnek. Előbb próbáld meg magad, és csak utána nyisd le a megoldást.
Feladat 1: törlés a begépelt rajtszám alapján
Tegyél a beviteli sorba egy Mezőnek vedd fel: NumericUpDown mezőt (1–99), és egy „Törlés a megadott rajtszámmal” gombot. Törölje a megadott rajtszámú versenyző összes sorát, de előbb nézze meg, hogy létezik-e ilyen rajtszám, és kérjen megerősítést.Megoldás
private NumericUpDown nudRajt;, a FeluletEpit-ben hozd létre (new NumericUpDown { Minimum = 1, Maximum = 99, Width = 60 }), és add a bevitel panelhez egy gombbal együtt, amely a TorolRajtszam()-ot hívja.private void TorolRajtszam()
{
int nr = (int)nudRajt.Value;
try
{
if (Convert.ToInt32(Skalar("SELECT COUNT(*) FROM Eredmeny WHERE Nr = @nr", "@nr", nr)) == 0)
{
Uzen("Nincs ilyen rajtszám az adatbázisban.", true);
return;
}
if (MessageBox.Show("Biztosan törlöd a(z) " + nr + " rajtszám összes eredményét?", "Törlés", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) { return; }
int db = Vegrehajt("DELETE FROM Eredmeny WHERE Nr = @nr", "@nr", nr);
Frissit();
Uzen(db + " sor törölve.", false);
}
catch (Exception ex) { Hiba(ex); }
}
Feladat 2: egy futam győztese
Adj a beviteli sorhoz egy „Futam győztese” gombot. A kiválasztott futam legtöbb pontot szerző versenyzőjét írja ki üzenetablakban. Mintaadatokkal az Olasz Nagydíjon Rossi (25), a Portugálon Rins (25) nyer. Az Megoldás
private void FutamGyoztes()
{
string futam = cmbFutam.SelectedItem as string;
if (futam == null) { Uzen("Válassz futamot.", true); return; }
try
{
DataTable t = Tabla("SELECT Pilota, Pont FROM Eredmeny WHERE Futam = @futam ORDER BY Pont DESC LIMIT 1", "@futam", futam);
if (t.Rows.Count == 0) { Uzen("Ehhez a futamhoz nincs adat.", true); return; }
MessageBox.Show("A(z) " + futam + " győztese: " + t.Rows[0]["Pilota"] + " (" + t.Rows[0]["Pont"] + " pont)", "Győztes", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex) { Hiba(ex); }
}ORDER BY Pont DESC LIMIT 1 csökkenő sorrendbe teszi a futam sorait, és az elsőt veszi. Holtverseny esetén ez egy tetszőleges az egyenlők közül. Ha kíváncsi vagy, hogyan kezelnéd, gondold végig, milyen lekérdezéssel adnád meg az összes győztest (segítség: WHERE Pont = (SELECT MAX(Pont) ...)).
Feladat 3: összetett bajnokság
Írj egy „Bajnokság” gombot, amely az alsó listában ( A dgvSzemely) kilistázza az összes versenyzőt a futamok összpontjával, csökkenő sorrendben. Mintaadatokkal a sorrend: Rossi 35, Marquez 33, Rins 33, Espargaro 28, Vinales 27, Mir 20.Megoldás
private void Bajnoksag()
{
try
{
dgvSzemely.DataSource = Tabla("SELECT Pilota, Istallo, SUM(Pont) AS Osszeg FROM Eredmeny GROUP BY Nr, Pilota, Istallo ORDER BY Osszeg DESC, Pilota");
lblSzemelyOssz.Text = "Bajnokság állása";
}
catch (Exception ex) { Hiba(ex); }
}GROUP BY Nr, Pilota, Istallo versenyzőnként egy sorba vonja össze a futamokat, a SUM(Pont) adja az összeget. Az ORDER BY Osszeg DESC, Pilota második kulcsa dönt az egyenlő pontszámú Marquez és Rins között (betűrendben Marquez az első).
Feladat 4: időzített próba, 90 perc
Zárd be ezt a leckét és a megoldást. Állíts be 90 percet, és építsd fel a teljes programot a feladat táblázata (1. szakasz) alapján, kizárólag a saját tudásodból. Utána pontozd magad:
| Terület | Pont |
|---|---|
| Hiba nélkül fordul és fut, az adatbázis hordozható | alapfeltétel (enélkül a többi nem számít) |
| Tábla, kulcs, kezdőadatok | 0–3 |
| Felvétel szigorú ellenőrzéssel | 0–3 |
| Törlés és módosítás | 0–3 |
| Táblázat és kattintás a sorra | 0–2 |
| Személyenkénti lista | 0–2 |
| Grafikon | 0–3 |
| Új helyszín | 0–3 |
| Info és Kilépés menü | 0–1 |
Ez a pontozás saját becslés a gyakorláshoz, csak tájékoztató becslés. Ha elfogysz az időből, írd fel, melyik részt hagytad ki, és azt gyakorold először.
Mit tudsz most?
- Összetett kulccsal tudsz táblát tervezni, és érted, miért van rá szükség.
- Tudsz objektumokat tárolni legördülő listában, és a kiválasztásból adatot kiolvasni.
- Két mező alapján tudsz módosítani, egy mező alapján törölni.
GROUP BYésSUMsegítségével összesítesz, és az eredményt grafikonon mutatod.- Párbeszédablakkal és tranzakcióval tudsz több sort együtt felvenni.
- Tudod ellenőrizni, hogy a programod másik gépen is fut.
Tovább a tanmenetre
Következik: egy új, hasonló gyakorlófeladat, amit most már teljesen önállóan is meg tudsz oldani.
