Grafikus C# – Windows Forms tanfolyam › 10.10 lecke

Windows Forms · 10.10 · záró mini projekt

Mini projekt: jegyzetek és beállítások tárolása

A 10. fejezet záró projektje egy jegyzetkezelő, amely az adataidat soha nem veszíti el. A jegyzetek JSON-fájlban tárolódnak (10.6), a mentés biztonságos: ideiglenes fájl, File.Replace, biztonsági másolat (10.9); a program emlékszik az ablakára, a témára és az utolsó jegyzetre (10.8); a fájlkezelés egységes és hibatűrő (10.1, 10.4); a jegyzeteket exportálhatod szövegként, Markdownként, CSV-ként (10.3, 10.5), és importálhatod is. Sérült fájl esetén a program felajánlja a tartalék másolatot, és soha nem ír felül épet hibás adattal. Ez a fejezet minden témáját egyetlen, valódi használatra alkalmas programban köti össze.

  • kb. 8–10 óra
  • haladó szint
  • záró projekt
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A projekt végére

  • adatmodellt, tárolást (JSON), beállításokat és felületet külön rétegekbe szervezel;
  • biztonságos, hibatűrő mentést és betöltést építesz tartalék másolattal és automatikus mentéssel;
  • a beállításokat (ablak, téma, utolsó jegyzet) megőrzöd és érvényesíted;
  • jegyzeteket keresel, szűrsz, rendezel (ékezetfüggetlenül), exportálsz és importálsz;
  • minden hibát érthetően jelzel, és semmilyen adatot nem veszítesz el csendben;
  • a programot tesztlista szerint végigteszteled.

Mielőtt elkezded

Ehhez a projekthez a 10.1–10.9 lecke szükséges (különösen a 10.4, 10.6, 10.8 és 10.9), valamint a 7.4 lecke (MessageBox) és a 8.4 lecke (listák, BindingList). Új Windows Forms projektben vedd át a FajlValaszto (10.4), a FajlHiba és a BiztonsagosFajl (10.9) osztályokat.

10.9 Mentési hibák →

Követelmények és pontozás

RészKövetelményPont
Modell és tárolás (20)Jegyzet (Id mint Guid, Cim, Szoveg, Cimkek, Rogzitett, Letrehozva, Modositva) és a verziózott JegyzetFajl burkoló; JSON (System.Text.Json): tagolt, ékezetes, Verzio; az adatmappa a %AppData% alatt.12
Betöltés hibatűréssel: hiányzó fájl (első indítás) nem hiba; hibás JSON (sor és pozíció üzenet), újabb verzió; a hibás fájlt a program soha nem írja felül.8
Biztonságos mentés (20)Ideiglenes fájl + Flush(true) + File.Replace + .bak; a mentés hibája érthető üzenet és napló, a „módosított” jelző megmarad.10
Automatikus mentés (időzítő, módosítás után 30 mp), mentés kilépéskor és jegyzetváltáskor; betöltés tartalékból, ha a főfájl sérült (a felhasználó döntésével).10
Jegyzetkezelés (20)Új, szerkesztés, törlés (megerősítéssel), rögzítés (kitűzés); cím és szöveg szerkesztése; címkék (vesszővel elválasztva).10
Keresés (ékezetfüggetlen: cím, szöveg, címke), címke-szűrő, rendezés (módosítás szerint, cím szerint, a rögzítettek elöl); a lista és a szerkesztő mindig egyezik.10
Beállítások (15)Properties.Settings: ablak helye, mérete, állapota (RestoreBounds, képernyő-ellenőrzés), betűméret, téma, utolsó jegyzet, automatikus mentés be/ki, indítások száma, első indítás; Upgrade; sérült beállításfájl kezelése.15
Export és import (15)Egy jegyzet exportja .txt és .md fájlba (UTF-8, a fájlnév tisztítva); az összes jegyzet exportja CSV-be (;, UTF-8 BOM, idézés, képletvédelem); biztonsági mentés JSON-be.8
Import: .txt/.md fájlokból új jegyzetek (kódolás-felismeréssel); JSON-ból az Id szerinti egyesítés (ütközésnél kérdés).7
Minőség (10)Rétegek szétválasztva (modell, tároló, felület); nincs üres catch; érthető üzenetek; billentyűparancsok (Ctrl+N, Ctrl+S, Ctrl+F, Delete), AccessibleName; állapotsor (mentve ekkor, jegyzetek száma).10

Összesen: 100 pont.

Felületterv

TerületVezérlők
MenüFájl: Új jegyzet (Ctrl+N), Mentés (Ctrl+S), Importálás (Szöveges fájlok..., Biztonsági mentés (JSON)...), Exportálás (Jegyzet fájlba..., Összes CSV-be..., Biztonsági mentés (JSON)...), Adatmappa megnyitása, Kilépés; Szerkesztés: Keresés (Ctrl+F), Törlés (Delete), Rögzítés/Feloldás; Beállítások: Téma (Világos, Sötét, Magas kontraszt), Betűméret..., Automatikus mentés (jelölő), Alaphelyzet.
Bal oldal (SplitContainer.Panel1)txtKereses, cmbCimke ((minden) + címkék), cmbRendezes, lstJegyzetek (cím, a rögzített 📌-lel).
Jobb oldal (Panel2)txtCim, txtCimkek, lblIdo (Létrehozva…, módosítva…), txtSzoveg (többsoros, Dock = Fill).
ÁllapotsortsslMentes (Mentve 10:32 / Nem mentett változás / hiba), tsslDb (12 jegyzet, 3 találat), tsslUzenet.

