Grafikus C# – Windows Forms tanfolyam › 4.6 lecke

Windows Forms · 4.6 · önálló gyakorlás

Önálló feladat VI.: Címjegyzék vagy teendőlista

A legtöbb valódi program adatokat kezel: felvesz, módosít, töröl, keres és szűr. Ebben az önálló feladatban egy teendőlistát építesz (vagy, ha jobban tetszik, címjegyzéket: a szerkezet ugyanaz, a változtatásokat külön táblázatban találod). Az egész a 3. fejezetben tanult eszközöket gyakoroltatja: SQLite, rétegek, paraméterek, szűrés, hibakezelés. A leírás és a támpontok a tieid, a teljes megoldás a végén lenyitható.

  • 150 perc
  • önálló munka
  • 100 pontos feladat
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A feladat végére

  • önállóan megtervezel egy egytáblás adatbázist, és rétegekre bontott alkalmazást építesz rá;
  • új, módosítás, törlés és „kész” jelölés műveletet valósítasz meg biztonságosan;
  • szűrsz állapot szerint, keresel a szövegben, és a hiányzó értékeket (NULL) kezeled a rendezésben;
  • a határidőt ISO formátumban tárolod, és a lejártakat kiemeled;
  • a hibákat érthető üzenettel kezeled, a részleteket naplózod;
  • tesztlistával és önértékeléssel ellenőrzöd a munkádat.

Mielőtt elkezded

A feladathoz a 3. fejezet leckéi kellenek, különösen a 3.5 (szűrés, rendezés), 3.6 (módosítás), 3.7 (paraméterek, dátumok, napló) és 3.9 (rétegek). Új projektet készíts, és használd újra a DB.cs segédosztályt (UjKapcsolat, IsoDatum, LikeVedelem, Naploz).

4.5 Rajzolóalkalmazás →

A feladat leírása

Készíts Teendőlista nevű Windows Forms alkalmazást SQLite adatbázissal. Egy teendőnek van címe (kötelező), leírása (nem kötelező), határideje (nem kötelező), fontos jelölése és kész állapota. Az ablak bal oldalán a teendők táblázata áll szűrővel (összes, nyitott, kész) és kereső mezővel; jobb oldalon a kijelölt teendő szerkesztő mezői. A határidő lejárt, nyitott teendőit a táblázat pirossal emeli ki. Alul egy összefoglaló felirat mutatja a nyitott, a kész és a késésben lévő teendők számát.

Ha a címjegyzéket választod

Ugyanezt a feladatot megoldhatod címjegyzékként is. A szerkezet, a követelmények és a pontozás azonos, csak a téma változik:

TeendőlistaCímjegyzék
Tábla: teendokTábla: kontaktok
Cím (kötelező)Név (kötelező)
Leírás, határidőTelefonszám, e-mail (nem kötelező, de ha megadod, legyen érvényes: az e-mail tartalmazzon @ jelet és pontot a jel után)
Fontos jelölésKedvenc jelölés
Kész / nyitott szűrőÖsszes / kedvencek szűrő
Késésben lévő (piros)Névnap vagy születésnap közelsége (haladó bővítés: a születésnap 7 napon belül)
📷 Képernyőkép helye

Mit tegyél ide: egy papírra rajzolt terv: balra a szűrő és a kereső, alatta a táblázat, jobbra a szerkesztő mezők és gombok, alul az összefoglaló felirat.Javasolt fájlnév: winforms-4-6-terv.webp · Alt szöveg: A teendőlista programjának terve

Követelmények és pontozás

RészKövetelményPont
Adatbázis és modell (15)Az teendok tábla (id, cim, leiras, hatarido, fontos, kesz), mintaadatokkal; a .db fájl a program mellé másolódik.7
Teendo osztály tulajdonságokkal és Hiba() ellenőrzéssel (a cím kötelező és legfeljebb 80 karakter; a leírás legfeljebb 500).8
Műveletek (30)Új teendő felvétele a mezőkből.8
Kijelölt teendő módosítása (a kijelölés betölti a mezőket).8
Törlés megerősítéssel.6
Kész/nyitott jelölés egy gombbal; a határidő nélküli teendő is kezelhető (NULL).8
Megjelenítés és szűrés (25)Táblázat: cím, határidő, fontos jel, állapot; rendezés: nyitottak előbb, azon belül a közeli határidő, a határidő nélküliek a nyitottak végén.10
Szűrő (összes, nyitott, kész) és kereső (cím vagy leírás), biztonságosan.8
A lejárt, nyitott teendők pirosak; összefoglaló felirat a darabszámokkal.7
Hibakezelés (15)Paraméteres SQL, érthető üzenetek, a váratlan hibák naplózása, üres és hibás bevitel kezelése.15
Szerkezet (15)Az SQL külön adatkezelő osztályban (TeendoTar), az ablakban nincs SQL; beszédes nevek, rövid metódusok.15

