Grafikus C# – Windows Forms tanfolyam › 10.10 lecke Windows Forms · 10.10 · záró mini projekt 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, Mini projekt: jegyzetek és beállítások tárolása
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.
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.
Követelmények és pontozás
| Rész | Követelmény | Pont |
|---|---|---|
| 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ület | Vezé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). |
| Állapotsor | tsslMentes (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ó
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)
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(); }
}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
| # | Teszt | Várt eredmény |
|---|---|---|
| 1 | Első 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. |
| 3 | Jegyzetváltás mentés nélkül. | A szerkesztett szöveg megmarad (commit), nem kerül másik jegyzetbe. |
| 4 | Automatikus 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. |
| 5 | Törlés megerősítéssel, Nem válasz. | A jegyzet megmarad. |
| 6 | Rö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. |
| 7 | Keresé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. |
| 8 | A 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. |
| 9 | A főfájl törlése, a .bak megmarad. | A program a tartalékot ajánlja fel. |
| 10 | A 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. |
| 11 | A fájl zárolása (másik program), mentés. | Újrapróbálás, érthető üzenet, nincs összeomlás. |
| 12 | Export .txt és .md (ékezetes cím, tiltott karakterek a címben). | UTF-8 fájl, a fájlnév tisztítva, a tartalom helyes. |
| 13 | CSV export Excelben megnyitva (ékezetek, ;, többsoros szöveg, = kezdetű jegyzet). | Helyes oszlopok és ékezetek; a = szövegként marad. |
| 14 | JSON mentés és importálás ütközéssel (Igen, Nem, Mégse). | Helyes egyesítés; az összegzés egyezik. |
| 15 | Szöveges import (UTF-8 és Windows-1250 fájl, nagy fájl). | Helyes ékezetek; a nagy fájl kimarad; a darabszám jelentve. |
| 16 | Ablak: á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. |
| 17 | A user.config elrontása; Alaphelyzet. | A program indul (a fájl félretéve), az alapértékekkel. |
| 18 | 1000 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. 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 (Ok és javítás
.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. Hiányzik a Ok és javítás
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. A kijelölést az Ok és javítás
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”. A Ok és javítás
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. Az Ok és javítás
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 Nincs képletvédelem: használd a = kezdetű jegyzet képletként fut.Ok és javítás
Csv.Sor metódust (az ' előtag a szöveges mezőkre).
Az értékeléshez ellenőrizd az alábbi szempontokat is:
.bak megvan, a kilépéskor a mentési hiba figyelmeztet.Jegyzet), a tárolás (Tarolo, BiztonsagosFajl) és a felület szétválik; a Form1 nem ismeri a JSON részleteit.catch; a felhasználó érthető üzenetet kap; a teljes hiba a naplóban; a when szűrő a fájlkivételekre.Bővítési ötletek, ha kész vagy
Ha maradt időd, próbáld ki:
- Gazdag szöveg:
RichTextBoxfélkövér, dőlt, lista; a tárolás Markdown vagy RTF. - Jelszavas jegyzetek: titkosítás a
ProtectedDatavagy 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? A Megoldás megnyitása
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? Az Megoldás megnyitása
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? Végigmész az Megoldás megnyitása
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? 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ő).Megoldás megnyitása
Kihívás szint
5. Egyidejű példány védelme
A program ne induljon el kétszer ugyanarra a fájlra. Hogyan? Névvel ellátott Megoldás megnyitása
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 A Cimkek mellé Szin mező kerül. Mit teszel a régi (1-es) fájlokkal?Megoldás megnyitása
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. 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. A szerkesztő tartalma még nincs az adatban; nélküle a változtatás elvész vagy másik jegyzetbe kerül. Ideiglenes fájl, Csak sikeres mentés után. Az Ablak, téma, utolsó jegyzet, utolsó mappa: igen; a jegyzetek tartalma és a jelszavak: nem (JSON-fájl). Az Excel a 1. Mit tesz a program hibás jegyzetfájlnál?
2. Miért kell a Commit a váltás előtt?
3. Mi a biztonságos mentés lépése?
Flush(true), File.Replace biztonsági másolattal.4. Mikor hamis a „módosult” jelző?
5. Hogyan őrzöd meg a kijelölést a lista újraépítésekor?
Id alapján (nem a sorszám szerint).6. Mit ment a Settings, és mit nem?
7. Miért kell képletvédelem a CSV-ben?
= kezdetű szöveget képletként hajtja végre (CSV-injekció).
Mit tudsz már? (a 10. fejezet végén)
- A
File,Path,Directoryosztá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.