Gondold végig!

A felhasználó szerkeszt egy jegyzetet, majd a listában másik jegyzetre kattint, mielőtt mentene. Mit kell tennie a programnak a szerkesztővel és az adatokkal?

Egy lehetséges válasz

Váltás előtt a szerkesztő tartalmát vissza kell írni a kijelölt jegyzetbe (és a módosítás idejét frissíteni), majd az új jegyzet adatait betölteni a szerkesztőbe. Ha ezt kihagyod, az eltűnik: a szerkesztett szöveg a másik jegyzethez kerül, vagy elvész. A lista újraépítése (szűrés, rendezés) közben ugyanez a szabály: előbb commit, utána frissítés.

Építés lépésenként

1. lépés: modell és tároló

Feladat: Jegyzet, JegyzetFajl; Tarolo.Betolt (hibatűrő, verzióellenőrzés, tartalék) és Tarolo.Ment (BiztonsagosFajl); az adatmappa.

Támpont: 10.1, 10.6, 10.9 lecke.

2. lépés: felület és a jegyzetek megjelenítése

Feladat: a felületterv megépítése; a lista a szűrt és rendezett jegyzetekből; a szerkesztő betöltése és visszaírása (commit); a lblIdo, az állapotsor.

Támpont: 8.4–8.7 lecke.

3. lépés: szerkesztés, mentés, automatikus mentés

Feladat: a módosítás jelzése; Timer (30 mp) az automatikus mentéshez; mentés váltáskor és kilépéskor; a mentés hibájának kezelése (a jelző megmarad, üzenet, napló).

Támpont: 10.8, 10.9 lecke.

4. lépés: keresés, címke-szűrő, rendezés

Feladat: ékezetfüggetlen keresés (Egyseges); a címkék gyűjtése a szűrőhöz; rendezés (a rögzítettek mindig elöl); a kijelölés megőrzése a lista újraépítésekor (Id alapján).

Támpont: 8.7 és 10.3 lecke.

5. lépés: beállítások

Feladat: a Settings felvétele (UtolsoJegyzet: System.Guid típus a Browse listából, vagy string; UtolsoMappa: string; Automentes: bool, alapérték True; Tema: string; BetuMeret: int, alapérték 11; az ablak-beállítások, az IndulasokSzama, az ElsoIndulas és a FrissitesKell a 10.8 lecke szerint); Upgrade; az ablak visszaállítása és mentése; a téma, a betűméret, az automatikus mentés jelölő, az utolsó jegyzet; a sérült beállításfájl kezelése; az első indítás üdvözlő jegyzete.

Támpont: 10.8 lecke.

6. lépés: export

Feladat: egy jegyzet .txt / .md fájlba (a fájlnév a címből, tisztítva, egyedi névvel); az összes CSV-be; biztonsági mentés JSON-ba; az utolsó mappa megjegyezve.

Támpont: 10.1, 10.4, 10.5 lecke.

7. lépés: import

Feladat: szöveges fájlok (több is) új jegyzetként (a cím a fájlnévből, a kódolás felismerve); JSON-ból egyesítés Id szerint, ütközésnél kérdés (Felülírás / Kihagyás / Másolat); az eredmény összegzése.

Támpont: 10.3 és 10.6 lecke.

8. lépés: utómunka, hozzáférhetőség, tesztelés

Feladat: billentyűparancsok, AccessibleName, magas kontraszt téma, a tesztlista végigjátszása, a naplófájl ellenőrzése.

Támpont: 6.9 és 10.9 lecke.

Mintamegoldás

Előbb próbáld meg önállóan!

A mintamegoldás az ellenőrzésre való. A felület a tiéd; itt a logikát adom meg. A FajlValaszto (10.4), a FajlHiba és a BiztonsagosFajl.Ment (10.9), az Egyseges (10.5) és a Csv.Sor (10.5) osztályokat nem ismétlem.

Megoldás: modell és tároló
Tarolo.cs (using System.Text.Json; using System.Text.Json.Serialization; using System.IO; using System.Text; using System.Linq; using System.Collections.Generic;)C#
public class Jegyzet
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Cim { get; set; } = "";
public string Szoveg { get; set; } = "";
public List<string> Cimkek { get; set; } = new List<string>();
public bool Rogzitett { get; set; }
public DateTime Letrehozva { get; set; } = DateTime.Now;
public DateTime Modositva { get; set; } = DateTime.Now;

public override string ToString() { return (Rogzitett ? "📌 " : "") + (Cim.Length == 0 ? "(cím nélkül)" : Cim); } // a lista megjelenítése
}

public class JegyzetFajl
{
public int Verzio { get; set; } = 1;
public List<Jegyzet> Jegyzetek { get; set; } = new List<Jegyzet>();
}