Összesen: 100 pont.

Időbeosztás (150 perc)

PercMit csinálj?
0–20Terv: mezők, táblázat oszlopai, a műveletek listája. Készítsd el az adatbázist a DB Browserben.
20–35Új projekt, NuGet csomag, DB.cs, az adatbázis a program mellé másolódik.
35–60Teendo osztály és TeendoTar (Betolt, Mentes, Torles, KeszValt).
60–90A felület és a lista: Lista(szuro, kereso) lekérdezés és a táblázat.
90–115Kijelölés, mezők betöltése, Új, Mentés, Törlés, Kész gomb.
115–135Késések kiemelése, összefoglaló felirat, hibakezelés és napló.
135–150Tesztlista végigpróbálása és javítás.

Támpontok (kész megoldás nélkül)

Hiányzó határidő: nullázható dátum

A DateTime nem lehet null. A nullázható változata a DateTime?: HasValue mondja meg, van-e értéke, és az .Value adja az értéket. A felületen egy chkVanHatarido jelölőnégyzet kapcsolja a DateTimePicker használatát (dtpHatarido.Enabled). Az adatbázisban a hiányzó határidő NULL, a megadott ISO szöveg (yyyy-MM-dd).

Egy tárolt logikai érték

Az SQLite-nak nincs logikai típusa: a fontos és a kesz oszlop egész szám (0 vagy 1). A C# oldalon bool, a két típus között a mentéskor kesz ? 1 : 0, olvasáskor Convert.ToInt32(...) == 1 alakít.

A rendezés a NULL határidővel

Az SQLite növekvő rendezésnél a NULL értékeket előre teszi, de a határidő nélküli teendők a végén legyenek. Megoldás: előbb egy CASE kifejezés szerint rendezz (CASE WHEN hatarido IS NULL THEN 1 ELSE 0 END), és csak utána a határidő szerint. Az ORDER BY szövege rögzített, nem a felhasználóé.

Szűrő és kereső egy lekérdezésben

Használd a 3.5 lecke „választható szűrő” mintáját: (@szuro = 0 OR (@szuro = 1 AND kesz = 0) OR (@szuro = 2 AND kesz = 1)). A kereső a 3.7 lecke szerint LIKE @minta ESCAPE '^', a minta speciális karaktereit védd. A leiras lehet NULL, ezért COALESCE(leiras, '') kell.

A késés megállapítása

Késésben van az a teendő, amely nyitott, van határideje, és a határideje kisebb a mai dátumnál. ISO szövegeknél a szöveges összehasonlítás időrend szerint dolgozik. A C# oldalon a string.Compare(a, b, StringComparison.Ordinal) negatív, ha a korábbi. Az összefoglalóhoz a SUM(CASE WHEN ... THEN 1 ELSE 0 END) kifejezést használhatod, COALESCE-szel, hogy üres tábla esetén is szám legyen.

A kijelölés és a frissítés

A táblázat frissítése közben a kijelölés többször változik. Használj frissites jelzőt, mint a 3.6 leckében, és frissítés után az azonosító alapján jelöld ki újra a sort.

Mintamegoldás

Előbb oldd meg önállóan!

A mintamegoldás csak az ellenőrzésre való, és nem az egyetlen helyes megoldás. Előbb próbálkozz saját kóddal, és csak elakadásnál vagy a végén nyisd meg.

Megoldás: adatbázis és a felület
SQLSQLite
CREATE TABLE teendok (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cim TEXT NOT NULL,
leiras TEXT,
hatarido TEXT, -- yyyy-MM-dd vagy NULL
fontos INTEGER NOT NULL DEFAULT 0,
kesz INTEGER NOT NULL DEFAULT 0
);