public static class Tarolo
{
public const int AktualisVerzio = 1;

public static readonly JsonSerializerOptions Beallitas = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

public static string Mappa()
{
string m = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MiniJegyzet");

Directory.CreateDirectory(m);

return m;
}

public static string Ut() { return Path.Combine(Mappa(), "jegyzetek.json"); }

// Eredmény: az adat, vagy null és a hiba; a hiányzó fájl nem hiba.
public static JegyzetFajl Olvas(string ut, out string hiba)
{
hiba = "";

if (!File.Exists(ut)) { return new JegyzetFajl(); }

try
{
JegyzetFajl f = JsonSerializer.Deserialize<JegyzetFajl>(File.ReadAllText(ut, Encoding.UTF8), Beallitas);

if (f == null) { hiba = "A fájl üres."; return null; }
if (f.Verzio > AktualisVerzio) { hiba = "A fájlt a program újabb változata írta (verzió: " + f.Verzio + ")."; return null; }

HashSet<Guid> latott = new HashSet<Guid>();

foreach (Jegyzet j in f.Jegyzetek)
{
if (j.Id == Guid.Empty || !latott.Add(j.Id)) { j.Id = Guid.NewGuid(); latott.Add(j.Id); }

j.Cim = j.Cim ?? "";
j.Szoveg = j.Szoveg ?? "";
j.Cimkek = j.Cimkek ?? new List<string>();
}

return f;
}
catch (JsonException ex) { hiba = "Hibás JSON (sor: " + ((ex.LineNumber ?? 0) + 1) + ", pozíció: " + (ex.BytePositionInLine ?? 0) + ")."; }
catch (IOException ex) { hiba = "A fájl nem olvasható: " + ex.Message; }
catch (UnauthorizedAccessException) { hiba = "Nincs jogosultság a fájlhoz."; }

return null;
}

// Betöltés tartalékkal: főfájl → (a felhasználó döntése után) .bak; a hívó kérdez, nem a tároló.
public static JegyzetFajl Betolt(out string hiba, out bool tartalekVan)
{
JegyzetFajl f = Olvas(Ut(), out hiba);

tartalekVan = f == null && File.Exists(Ut() + ".bak");

return f;
}

public static JegyzetFajl TartalekBetolt(out string hiba) { return Olvas(Ut() + ".bak", out hiba); }

public static void Ment(JegyzetFajl f)
{
f.Verzio = AktualisVerzio;

BiztonsagosFajl.Ujraproba(() => BiztonsagosFajl.Ment(Ut(), JsonSerializer.Serialize(f, Beallitas)), 3, 200);
}

// a sérült főfájl félretétele (nem törlés): .hibas
public static void SerultFelretesz()
{
string ut = Ut();

if (File.Exists(ut)) { File.Move(ut, ut + "." + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".hibas"); }
}
}
Megoldás: Form1.cs (betöltés, lista, szerkesztő, mentés)
Form1.cs (using System.Collections.Specialized; using System.Configuration; using System.Linq; using System.IO; using System.Text; using System.Collections.Generic;)C#
private JegyzetFajl adat = new JegyzetFajl();
private Jegyzet aktualis = null;
private bool modosult = false;
private bool tolt = false; // a vezérlők kódból való töltésekor nem reagálunk
private System.Windows.Forms.Timer automentes = new System.Windows.Forms.Timer { Interval = 30000 };
private Properties.Settings be { get { return Properties.Settings.Default; } }

// ---------- indulás ----------

private void Form1_Load(object sender, EventArgs e)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

try { int proba = be.BetuMeret; }
catch (ConfigurationErrorsException ex)
{
string fajl = ex.Filename ?? (ex.InnerException as ConfigurationErrorsException)?.Filename;

if (fajl != null && File.Exists(fajl)) { File.Move(fajl, fajl + ".hibas"); } // a sérült beállítások félretéve
}

if (be.FrissitesKell) { be.Upgrade(); be.FrissitesKell = false; be.Save(); }

AblakVisszaallit();
TemaAlkalmaz();
miAutomentes.Checked = be.Automentes;
be.IndulasokSzama++;

if (!AdatBetolt()) { return; }

if (be.ElsoIndulas || adat.Jegyzetek.Count == 0)
{
adat.Jegyzetek.Add(new Jegyzet { Cim = "Üdv a jegyzetek között!", Szoveg = "Írj ide, a program néhány másodpercenként magától ment.", Cimkek = new List<string> { "súgó" } });
be.ElsoIndulas = false;
}

CimkeSzuroFrissit();
ListaFrissit(be.UtolsoJegyzet);

automentes.Tick += (s, ev) => { if (modosult && be.Automentes) { Mentes(false); } };
automentes.Start();
}