INSERT INTO teendok (cim, leiras, hatarido, fontos, kesz) VALUES
('Beadandó leadása', 'A C# feladat PDF-ben', '2026-10-30', 1, 0),
('Könyvtári könyv visszavitele', NULL, '2026-10-20', 0, 0),
('Tanulás: SQL JOIN', 'A 3.10 lecke feladatai', NULL, 0, 0),
('Edzés', NULL, '2026-10-27', 0, 0),
('Születésnapi ajándék', 'Vegyél valamit!', '2026-10-15', 1, 1);
Vezérlő(Name)Beállítás
ComboBoxcmbSzuroDropDownList; elemek: Összes, Nyitott, Kész
TextBoxtxtKeresokeresés a címben és a leírásban
DataGridViewdgvTeendokReadOnly, FullRowSelect, MultiSelect: False, AllowUserToAddRows: False
TextBoxtxtCim, txtLeirastxtLeiras: Multiline, MaxLength: 500; txtCim: MaxLength: 80
CheckBox, DateTimePickerchkVanHatarido, dtpHataridoFormat: Short; dtpHatarido.Enabled a jelölőnégyzettől függ
CheckBoxchkFontosText: Fontos
Button ×4btnUj, btnMent, btnKesz, btnTorolÚj, Mentés, Kész/Nyitott, Törlés
Label ×2lblUzenet, lblStatvisszajelzés és összefoglaló
Megoldás: Teendo.cs és TeendoTar.cs
Teendo.cs, TeendoTar.csC#
// ---- Teendo.cs ----
public class Teendo
{
public int Id { get; set; }
public string Cim { get; set; } = "";
public string Leiras { get; set; } = "";
public DateTime? Hatarido { get; set; } // null: nincs határidő
public bool Fontos { get; set; }
public bool Kesz { get; set; }

public string Hiba()
{
if (Cim.Length == 0)
{
return "A cím kötelező.";
}

if (Cim.Length > 80)
{
return "A cím legfeljebb 80 karakteres lehet.";
}

if (Leiras.Length > 500)
{
return "A leírás legfeljebb 500 karakteres lehet.";
}

return "";
}
}

// ---- TeendoTar.cs ----
public static class TeendoTar
{
// szuro: 0 összes, 1 nyitott, 2 kész
public static DataTable Lista(int szuro, string kereso)
{
string sql = "SELECT id, cim, COALESCE(hatarido, '') AS hatarido, "
+ "CASE WHEN fontos = 1 THEN '★' ELSE '' END AS fontos, "
+ "CASE WHEN kesz = 1 THEN 'kész' ELSE 'nyitott' END AS allapot "
+ "FROM teendok "
+ "WHERE (@szuro = 0 OR (@szuro = 1 AND kesz = 0) OR (@szuro = 2 AND kesz = 1)) "
+ "AND (cim LIKE @minta ESCAPE '^' OR COALESCE(leiras, '') LIKE @minta ESCAPE '^') "
+ "ORDER BY kesz, CASE WHEN hatarido IS NULL THEN 1 ELSE 0 END, hatarido, fontos DESC, cim";

DataTable tabla = new DataTable();

using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@szuro", szuro);
parancs.Parameters.AddWithValue("@minta", "%" + DB.LikeVedelem(kereso) + "%");

using (SQLiteDataAdapter adapter = new SQLiteDataAdapter(parancs))
{
adapter.Fill(tabla);
}
}
}

return tabla;
}

public static Teendo Betolt(int id)
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand("SELECT id, cim, leiras, hatarido, fontos, kesz FROM teendok WHERE id = @id", kapcsolat))
{
parancs.Parameters.AddWithValue("@id", id);

using (SQLiteDataReader olvaso = parancs.ExecuteReader())
{
if (!olvaso.Read())
{
return null;
}

Teendo t = new Teendo();
t.Id = Convert.ToInt32(olvaso["id"]);
t.Cim = Convert.ToString(olvaso["cim"]);
t.Leiras = Convert.ToString(olvaso["leiras"]);
t.Fontos = Convert.ToInt32(olvaso["fontos"]) == 1;
t.Kesz = Convert.ToInt32(olvaso["kesz"]) == 1;

if (olvaso["hatarido"] != DBNull.Value)
{
t.Hatarido = DateTime.Parse(Convert.ToString(olvaso["hatarido"]), CultureInfo.InvariantCulture);
}

return t;
}
}
}
}

public static int Mentes(Teendo t)
{
string hiba = t.Hiba();

if (hiba.Length > 0)
{
throw new ArgumentException(hiba);
}

object hatarido = t.Hatarido.HasValue ? (object)DB.IsoDatum(t.Hatarido.Value) : DBNull.Value;
object leiras = t.Leiras.Length == 0 ? (object)DBNull.Value : t.Leiras;

using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

string sql = t.Id == 0
? "INSERT INTO teendok (cim, leiras, hatarido, fontos, kesz) VALUES (@cim, @leiras, @hatarido, @fontos, @kesz)"
: "UPDATE teendok SET cim = @cim, leiras = @leiras, hatarido = @hatarido, fontos = @fontos, kesz = @kesz WHERE id = @id";

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@cim", t.Cim);
parancs.Parameters.AddWithValue("@leiras", leiras);
parancs.Parameters.AddWithValue("@hatarido", hatarido);
parancs.Parameters.AddWithValue("@fontos", t.Fontos ? 1 : 0);
parancs.Parameters.AddWithValue("@kesz", t.Kesz ? 1 : 0);
parancs.Parameters.AddWithValue("@id", t.Id);

int erintett = parancs.ExecuteNonQuery();

if (t.Id != 0 && erintett != 1)
{
throw new InvalidOperationException("A teendő már nem létezik.");
}
}

return t.Id == 0 ? (int)kapcsolat.LastInsertRowId : t.Id;
}
}

public static void KeszValt(int id)
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand("UPDATE teendok SET kesz = 1 - kesz WHERE id = @id", kapcsolat))
{
parancs.Parameters.AddWithValue("@id", id);

if (parancs.ExecuteNonQuery() != 1)
{
throw new InvalidOperationException("A teendő már nem létezik.");
}
}
}
}

public static void Torles(int id)
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand("DELETE FROM teendok WHERE id = @id", kapcsolat))
{
parancs.Parameters.AddWithValue("@id", id);

if (parancs.ExecuteNonQuery() != 1)
{
throw new InvalidOperationException("A teendő már nem létezik.");
}
}
}
}

// nyitott, kész és késésben lévő teendők száma
public static int[] Statisztika()
{
string sql = "SELECT COALESCE(SUM(CASE WHEN kesz = 0 THEN 1 ELSE 0 END), 0), "
+ "COALESCE(SUM(CASE WHEN kesz = 1 THEN 1 ELSE 0 END), 0), "
+ "COALESCE(SUM(CASE WHEN kesz = 0 AND hatarido IS NOT NULL AND hatarido < @ma THEN 1 ELSE 0 END), 0) FROM teendok";

using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@ma", DB.IsoDatum(DateTime.Today));

using (SQLiteDataReader olvaso = parancs.ExecuteReader())
{
olvaso.Read();
return new int[] { Convert.ToInt32(olvaso[0]), Convert.ToInt32(olvaso[1]), Convert.ToInt32(olvaso[2]) };
}
}
}
}
}
Megoldás: Form1.cs
Form1.csC#
private int kijeloltId = 0;
private bool frissites = false;
private bool betoltve = false;

private void Uzenet(string szoveg, bool hiba)
{
lblUzenet.Text = szoveg;
lblUzenet.ForeColor = hiba ? Color.Firebrick : Color.SeaGreen;
}

private void Form1_Load(object sender, EventArgs e)
{
cmbSzuro.SelectedIndex = 1; // alapból a nyitott teendők
UjTeendo();
betoltve = true;
Frissit(0);
}

private void UjTeendo()
{
kijeloltId = 0;
txtCim.Clear();
txtLeiras.Clear();
chkVanHatarido.Checked = false;
chkFontos.Checked = false;
btnMent.Text = "Új teendő felvétele";
btnKesz.Enabled = false;
txtCim.Focus();
}

private void chkVanHatarido_CheckedChanged(object sender, EventArgs e)
{
dtpHatarido.Enabled = chkVanHatarido.Checked;
}

private void Frissit(int kijelolendoId)
{
if (!betoltve)
{
return;
}

frissites = true;

try
{
dgvTeendok.DataSource = TeendoTar.Lista(cmbSzuro.SelectedIndex, txtKereso.Text.Trim());
dgvTeendok.Columns["id"].Visible = false;
dgvTeendok.Columns["cim"].HeaderText = "Teendő";
dgvTeendok.Columns["hatarido"].HeaderText = "Határidő";
dgvTeendok.Columns["fontos"].HeaderText = "!";
dgvTeendok.Columns["allapot"].HeaderText = "Állapot";

foreach (DataGridViewRow sor in dgvTeendok.Rows)
{
if (Convert.ToInt32(sor.Cells["id"].Value) == kijelolendoId)
{
dgvTeendok.CurrentCell = sor.Cells["cim"];
sor.Selected = true;
break;
}
}

int[] stat = TeendoTar.Statisztika();
lblStat.Text = $"Nyitott: {stat[0]} | Kész: {stat[1]} | Késésben: {stat[2]}";
}
catch (Exception ex)
{
DB.Naploz(ex);
MessageBox.Show("A lista betöltése most nem sikerült.");
}
finally
{
frissites = false;
}

MezokTolt();
}

private void dgvTeendok_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
string ma = DB.IsoDatum(DateTime.Today);