private bool AdatBetolt()
{
string hiba;
bool tartalek;
JegyzetFajl f = Tarolo.Betolt(out hiba, out tartalek);

if (f != null) { adat = f; return true; }

// hibás főfájl: NEM írjuk felül; a felhasználó dönt
string kerdes = "A jegyzetek fájlja nem tölthető be: " + hiba + Environment.NewLine + Environment.NewLine + (tartalek ? "Betöltsem a tartalék másolatot?" : "Üres jegyzetlistával indítsak? A hibás fájl félre lesz téve, nem törlődik.");

if (tartalek)
{
DialogResult v = MessageBox.Show(this, kerdes, "Jegyzetek", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);

if (v == DialogResult.Yes)
{
string h2;
JegyzetFajl t = Tarolo.TartalekBetolt(out h2);

if (t != null) { adat = t; tsslUzenet.Text = "A tartalék másolatot töltöttem be."; Tarolo.SerultFelretesz(); modosult = true; return true; }

MessageBox.Show(this, "A tartalék sem használható: " + h2, "Jegyzetek", MessageBoxButtons.OK, MessageBoxIcon.Error);
}

if (v != DialogResult.No) { Close(); return false; } // Mégse: kilépés, nincs adatvesztés
}
else if (MessageBox.Show(this, kerdes, "Jegyzetek", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) { Close(); return false; }

Tarolo.SerultFelretesz();
adat = new JegyzetFajl();

return true;
}

// ---------- lista, szűrés, szerkesztő ----------

private void ListaFrissit(Guid? kijelolendo = null)
{
Commit(); // előbb a szerkesztő tartalmát vissza az adatba

Guid? marad = kijelolendo ?? (aktualis != null ? (Guid?)aktualis.Id : null);
string kulcs = Egyseges(txtKereses.Text);
string cimke = cmbCimke.SelectedIndex <= 0 ? null : (string)cmbCimke.SelectedItem;

IEnumerable<Jegyzet> q = adat.Jegyzetek.Where(j => (cimke == null || j.Cimkek.Contains(cimke, StringComparer.CurrentCultureIgnoreCase)) && (kulcs.Length == 0 || Egyseges(j.Cim).Contains(kulcs) || Egyseges(j.Szoveg).Contains(kulcs) || j.Cimkek.Any(c => Egyseges(c).Contains(kulcs))));

q = cmbRendezes.SelectedIndex == 1 ? q.OrderByDescending(j => j.Rogzitett).ThenBy(j => j.Cim, StringComparer.CurrentCultureIgnoreCase)
: q.OrderByDescending(j => j.Rogzitett).ThenByDescending(j => j.Modositva);

List<Jegyzet> lista = q.ToList();

tolt = true;
lstJegyzetek.BeginUpdate();
lstJegyzetek.Items.Clear();
foreach (Jegyzet j in lista) { lstJegyzetek.Items.Add(j); }
lstJegyzetek.EndUpdate();

Jegyzet kij = marad.HasValue ? lista.FirstOrDefault(j => j.Id == marad.Value) : null;

if (kij == null && lista.Count > 0) { kij = lista[0]; }

lstJegyzetek.SelectedItem = kij;
tolt = false;

Betolt(kij);
tsslDb.Text = adat.Jegyzetek.Count + " jegyzet, " + lista.Count + " találat";
}

private void Betolt(Jegyzet j)
{
aktualis = j;
tolt = true;

txtCim.Text = j == null ? "" : j.Cim;
txtSzoveg.Text = j == null ? "" : j.Szoveg;
txtCimkek.Text = j == null ? "" : string.Join(", ", j.Cimkek);
lblIdo.Text = j == null ? "" : "Létrehozva: " + j.Letrehozva.ToString("yyyy. MM. dd. HH:mm") + ", módosítva: " + j.Modositva.ToString("yyyy. MM. dd. HH:mm");
pnlSzerkeszto.Enabled = j != null;
tolt = false;

if (j != null) { be.UtolsoJegyzet = j.Id; }
}

// a szerkesztő tartalmát vissza az aktuális jegyzetbe (csak ha tényleg változott)
private void Commit()
{
if (aktualis == null) { return; }

List<string> cimkek = txtCimkek.Text.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries).Select(c => c.Trim()).Where(c => c.Length > 0).Distinct(StringComparer.CurrentCultureIgnoreCase).ToList();

if (aktualis.Cim != txtCim.Text.Trim() || aktualis.Szoveg != txtSzoveg.Text || !aktualis.Cimkek.SequenceEqual(cimkek))
{
aktualis.Cim = txtCim.Text.Trim();
aktualis.Szoveg = txtSzoveg.Text;
aktualis.Cimkek = cimkek;
aktualis.Modositva = DateTime.Now;
modosult = true;
}
}

private void Szerkesztes_Valtozott(object sender, EventArgs e)
{
if (tolt || aktualis == null) { return; }

modosult = true;
tsslMentes.Text = "Nem mentett változás";
}

private void lstJegyzetek_SelectedIndexChanged(object sender, EventArgs e)
{
if (tolt) { return; }

Commit(); // váltás előtt a szerkesztett jegyzet mentése az adatba
Betolt(lstJegyzetek.SelectedItem as Jegyzet);

if (modosult) { Mentes(false); }
}

private void CimkeSzuroFrissit()
{
object regi = cmbCimke.SelectedItem;

cmbCimke.Items.Clear();
cmbCimke.Items.Add("(minden)");
foreach (string c in adat.Jegyzetek.SelectMany(j => j.Cimkek).Distinct(StringComparer.CurrentCultureIgnoreCase).OrderBy(c => c)) { cmbCimke.Items.Add(c); }
cmbCimke.SelectedItem = regi != null && cmbCimke.Items.Contains(regi) ? regi : cmbCimke.Items[0];
}

private void Szuro_Valtozott(object sender, EventArgs e) { if (!tolt) { ListaFrissit(); } }

// ---------- új, törlés, rögzítés ----------

private void miUj_Click(object sender, EventArgs e)
{
Commit();

Jegyzet j = new Jegyzet { Cim = "Új jegyzet" };

adat.Jegyzetek.Add(j);
modosult = true;
txtKereses.Clear();
ListaFrissit(j.Id);
txtCim.Focus();
txtCim.SelectAll();
}