foreach (DataGridViewRow sor in dgvTeendok.Rows)
{
string hatarido = Convert.ToString(sor.Cells["hatarido"].Value);
bool nyitott = Convert.ToString(sor.Cells["allapot"].Value) == "nyitott";

if (nyitott && hatarido.Length > 0 && string.Compare(hatarido, ma, StringComparison.Ordinal) < 0)
{
sor.DefaultCellStyle.BackColor = Color.MistyRose;
sor.DefaultCellStyle.ForeColor = Color.Firebrick;
}
}
}

private void MezokTolt()
{
if (dgvTeendok.CurrentRow == null)
{
return;
}

int id = Convert.ToInt32(dgvTeendok.CurrentRow.Cells["id"].Value);
Teendo t = TeendoTar.Betolt(id);

if (t == null)
{
return;
}

kijeloltId = t.Id;
txtCim.Text = t.Cim;
txtLeiras.Text = t.Leiras;
chkFontos.Checked = t.Fontos;
chkVanHatarido.Checked = t.Hatarido.HasValue;

if (t.Hatarido.HasValue)
{
dtpHatarido.Value = t.Hatarido.Value;
}

btnMent.Text = "Módosítás mentése";
btnKesz.Enabled = true;
btnKesz.Text = t.Kesz ? "Újra nyitott" : "Kész";
}

private void dgvTeendok_SelectionChanged(object sender, EventArgs e)
{
if (!frissites)
{
MezokTolt();
}
}

private void SzuroValtozott(object sender, EventArgs e)
{
Frissit(kijeloltId);
}

private void btnUj_Click(object sender, EventArgs e)
{
dgvTeendok.ClearSelection();
UjTeendo();
Uzenet("", false);
}

private void btnMent_Click(object sender, EventArgs e)
{
Teendo t = new Teendo();
t.Id = kijeloltId;
t.Cim = txtCim.Text.Trim();
t.Leiras = txtLeiras.Text.Trim();
t.Fontos = chkFontos.Checked;

if (chkVanHatarido.Checked)
{
t.Hatarido = dtpHatarido.Value.Date;
}

if (kijeloltId != 0)
{
Teendo regi = TeendoTar.Betolt(kijeloltId);
t.Kesz = regi != null && regi.Kesz;
}

string hiba = t.Hiba();

if (hiba.Length > 0)
{
Uzenet(hiba, true);
return;
}

try
{
int id = TeendoTar.Mentes(t);
Uzenet("A teendő mentve.", false);
Frissit(id);
}
catch (InvalidOperationException ex)
{
Uzenet(ex.Message, true);
Frissit(0);
}
catch (Exception ex)
{
DB.Naploz(ex);
MessageBox.Show("A mentés most nem sikerült.");
}
}

private void btnKesz_Click(object sender, EventArgs e)
{
if (kijeloltId == 0)
{
return;
}

try
{
TeendoTar.KeszValt(kijeloltId);
Frissit(kijeloltId);
}
catch (InvalidOperationException ex)
{
Uzenet(ex.Message, true);
Frissit(0);
}
catch (Exception ex)
{
DB.Naploz(ex);
MessageBox.Show("A módosítás most nem sikerült.");
}
}

private void btnTorol_Click(object sender, EventArgs e)
{
if (kijeloltId == 0)
{
Uzenet("Előbb válassz ki egy teendőt!", true);
return;
}

DialogResult valasz = MessageBox.Show($"Biztosan törlöd: {txtCim.Text}?", "Törlés", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);

if (valasz != DialogResult.Yes)
{
return;
}

try
{
TeendoTar.Torles(kijeloltId);
Uzenet("A teendő törölve.", false);
}
catch (InvalidOperationException ex)
{
Uzenet(ex.Message, true);
}
catch (Exception ex)
{
DB.Naploz(ex);
MessageBox.Show("A törlés most nem sikerült.");
}

UjTeendo();
Frissit(0);
}
Megoldás: miért így épül fel?

A szerkezet a 3.9 lecke mintáját követi: az ablak csak a felületet kezeli, a Teendo osztály az adatot és az ellenőrzést adja, a TeendoTar az SQL-t. A hiányzó határidő DateTime? a modellben és NULL az adatbázisban. A rendezés CASE kifejezése a NULL határidőket a végére teszi. A szűrő és a kereső egyetlen lekérdezésben él (választható szűrő minta, ESCAPE). A késés kiemelése a DataBindingComplete eseményben történik, ISO szövegek összehasonlításával. A Kesz állapot a módosításnál megmarad (a mentés nem írja át véletlenül), és külön gomb váltja.

Tesztlista

#TesztVárt eredmény
1A program indul.A nyitott teendők látszanak, a lejártak pirosak, az összefoglaló feliraton a számok helyesek.
2Új teendő felvétele csak címmel.Megjelenik, a határidő üres, a nyitottak végén áll.
3Új teendő üres címmel.Hibaüzenet, nem kerül az adatbázisba.
4Teendő határidővel és fontos jelöléssel.A táblázatban látszik a dátum és a csillag, a sorrend határidő szerint helyes.
5Egy teendő módosítása (cím és határidő).A lista frissül, a módosított sor kijelölve marad.
6A határidő törlése (jelölőnégyzet kikapcsolása).A teendő határidő nélküli lesz (NULL).
7Kész jelölés, majd vissza nyitottra.A szűrő szerint eltűnik/megjelenik, a számok frissülnek.
8Szűrő: Összes, Nyitott, Kész.A találatok szűrőnek megfelelőek.
9Keresés: ékezetes betűvel, %, _, ' jellel.Nincs hiba, a speciális karakterek szó szerint értendők.
10Keresés a leírásban (egy leírás nélküli teendő is a listában).A leírás nélküli teendők nem okoznak hibát.
11Törlés megerősítéssel, és a Nem válasz.Igen: törlődik; Nem: marad.
12Lejárt határidő kijavítása jövőbelire.A sor már nem piros.
13A DB Browserben törölt sor módosítása a programból.Érthető üzenet, nem omlik össze.
14Az adatbázisfájl átnevezése, majd indítás.Érthető hibaüzenet, a naplóban a részletek.
15A program újraindítása.Az adatok megmaradtak.

Gyakori hibák és önértékelés

Ezeken szoktak elcsúszni

Gondold végig, nálad előfordul-e valamelyik.

1. Minden teendőnek lesz határideje

A határidő nélküli teendő mentés után a mai dátumot kapja.

Ok és javítás

A DateTimePicker.Value mindig tartalmaz dátumot. A chkVanHatarido döntse el, hogy a határidő számít-e: ha nincs bejelölve, a Hatarido maradjon null, és az adatbázisba DBNull.Value kerüljön.

2. A határidő nélküliek a lista elején állnak

ORDER BY kesz, hatarido
Hiba és javítás

Az SQLite növekvő rendezésnél a NULL értéket a legkisebbnek veszi, ezért elöl jelenik meg. Rendezz előbb egy CASE WHEN hatarido IS NULL THEN 1 ELSE 0 END kifejezés szerint, utána a határidő szerint.

3. A szűrő és a kereső összekeveri a sorokat

WHERE kesz = 0 AND cim LIKE @minta OR leiras LIKE @minta
Hiba és javítás

Az AND erősebb, ezért a leiras találatai a kesz szűrőtől függetlenül megjelennek. Zárójelezd: kesz = 0 AND (cim LIKE @minta OR leiras LIKE @minta). A NULL leírású teendő a LIKE vizsgálatban nem találat, ezért kell a COALESCE.

4. InvalidCastException a Kész oszlopnál

bool kesz = (bool)olvaso["kesz"];
Hiba és javítás

Az SQLite-ban a kesz egész szám (0 vagy 1), amely a C# oldalon Int64. A bool típusra a közvetlen átalakítás hibát ad. Használd: Convert.ToInt32(olvaso["kesz"]) == 1.

5. A lista frissítése után más sor van kijelölve

Mentés vagy a Kész gomb után az első sor kerül kijelölésre, és a mezők a másik teendőt mutatják.

Ok és javítás

A frissítés újratölti a táblázatot, és a kijelölés elveszik. Add át az azonosítót a Frissit(id) metódusnak, amely a frissítés után újra kijelöli a sort. A frissites jelző kerüli el, hogy a közbeeső kijelölések mezőtöltést indítsanak.

6. A lejárt teendők nem pirosak

Egy tegnapi határidejű, nyitott teendő is normál színű.

Ok és javítás

Ellenőrizd, hogy a határidő ISO formátumban (yyyy-MM-dd) van-e az adatbázisban és az összehasonlításban is, és hogy a színezés a DataBindingComplete eseményben fut (a DataSource beállítása után). Nyitott állapotot és nem üres határidőt is vizsgálj.

Az értékeléshez ellenőrizd az alábbi szempontokat is:

MűködésA tesztlista mind a 15 pontja teljesül.
BiztonságMinden SQL paraméteres; az ORDER BY csak rögzített szöveg; a LIKE speciális karakterei védettek.
SzerkezetAz ablakban nincs SQL; az ellenőrzés a Teendo.Hiba() metódusban van.
HibakezelésA felhasználó általános üzenetet kap, a részletek a naplóba kerülnek.