private void miTorles_Click(object sender, EventArgs e)
{
if (aktualis == null) { return; }

if (MessageBox.Show(this, "Törlöd a(z) '" + aktualis.Cim + "' jegyzetet?", "Törlés", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes) { return; }

adat.Jegyzetek.Remove(aktualis);
aktualis = null;
modosult = true;
CimkeSzuroFrissit();
ListaFrissit();
Mentes(false);
}

private void miRogzit_Click(object sender, EventArgs e)
{
if (aktualis == null) { return; }

aktualis.Rogzitett = !aktualis.Rogzitett;
modosult = true;
ListaFrissit(aktualis.Id);
}

// ---------- mentés ----------

private bool Mentes(bool hangosan)
{
Commit();

try
{
Tarolo.Ment(adat);
modosult = false;
tsslMentes.Text = "Mentve " + DateTime.Now.ToString("HH:mm");
return true;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
bool ujra;
string uzenet = FajlHiba.Szoveg(ex, "A mentés", Tarolo.Ut(), out ujra);

Naploz(ex);
tsslMentes.Text = "A mentés NEM sikerült!"; // a módosított jelző megmarad

if (hangosan || !figyelmeztetve) { MessageBox.Show(this, uzenet, "Mentés", MessageBoxButtons.OK, MessageBoxIcon.Warning); figyelmeztetve = true; }

return false;
}
}

private bool figyelmeztetve = false;

private void Naploz(Exception ex)
{
try { File.AppendAllText(Path.Combine(Tarolo.Mappa(), "hibak.log"), DateTime.Now.ToString("s") + Environment.NewLine + ex + Environment.NewLine + Environment.NewLine); }
catch (IOException) { } // a naplózás hibája nem állíthatja le a programot
}

private void miMent_Click(object sender, EventArgs e) { if (Mentes(true)) { figyelmeztetve = false; } }

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (!Mentes(true))
{
if (MessageBox.Show(this, "A jegyzetek mentése nem sikerült. Kilépsz mentés nélkül?", "Kilépés", MessageBoxButtons.YesNo, MessageBoxIcon.Stop, MessageBoxDefaultButton.Button2) != DialogResult.Yes) { e.Cancel = true; return; }
}

if (this.WindowState == FormWindowState.Normal) { be.AblakHely = Location; be.AblakMeret = Size; be.AblakAllapot = "Normal"; }
else { be.AblakHely = RestoreBounds.Location; be.AblakMeret = RestoreBounds.Size; be.AblakAllapot = WindowState == FormWindowState.Maximized ? "Maximized" : "Normal"; }

try { be.Save(); } catch (ConfigurationErrorsException) { }
}

// ---------- export ----------

private void miExportEgy_Click(object sender, EventArgs e)
{
if (aktualis == null) { return; }

Commit();

int idx;
string ut = FajlValaszto.Ment(this, "Szöveg (*.txt)|*.txt|Markdown (*.md)|*.md", be.UtolsoMappa, FajlnevTisztit(aktualis.Cim), out idx);

if (ut == null) { return; }

string tartalom = idx == 2 ? "# " + aktualis.Cim + Environment.NewLine + Environment.NewLine + aktualis.Szoveg : aktualis.Cim + Environment.NewLine + new string('=', Math.Max(3, aktualis.Cim.Length)) + Environment.NewLine + aktualis.Szoveg;

try { File.WriteAllText(ut, tartalom, new UTF8Encoding(false)); be.UtolsoMappa = Path.GetDirectoryName(ut); tsslUzenet.Text = "Exportálva: " + Path.GetFileName(ut); }
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { bool u; MessageBox.Show(this, FajlHiba.Szoveg(ex, "Az exportálás", ut, out u), "Exportálás", MessageBoxButtons.OK, MessageBoxIcon.Warning); }
}

private string FajlnevTisztit(string nev)
{
foreach (char c in Path.GetInvalidFileNameChars()) { nev = nev.Replace(c, '_'); }

nev = nev.Trim().TrimEnd('.');

return nev.Length == 0 ? "jegyzet" : nev;
}