Bővítési ötletek, ha kész vagy

Ha maradt időd, próbáld ki:

  • Prioritás: egy 1–3 értékű oszlop (ALTER TABLE) és egy ComboBox a szerkesztéshez.
  • Kategóriák: egy második tábla (kategoriak) idegen kulccsal (3.10 lecke).
  • Ismétlődő teendők: kész jelölésnél a határidő eltolása egy héttel.
  • Export: a nyitott teendők szöveges fájlba mentése.
  • Értesítés: induláskor üzenet a mai és a lejárt teendőkről.

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. Az első kettő a DB Browserben, a többi a kész teendőlistán oldható meg. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. A mai és a lejárt teendők

Írj lekérdezést, amely a ma (2026-10-26) esedékes és a már lejárt, nyitott teendőket adja, a legrégebbi határidővel előre.

Megoldás megnyitása

A hatarido <= '2026-10-26' a mait és a korábbiakat is tartalmazza. A NULL határidő a feltételben nem igaz, ezért kimarad.

SQLSQLite
SELECT cim, hatarido
FROM teendok
WHERE kesz = 0 AND hatarido <= '2026-10-26'
ORDER BY hatarido;

2. Fontos és nyitott

Add meg a fontos, még nem kész teendők számát, és azt, hány teendőnek nincs határideje.

Megoldás megnyitása

Az összesítés COUNT(*), a hiányzó határidőt az IS NULL vizsgálja.

SQLSQLite
SELECT COUNT(*) FROM teendok WHERE fontos = 1 AND kesz = 0;

SELECT COUNT(*) FROM teendok WHERE hatarido IS NULL;

Közepes szint

3. Gyors felvétel Enterrel

Egy txtGyors mezőbe címet írva az Enter megnyomására azonnal új teendő kerüljön a listára (határidő nélkül), és a mező ürüljön.

Megoldás megnyitása

A KeyDown eseményben az Enter billentyűt vizsgáljuk. Az e.SuppressKeyPress = true megakadályozza a csengő hangot. Az ellenőrzést a Teendo.Hiba() metódus végzi, ugyanúgy, mint a Mentés gombnál.

Form1.csC#
private void txtGyors_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode != Keys.Enter)
{
return;
}

e.SuppressKeyPress = true;

Teendo t = new Teendo();
t.Cim = txtGyors.Text.Trim();

string hiba = t.Hiba();

if (hiba.Length > 0)
{
Uzenet(hiba, true);
return;
}

try
{
int id = TeendoTar.Mentes(t);
txtGyors.Clear();
Frissit(id);
}
catch (Exception ex)
{
DB.Naploz(ex);
MessageBox.Show("A mentés most nem sikerült.");
}
}

4. A kész teendők törlése

Egy Kész teendők törlése gomb törölje az összes kész teendőt, de előbb kérdezzen rá a darabszámmal (3 kész teendőt törölsz. Folytatod?), és utána írja ki, hányat törölt.

Megoldás megnyitása

Előbb megszámoljuk a törlendő sorokat, így a megerősítésben a pontos szám szerepel. Az ExecuteNonQuery a törölt sorok számát adja. Ha nincs kész teendő, a program ezt jelzi, és nem kérdez.

TeendoTar.cs és Form1.csC#
// TeendoTar.cs
public static int KeszekSzama()
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand("SELECT COUNT(*) FROM teendok WHERE kesz = 1", kapcsolat))
{
return Convert.ToInt32(parancs.ExecuteScalar());
}
}
}

public static int KeszekTorlese()
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand("DELETE FROM teendok WHERE kesz = 1", kapcsolat))
{
return parancs.ExecuteNonQuery();
}
}
}