private void miExportCsv_Click(object sender, EventArgs e)
{
Commit();

int idx;
string ut = FajlValaszto.Ment(this, "CSV (*.csv)|*.csv", be.UtolsoMappa, "jegyzetek", out idx);

if (ut == null) { return; }

try
{
using (StreamWriter iro = new StreamWriter(ut, false, new UTF8Encoding(true))) // BOM az Excelnek
{
iro.WriteLine(Csv.Sor(';', "Cím", "Címkék", "Létrehozva", "Módosítva", "Szöveg"));

foreach (Jegyzet j in adat.Jegyzetek) { iro.WriteLine(Csv.Sor(';', j.Cim, string.Join(", ", j.Cimkek), j.Letrehozva.ToString("yyyy-MM-dd HH:mm"), j.Modositva.ToString("yyyy-MM-dd HH:mm"), j.Szoveg)); } // idézés és képletvédelem a Csv.Mezo-ban
}

be.UtolsoMappa = Path.GetDirectoryName(ut);
tsslUzenet.Text = adat.Jegyzetek.Count + " jegyzet exportálva.";
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { bool u; MessageBox.Show(this, FajlHiba.Szoveg(ex, "Az exportálás", ut, out u), "Exportálás", MessageBoxButtons.OK, MessageBoxIcon.Warning); }
}

private void miExportJson_Click(object sender, EventArgs e)
{
Commit();

int idx;
string ut = FajlValaszto.Ment(this, "Jegyzet-mentés (*.json)|*.json", be.UtolsoMappa, "jegyzetek-mentes-" + DateTime.Now.ToString("yyyyMMdd"), out idx);

if (ut == null) { return; }

try { BiztonsagosFajl.Ment(ut, JsonSerializer.Serialize(adat, Tarolo.Beallitas)); tsslUzenet.Text = "Biztonsági mentés kész."; }
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { bool u; MessageBox.Show(this, FajlHiba.Szoveg(ex, "A mentés", ut, out u), "Exportálás", MessageBoxButtons.OK, MessageBoxIcon.Warning); }
}

// ---------- import ----------

private void miImportSzoveg_Click(object sender, EventArgs e)
{
string[] utak = FajlValaszto.MegnyitTobb(this, FajlValaszto.JegyzetSzuro, be.UtolsoMappa);

if (utak == null) { return; }

int sikeres = 0, hibas = 0;

foreach (string ut in utak.Take(200)) // korlát: legfeljebb 200 fájl
{
try
{
if (new FileInfo(ut).Length > 2 * 1024 * 1024) { hibas++; continue; } // 2 MB felett kihagyjuk

byte[] b = File.ReadAllBytes(ut);
string szoveg;

try { szoveg = new UTF8Encoding(false, true).GetString(b); }
catch (DecoderFallbackException) { szoveg = Encoding.GetEncoding(1250).GetString(b); }

adat.Jegyzetek.Add(new Jegyzet { Cim = Path.GetFileNameWithoutExtension(ut), Szoveg = szoveg.TrimStart('\uFEFF') });
sikeres++;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { hibas++; }
}

if (sikeres > 0) { modosult = true; CimkeSzuroFrissit(); ListaFrissit(); Mentes(false); }

tsslUzenet.Text = sikeres + " fájl importálva, " + hibas + " kimaradt.";
}

private void miImportJson_Click(object sender, EventArgs e)
{
string ut = FajlValaszto.Megnyit(this, "Jegyzet-mentés (*.json)|*.json", be.UtolsoMappa);

if (ut == null) { return; }

string hiba;
JegyzetFajl be2 = Tarolo.Olvas(ut, out hiba);

if (be2 == null) { MessageBox.Show(this, "Az importálás nem sikerült: " + hiba, "Importálás", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }

Commit();

int uj = 0, felulir = 0, kihagy = 0;

foreach (Jegyzet bej in be2.Jegyzetek)
{
Jegyzet van = adat.Jegyzetek.FirstOrDefault(j => j.Id == bej.Id);

if (van == null) { adat.Jegyzetek.Add(bej); uj++; continue; }

if (van.Modositva >= bej.Modositva) { kihagy++; continue; } // a meglévő frissebb

DialogResult v = MessageBox.Show(this, "A(z) '" + van.Cim + "' jegyzet az importált fájlban újabb. Felülírod a meglévőt?", "Ütközés", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);

if (v == DialogResult.Cancel) { break; }

if (v == DialogResult.Yes) { adat.Jegyzetek[adat.Jegyzetek.IndexOf(van)] = bej; felulir++; }
else { bej.Id = Guid.NewGuid(); adat.Jegyzetek.Add(bej); uj++; } // Nem: megtartjuk mindkettőt (másolat)
}

modosult = true;
aktualis = null;
CimkeSzuroFrissit();
ListaFrissit();
Mentes(false);
tsslUzenet.Text = uj + " új, " + felulir + " felülírt, " + kihagy + " kihagyott.";
}

// ---------- beállítások ----------

private void TemaAlkalmaz()
{
Color hatter = Color.White, szoveg = Color.Black;

if (be.Tema == "Sötét") { hatter = Color.FromArgb(32, 32, 36); szoveg = Color.WhiteSmoke; }
else if (be.Tema == "Magas kontraszt") { hatter = Color.Black; szoveg = Color.Yellow; }

foreach (Control c in new Control[] { txtSzoveg, txtCim, txtCimkek, lstJegyzetek, txtKereses }) { c.BackColor = hatter; c.ForeColor = szoveg; }

Font uj = new Font(txtSzoveg.Font.FontFamily, Math.Max(8, Math.Min(32, be.BetuMeret)));
Font regi = txtSzoveg.Font;

txtSzoveg.Font = uj;
regi.Dispose();
}

private void AblakVisszaallit()
{
Size meret = be.AblakMeret;
Point hely = be.AblakHely;

if (meret.Width < MinimumSize.Width || meret.Height < MinimumSize.Height) { return; }

Rectangle kert = new Rectangle(hely, meret);

if (!Screen.AllScreens.Any(s => Rectangle.Intersect(s.WorkingArea, kert).Width >= 100 && Rectangle.Intersect(s.WorkingArea, kert).Height >= 50)) { return; }

StartPosition = FormStartPosition.Manual;
Location = hely;
Size = meret;

if (be.AblakAllapot == "Maximized") { WindowState = FormWindowState.Maximized; }
}

private void miAdatmappa_Click(object sender, EventArgs e)
{
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(Tarolo.Mappa()) { UseShellExecute = true }); }
catch (Exception ex) when (ex is System.ComponentModel.Win32Exception || ex is InvalidOperationException) { tsslUzenet.Text = "A mappa nem nyitható meg: " + Tarolo.Mappa(); }
}
📷 Képernyőkép helye

Mit tegyél ide: a kész jegyzetkezelő: a bal oldali lista (rögzített jegyzettel), a szerkesztő, a címkék és az állapotsor (Mentve 10:32).Javasolt fájlnév: winforms-10-10-kesz.webp · Alt szöveg: A kész jegyzetkezelő

Tesztlista

#TesztVárt eredmény
1Első indítás (nincs adatmappa).A program elindul, az üdvözlő jegyzet létrejön, nincs hiba.
2Új jegyzet, szerkesztés, újraindítás.A jegyzet és a szerkesztett szöveg megvan; az utolsó jegyzet kijelölve.
3Jegyzetváltás mentés nélkül.A szerkesztett szöveg megmarad (commit), nem kerül másik jegyzetbe.
4Automatikus mentés: 30 másodperc várakozás szerkesztés után; az automatikus mentés kikapcsolása.Bekapcsolva mentve; kikapcsolva a „Nem mentett változás” marad.
5Törlés megerősítéssel, Nem válasz.A jegyzet megmarad.
6Rögzítés és feloldás, rendezés cím és módosítás szerint.A rögzített elöl; a kijelölés megmarad.
7Keresés: ékezet nélkül (tojas → tojás), címke szerint, szűrő és keresés együtt.Helyes találatok; az állapotsor a találatok számát mutatja.
8A jegyzetek.json elrontása (zárójel törlése), indítás.Hibaüzenet sorral és pozícióval; a fájl nem íródik felül; a program a tartalékot vagy az üres listát kínálja.
9A főfájl törlése, a .bak megmarad.A program a tartalékot ajánlja fel.
10A fájl írásvédetté tétele, mentés.Érthető üzenet, a „NEM sikerült” jelző, a módosított állapot megmarad; kilépéskor figyelmeztetés.
11A fájl zárolása (másik program), mentés.Újrapróbálás, érthető üzenet, nincs összeomlás.
12Export .txt és .md (ékezetes cím, tiltott karakterek a címben).UTF-8 fájl, a fájlnév tisztítva, a tartalom helyes.
13CSV export Excelben megnyitva (ékezetek, ;, többsoros szöveg, = kezdetű jegyzet).Helyes oszlopok és ékezetek; a = szövegként marad.
14JSON mentés és importálás ütközéssel (Igen, Nem, Mégse).Helyes egyesítés; az összegzés egyezik.
15Szöveges import (UTF-8 és Windows-1250 fájl, nagy fájl).Helyes ékezetek; a nagy fájl kimarad; a darabszám jelentve.
16Ablak: áthelyezés, átméretezés, maximalizálás, újraindítás; a második monitor eltávolítása.A pozíció visszaáll; képernyőn kívüli hely esetén az alap.
17A user.config elrontása; Alaphelyzet.A program indul (a fájl félretéve), az alapértékekkel.
181000 jegyzet; billentyűzetes használat (Ctrl+N, Ctrl+S, Ctrl+F, Delete); 150% betűméret; magas kontraszt téma.Gyors lista és keresés; minden elérhető billentyűvel; olvasható.

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

Ezeken szoktak elcsúszni

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

1. A hibás fájl betöltése után a program felülírja az adatot

A felhasználó adatai elvesznek.

Ok és javítás

A betöltés hibájánál üres listát állítottál be, és a következő mentés felülírta a fájlt. Hiba esetén a hibás fájl félre kerül (.hibas), a felhasználó dönt, és a tartalék .bak az első választás.

2. A váltáskor elvész a szerkesztett szöveg

Másik jegyzetre kattintva az előző változtatásai eltűnnek.

Ok és javítás

Hiányzik a Commit() váltás előtt. A lista újraépítése (ListaFrissit) is előbb commitol, és a tolt jelző megakadályozza, hogy a kódból töltött vezérlők „módosításként” számítsanak.

3. A jegyzet kijelölése eltűnik szűréskor

Keresés után más jegyzet jelenik meg.

Ok és javítás

A kijelölést az Id alapján kell megőrizni (nem a lista sorszáma szerint): az újraépítés után a Guid-ot keresed meg.

4. A sikertelen mentés után nyomtalanul „mentettnek” látszik

A mentés hibája után az állapotsor „mentve”.

Ok és javítás

A modosult jelzőt csak sikeres mentés állíthatja hamisra; a hiba esetén a jelző marad, és az állapotsor jelez.

5. Az importált jegyzet duplikálódik, vagy felülír frissebbet

Az import után ugyanaz a jegyzet kétszer szerepel.

Ok és javítás

Az Id alapú egyesítés hiányzik. Az azonos Id az ütközés (módosítási idő szerint döntsd el), az új Id új jegyzet; a másolatnak új Id kell.

6. A CSV-ben a képletek végrehajtódnak

Az Excelben a = kezdetű jegyzet képletként fut.

Ok és javítás

Nincs képletvédelem: használd a Csv.Sor metódust (az ' előtag a szöveges mezőkre).

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

AdatbiztonságSoha nem vész el adat csendben: a hibás fájl félretéve, a mentés atomi, a .bak megvan, a kilépéskor a mentési hiba figyelmeztet.
RétegezésA modell (Jegyzet), a tárolás (Tarolo, BiztonsagosFajl) és a felület szétválik; a Form1 nem ismeri a JSON részleteit.
HibakezelésNincs üres catch; a felhasználó érthető üzenetet kap; a teljes hiba a naplóban; a when szűrő a fájlkivételekre.
HozzáférhetőségBillentyűzettel használható; az állapotsor szövegesen jelez; a magas kontraszt téma; a betűméret állítható.

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

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

  • Gazdag szöveg: RichTextBox félkövér, dőlt, lista; a tárolás Markdown vagy RTF.
  • Jelszavas jegyzetek: titkosítás a ProtectedData vagy egy jelszóból képzett kulcs segítségével (a jelszót nem tárolod).
  • Verziótörténet: a jegyzet korábbi változatai (utolsó 10 módosítás), visszaállítás.
  • Többes kijelölés és csoportos művelet: címke hozzáadása több jegyzethez.
  • Szinkronizálás: a FileSystemWatcher észleli, ha másik gép módosította a fájlt (felhő-mappa), és felajánlja az egyesítést.
  • Háttérmentés: a mentés és az import háttérszálon (11. fejezet), előrehaladás-jelzővel.
  • Nyomtatás és PDF: a jegyzet nyomtatása (2.15 lecke), exportálás képként (9.2 lecke).

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. A kész programot bővíted. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Szószámláló

Az állapotsor mutassa a jegyzet szavainak és karaktereinek számát. Mit használsz?

Megoldás megnyitása

A txtSzoveg.TextChanged eseményben: Split(new[]{' ', '\r', '\n', '\t'}, StringSplitOptions.RemoveEmptyEntries).Length a szavakra, Text.Length a karakterekre. Nagy szövegnél érdemes időzítővel késleltetni a számolást.

2. Utoljára módosított jegyzetek menü

A Fájl menü mutassa az utolsó 5 módosított jegyzetet. Honnan veszed az adatot?

Megoldás megnyitása

Az adat.Jegyzetek.OrderByDescending(j => j.Modositva).Take(5) felsorolásból, minden menünyitásnál (DropDownOpening) újraépítve (7.1 lecke).

Közepes szint

3. Címkék kezelése

A címkék átnevezése minden jegyzetben egyszerre. Hogyan?

Megoldás megnyitása

Végigmész az adat.Jegyzetek elemein, és a Cimkek listában kicseréled a régi nevet az újra (Distinct a duplikátumok miatt); egy Mentes, majd a szűrő frissítése.

4. Visszavonás a törlésre

A törölt jegyzet 10 másodpercig visszahozható. Hogyan?

Megoldás megnyitása

A törölt jegyzetet egy kuka listába teszed (időbélyeggel), az állapotsor Visszavonás linkje visszateszi; az automatikus mentés a kukát nem tartalmazza, a kuka kiürülhet kilépéskor (vagy külön fájlba menthető).

Kihívás szint

5. Egyidejű példány védelme

A program ne induljon el kétszer ugyanarra a fájlra. Hogyan?

Megoldás megnyitása

Névvel ellátott Mutex a Main elején: ha a createdNew hamis, a program a már futó példányra irányítja a figyelmet, és kilép. Így nem keletkezik két példány, amely felülírja egymás mentését.

6. Adatok migrációja

A 2. verzióban a Cimkek mellé Szin mező kerül. Mit teszel a régi (1-es) fájlokkal?

Megoldás megnyitása

A Verzio alapján (10.6 lecke) a betöltés után alapértéket adsz az új mezőnek (Szin = "#FFFFFF"), és Verzio = 2 értékkel mentesz; előtte a régi fájlról automatikus .v1.bak másolat készül.

Gyors önellenőrzés

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

1. Mit tesz a program hibás jegyzetfájlnál?

Jelzi a hibát, nem írja felül a fájlt, a tartalékot vagy az üres listát ajánlja, és a hibás fájlt félreteszi.

2. Miért kell a Commit a váltás előtt?

A szerkesztő tartalma még nincs az adatban; nélküle a változtatás elvész vagy másik jegyzetbe kerül.

3. Mi a biztonságos mentés lépése?

Ideiglenes fájl, Flush(true), File.Replace biztonsági másolattal.

4. Mikor hamis a „módosult” jelző?

Csak sikeres mentés után.

5. Hogyan őrzöd meg a kijelölést a lista újraépítésekor?

Az Id alapján (nem a sorszám szerint).

6. Mit ment a Settings, és mit nem?

Ablak, téma, utolsó jegyzet, utolsó mappa: igen; a jegyzetek tartalma és a jelszavak: nem (JSON-fájl).

7. Miért kell képletvédelem a CSV-ben?

Az Excel a = kezdetű szöveget képletként hajtja végre (CSV-injekció).

Mit tudsz már? (a 10. fejezet végén)

  • A File, Path, Directory osztályokkal és adatfolyamokkal dolgozol, tudod a helyes kódolást.
  • Fájl- és mappaválasztót egységesen, hibatűrően használsz.
  • CSV-t, JSON-t és XML-t olvasol és írsz, kezeled a hibás fájlokat.
  • Az Application Settings segítségével megőrzöd a program beállításait.
  • Biztonságosan mentesz: ideiglenes fájl, csere, biztonsági másolat, érthető hibaüzenetek.
  • Elkészítettél egy valódi jegyzetkezelőt, amely nem veszíti el az adatot.

Következő fejezet: időzítők, hosszú műveletek és aszinkron feladatok

A 10. fejezet kész. A 11. fejezetben megtanulod, hogyan ne akadjon meg az ablak a hosszú műveletek (nagy fájl, hálózat, számítás) közben: időzítők, háttérszálak és async/await.