// Form1.cs
private void btnKeszTorol_Click(object sender, EventArgs e)
{
int db = TeendoTar.KeszekSzama();

if (db == 0)
{
Uzenet("Nincs kész teendő.", false);
return;
}

if (MessageBox.Show($"{db} kész teendőt törölsz. Folytatod?", "Törlés", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
{
return;
}

int torolt = TeendoTar.KeszekTorlese();
Uzenet($"{torolt} kész teendő törölve.", false);
UjTeendo();
Frissit(0);
}

Kihívás szint

5. Prioritás

Adj a teendőkhöz egy prioritas oszlopot (1 = magas, 2 = közepes, 3 = alacsony, alapérték 2), válaszd ki egy cmbPrioritas listából, és rendezd úgy a listát, hogy a nyitottak között a magasabb prioritás előbb áll. Mi változik a modellben, az SQL-ben és az ablakban?

Megoldás megnyitása

Három helyen változik: (1) az adatbázis: új oszlop ALTER TABLE-lel, (2) a modell: Prioritas tulajdonság és ellenőrzés (1–3), (3) az adatkezelő és az ablak: a Mentes és a Betolt kezeli az oszlopot, a rendezés a prioritas szerint (a határidő előtt vagy után, a követelmény szerint). A rétegek miatt az ablak kódja alig változik.

SQL és C#SQLite
-- egyszer, a DB Browserben:
ALTER TABLE teendok ADD COLUMN prioritas INTEGER NOT NULL DEFAULT 2;

// Teendo.cs
public int Prioritas { get; set; } = 2;

// Teendo.Hiba():
if (Prioritas < 1 || Prioritas > 3)
{
return "A prioritás 1 és 3 közötti legyen.";
}

// TeendoTar.Lista ORDER BY: a prioritás a határidő elé kerül
// ... ORDER BY kesz, prioritas, CASE WHEN hatarido IS NULL THEN 1 ELSE 0 END, hatarido, cim

6. A nyitott teendők exportálása

Egy Export gomb mentse a nyitott teendőket szöveges fájlba, soronként [ ] Cím (határidő) alakban, a lejártakat !! jelzéssel. Mire kell figyelni a fájlkezelésnél?

Megoldás megnyitása

A StreamWriter using blokkban zárja a fájlt. A fájlnevet a SaveFileDialog kéri, a hibákat (írásvédett hely) try–catch kezeli. A lekérdezés ugyanaz a Lista metódus a nyitott szűrővel és üres kereséssel.

Form1.csC#
private void btnExport_Click(object sender, EventArgs e)
{
if (sfdExport.ShowDialog() != DialogResult.OK)
{
return;
}

try
{
DataTable tabla = TeendoTar.Lista(1, "");
string ma = DB.IsoDatum(DateTime.Today);

using (System.IO.StreamWriter iro = new System.IO.StreamWriter(sfdExport.FileName))
{
foreach (DataRow sor in tabla.Rows)
{
string hatarido = Convert.ToString(sor["hatarido"]);
bool lejart = hatarido.Length > 0 && string.Compare(hatarido, ma, StringComparison.Ordinal) < 0;

iro.WriteLine("[ ] " + sor["cim"] + (hatarido.Length > 0 ? " (" + hatarido + ")" : "") + (lejart ? " !!" : ""));
}
}

Uzenet("Az export elkészült.", false);
}
catch (Exception ex)
{
DB.Naploz(ex);
Uzenet("Az export nem sikerült.", true);
}
}

Gyors önellenőrzés

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

1. Mi a DateTime? típus, és miért kell?

A nullázható dátum. Akkor kell, ha egy dátum hiányozhat, mint a nem kötelező határidő. A HasValue megmondja, van-e értéke.

2. Hogyan tárolsz logikai értéket SQLite-ban?

Egész számként (0 vagy 1). Olvasáskor Convert.ToInt32(...) == 1, íráskor érték ? 1 : 0.

3. Hogyan rendezed a NULL határidőket a végére?

Előbb CASE WHEN hatarido IS NULL THEN 1 ELSE 0 END szerint, utána a határidő szerint rendezel.

4. Miért kell a COALESCE a keresésben?

A NULL leírás a LIKE vizsgálatban sem igaz, sem hamis, ezért a COALESCE(leiras, '') üres szöveggé alakítja.

5. Miért nincs SQL az ablakban?

Hogy a felület és az adatkezelés szétváljon, és egy változtatás egy helyen történjen. Az ablak csak a TeendoTar metódusait hívja.

6. Mi a különbség a szűrő és a kereső között a lekérdezésben?

A szűrő az állapotot korlátozza (kész/nyitott), a kereső a szövegre szűr. Egyetlen lekérdezésben, zárójelezve, AND kapcsolja őket.

Mit tudsz már?

  • Önállóan felépítesz egy rétegekre bontott, adatbázisos nyilvántartást.
  • Kezeled a hiányzó értékeket a modellben, az SQL-ben és a rendezésben.
  • Szűrést és keresést építesz egy biztonságos lekérdezésbe.
  • Határidőt kezelsz, és a lejártakat kiemeled.
  • Tesztlistával ellenőrzöd, és önértékeled a saját munkád.

Következő: önálló projekt megtervezése és tesztelése

A három önálló alkalmazás (számológép, rajzoló, nyilvántartás) után a következő lépés a saját ötleted megtervezése: követelmények, tervrajz és tesztterv.