Grafikus C# – Windows Forms tanfolyam › 10.4 lecke

Windows Forms · 10.4 lecke · párbeszédablakok

Fájl- és mappaválasztó párbeszédablakok

A 7.8 leckében az OpenFileDialog, a SaveFileDialog és a FolderBrowserDialog alapjait már megismerted. Egy valódi programban ugyanazt a kódot sok helyen írnád újra: ugyanaz a szűrő, ugyanaz a kezdőmappa, ugyanaz a hibakezelés. Ebben a leckében a fájlválasztást újrahasznosítható segédosztályba szervezed, megjegyezteted a felhasználó utoljára használt mappáját, ellenőrzöd az útvonalat (létezik-e, írható-e), kezeled a legutóbbi fájlok listáját és a húzással megnyitást. Ez a program kényelmes és megbízható használatát adja.

  • kb. 90 perc
  • közepes szint
  • fájlválasztás
Diák C# programozást gyakorol egy tanulósarokban

A lecke végére

  • segédosztályba szervezed a fájlválasztást (szűrők, kezdőmappa, owner);
  • megjegyzed az utolsó mappát és a legutóbbi fájlokat (listaként, a következő indításig);
  • ellenőrzöd, hogy a választott fájl létezik, olvasható, és a cél mappa írható;
  • a FilterIndex alapján választod a formátumot, és kezeled a kiterjesztést;
  • húzással és a parancssori argumentummal (Megnyitás ezzel) nyitsz meg fájlt;
  • elkerülöd a tipikus hibákat (hiányzó owner, munkamappa változása, Dispose hiánya).

Mielőtt elkezded

Ehhez a leckéhez a 7.8 lecke (a három párbeszédablak alapjai), a 10.1 lecke (Path, Directory) és a 7.1 lecke (menü) szükséges.

10.3 Kódolás →

Fájlválasztó segédosztály

Egy statikus osztály, amely a program összes fájlválasztását egy helyre gyűjti: a szűrők, a cím és a hibatűrés egységesek, a hívó egy sorban kéri a fájlt. A párbeszédablakot using blokkban hozza létre (9.8 lecke), és mindig megkapja a owner ablakot (a párbeszéd az alkalmazás ablakához kötődik, nem „lóg” mellé):

FajlValaszto.cs (using System.IO; using System.Windows.Forms;)C#
public static class FajlValaszto
{
public const string JegyzetSzuro = "Jegyzet (*.txt;*.md)|*.txt;*.md|Minden fájl (*.*)|*.*";
public const string KepSzuro = "Képek (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp";

// egy fájl megnyitása: null, ha a felhasználó megszakította
public static string Megnyit(IWin32Window tulajdonos, string szuro, string kezdoMappa, string cim = "Megnyitás")
{
using (OpenFileDialog ofd = new OpenFileDialog())
{
ofd.Title = cim;
ofd.Filter = szuro;
ofd.CheckFileExists = true;
ofd.CheckPathExists = true;
ofd.RestoreDirectory = true; // a munkamappát nem változtatja meg (10.1 lecke csapdája)

if (kezdoMappa != null && Directory.Exists(kezdoMappa)) { ofd.InitialDirectory = kezdoMappa; }

return ofd.ShowDialog(tulajdonos) == DialogResult.OK ? ofd.FileName : null;
}
}

// mentés: a szűrő sorszáma alapján a kiterjesztés; a felülírás-kérdést a párbeszéd teszi fel
public static string Ment(IWin32Window tulajdonos, string szuro, string kezdoMappa, string javasoltNev, out int szuroIndex)
{
szuroIndex = 0;

using (SaveFileDialog sfd = new SaveFileDialog())
{
sfd.Filter = szuro;
sfd.FileName = javasoltNev;
sfd.AddExtension = true; // a hiányzó kiterjesztést a szűrő szerint kiegészíti
sfd.OverwritePrompt = true;
sfd.RestoreDirectory = true;

if (kezdoMappa != null && Directory.Exists(kezdoMappa)) { sfd.InitialDirectory = kezdoMappa; }

if (sfd.ShowDialog(tulajdonos) != DialogResult.OK) { return null; }

szuroIndex = sfd.FilterIndex; // 1-től számozott: melyik formátumot választotta
return sfd.FileName;
}
}

public static string Mappa(IWin32Window tulajdonos, string kezdoMappa, string leiras)
{
using (FolderBrowserDialog fbd = new FolderBrowserDialog())
{
fbd.Description = leiras;
fbd.ShowNewFolderButton = true;

if (kezdoMappa != null && Directory.Exists(kezdoMappa)) { fbd.SelectedPath = kezdoMappa; }

return fbd.ShowDialog(tulajdonos) == DialogResult.OK ? fbd.SelectedPath : null;
}
}
}

// használat a Formban: egy sor, nincs ismétlődő beállítás
string ut = FajlValaszto.Megnyit(this, FajlValaszto.JegyzetSzuro, utolsoMappa);

if (ut != null) { Betolt(ut); }
Miért segédosztály?A szűrő, a kezdőmappa, a RestoreDirectory és az owner beállítása ismétlődik. Egy helyen javítod: egységes viselkedés, kevesebb hiba. A kapott érték null a megszakításra (egyszerűbb, mint a DialogResult kezelése minden hívásnál).
owner (tulajdonos)A ShowDialog(this) az ablakhoz köti a párbeszédet: a háttér letiltott, a párbeszéd nem tűnik el az ablak mögött. A paraméter nélküli ShowDialog() gyakori hiba (a párbeszéd egy másik ablak mögé kerülhet).
RestoreDirectoryA RestoreDirectory = true megakadályozza, hogy a párbeszéd a program munkamappáját átállítsa (10.1 lecke csapdája).
FilterIndexA FilterIndex 1-től számozott: a mentésnél ebből tudod, melyik formátumot kérte a felhasználó (a szűrő sorrendje a program döntése).

Jósolj!

Az OpenFileDialog-ot RestoreDirectory = false (alapértelmezett) beállítással nyitja meg a felhasználó, kiválaszt egy fájlt a Letöltések mappában, majd a program a File.WriteAllText("beallitas.cfg", ...) relatív útvonalra ment. Hová kerül a fájl?

Jóslat ellenőrzése

Gyakran a Letöltések mappába: a párbeszéd megváltoztathatja a folyamat munkamappáját. A RestoreDirectory = true megakadályozza, és az adatot teljes útvonalra kell menteni (10.1 lecke).

Az utolsó mappa megjegyzése

A felhasználó megszokja, hogy a program ott nyílik meg, ahol utoljára dolgozott. A mappát az alkalmazás beállításaiban tárolod (10.8 lecke: Properties.Settings); addig is egy egyszerű szöveges fájl is megteszi:

Form1.csC#
private string utolsoMappa = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

private void miMegnyit_Click(object sender, EventArgs e)
{
string ut = FajlValaszto.Megnyit(this, FajlValaszto.JegyzetSzuro, utolsoMappa);

if (ut == null) { return; }

utolsoMappa = Path.GetDirectoryName(ut); // a következő párbeszéd itt nyílik
Betolt(ut);
}
A mappa még létezik?A megjegyzett mappa megszűnhet (törölt, pendrive, hálózati meghajtó). A segédosztály ezért ellenőrzi a Directory.Exists értéket, és ha nincs, a párbeszéd az alapértelmezett helyen nyílik.
TárolásA mappát a program beállításaiba mentjük (10.8 lecke), így a következő indításkor is megvan. Ha nincs mentve, az indulás utáni első párbeszéd a Dokumentumok mappában nyílik.
Külön mappa a mentéshez?Egyes programok külön emlékeznek a megnyitás és a mentés mappájára (például a képnézegető és az exportálás). Egy közös érték egyszerűbb; külön érték kényelmesebb, ha a két feladat elválik.
Ne nyisd meg magadA felhasználó utolsó mappájának tárolása az ő adata: kerüld a személyes adatokat (teljes felhasználónév a naplóban), és a beállítások törlését kínáld fel.

Jósolj!

Az utolsó mappa egy hálózati meghajtó (Z:\Munka), amely a következő indításkor nem elérhető. A program InitialDirectory = utolsoMappa beállítással nyitja meg a párbeszédet, ellenőrzés nélkül. Mi történik?

Jóslat ellenőrzése

A párbeszéd a nem létező kezdőmappát figyelmen kívül hagyja (a rendszer alapértelmezett mappájában nyílik), vagy lassan, hálózati időtúllépés után nyílik meg. Az ellenőrzés (Directory.Exists) hálózati meghajtónál is lassú lehet: a program rövid ideig „lefagy”.

Útvonal-ellenőrzés: olvasható, írható

A CheckFileExists a választáskor ellenőriz, de a fájl a választás és a megnyitás között változhat. Mentésnél érdemes előre kipróbálni, hogy a cél mappa írható-e (különben a felhasználó hosszú munka végén kap hibát):

Form1.csC#
// érvényes-e az útvonal és a fájlnév? (hiba esetén a szövege a hibaüzenet)
private bool UtvonalEllenorzes(string ut, out string hiba)
{
hiba = "";

if (string.IsNullOrWhiteSpace(ut)) { hiba = "Nincs megadva fájl."; return false; }

if (ut.IndexOfAny(Path.GetInvalidPathChars()) >= 0) { hiba = "Az útvonal érvénytelen karaktert tartalmaz."; return false; }

string mappa = Path.GetDirectoryName(Path.GetFullPath(ut));

if (!Directory.Exists(mappa)) { hiba = "A mappa nem létezik."; return false; }

return true;
}

// írásteszt: ideiglenes fájl létrehozása a célmappában
private bool MappaIrhato(string mappa)
{
try
{
string proba = Path.Combine(mappa, Guid.NewGuid().ToString("N") + ".tmp");

using (FileStream fs = new FileStream(proba, FileMode.CreateNew, FileAccess.Write, FileShare.None, 1, FileOptions.DeleteOnClose)) { }

return true;
}
catch (UnauthorizedAccessException) { return false; }
catch (IOException) { return false; }
}
DeleteOnCloseA FileOptions.DeleteOnClose az ideiglenes fájlt a lezáráskor automatikusan törli, ezért nem marad szemét a mappában, még hiba esetén sem.
Csak jelzésAz írásteszt pillanatfelvétel: a tényleges mentés közben a helyzet változhat (megtelt a lemez). A tényleges mentést továbbra is try–catch védje (10.9 lecke); az előzetes teszt a felhasználó kényelmét szolgálja.
Érvénytelen útvonalA GetFullPath érvénytelen karakternél ArgumentException kivételt dob, ezért előbb a GetInvalidPathChars ellenőrzés. A párbeszéd maga érvényes útvonalat ad; a kézzel beírt vagy a fájlból olvasott útvonalat kell védeni.
Az útvonal hosszaA régi Windows API-k 260 karakteres útvonalhatárt használnak (PathTooLongException). A mélyen egymásba ágyazott mappák ezt átléphetik: a hibát érthetően jelezd.

Jósolj!

A felhasználó a C:\Program Files mappát választja mentési helynek egy átlagos (nem rendszergazda) fiókkal. A SaveFileDialog elfogadja, és a File.WriteAllText hívás fut le. Mi történik?

Jóslat ellenőrzése

UnauthorizedAccessException (vagy a rendszer a fájlt a VirtualStore mappába irányítja régebbi programoknál). Az előzetes MappaIrhato teszt jelezhette volna, mielőtt a felhasználó a munkáját „elmentettnek” gondolja.

Legutóbbi fájlok és húzással megnyitás

Form1.cs (using System.Collections.Generic; using System.Linq;)C#
private const int MaxLegutobbi = 8;
private List<string> legutobbi = new List<string>();

// egy fájl megnyitása után: előre kerül, nincs duplikátum, korlátozott darabszám
private void LegutobbiHozzaad(string ut)
{
legutobbi.RemoveAll(x => string.Equals(x, ut, StringComparison.OrdinalIgnoreCase)); // a Windows útvonalai betűnagyság-függetlenek
legutobbi.Insert(0, ut);

if (legutobbi.Count > MaxLegutobbi) { legutobbi.RemoveRange(MaxLegutobbi, legutobbi.Count - MaxLegutobbi); }

LegutobbiMenuFrissit();
}

private void LegutobbiMenuFrissit()
{
miLegutobbi.DropDownItems.Clear();

for (int i = 0; i < legutobbi.Count; i++)
{
string ut = legutobbi[i];
ToolStripMenuItem elem = new ToolStripMenuItem("&" + (i + 1) + " " + Path.GetFileName(ut));

elem.ToolTipText = ut;
elem.Click += (s, ev) =>
{
if (File.Exists(ut)) { Betolt(ut); }
else
{
MessageBox.Show(this, "A fájl már nem létezik, eltávolítom a listáról.", "Megnyitás");
legutobbi.Remove(ut);
LegutobbiMenuFrissit();
}
};

miLegutobbi.DropDownItems.Add(elem);
}

miLegutobbi.Enabled = legutobbi.Count > 0;
}

// húzással megnyitás (AllowDrop = true a Formon)
private void Form1_DragEnter(object sender, DragEventArgs e)
{
e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
}

private void Form1_DragDrop(object sender, DragEventArgs e)
{
string[] fajlok = (string[])e.Data.GetData(DataFormats.FileDrop);

if (fajlok.Length > 0 && File.Exists(fajlok[0])) { Betolt(fajlok[0]); }
}

// a program indításakor: a parancssori argumentum (a „Megnyitás ezzel” vagy dupla kattintás átadja a fájlt)
// Program.cs: Application.Run(new Form1(args.Length > 0 ? args[0] : null));
Legutóbbi lista szabályaiAz új elem előre kerül; a már szereplő nem duplázódik; a lista mérete korlátozott; a megszűnt fájlt a program a kattintásnál észleli és eltávolítja (nincs lassú ellenőrzés indításkor).
BetűnagyságA Windows útvonalai nem érzékenyek a betűnagyságra: az összehasonlítás OrdinalIgnoreCase, különben ugyanaz a fájl kétszer szerepelne.
HúzásA FileDrop a húzott fájlok útvonalait adja: bármi lehet (mappa, nem támogatott típus). A File.Exists és a típus ellenőrzése kötelező.
Parancssori argumentumHa a felhasználó a Fájlkezelőben a program típusú fájlra duplán kattint, a Windows a fájl útvonalát a program első argumentumaként adja át. A Main(string[] args) így továbbadható a Form1 konstruktorának.

Jósolj!

A legutóbbi lista kezelése legutobbi.Add(ut) (a végére), ellenőrzés nélkül. A felhasználó háromszor nyitja meg ugyanazt a fájlt. Mit mutat a menü?

Jóslat ellenőrzése

Ugyanazt a fájlt háromszor, és a legutóbb megnyitott a lista végén áll. A RemoveAll és az Insert(0, ...) páros kiszűri a duplikátumot, és az újat előre teszi.

📷 Képernyőkép helye

Mit tegyél ide: a dokumentumkezelő Fájl menüje a legutóbbi fájlok listájával, és egy ablak, amelyre fájlt húznak.Javasolt fájlnév: winforms-10-4-legutobbi.webp · Alt szöveg: Legutóbbi fájlok menü és húzással megnyitás

Hibakeresés: te vagy a hibadetektív

Mi a baj, és hogyan javítod?

Gondold végig az okot, és csak utána nyisd le a megoldást.

1. A párbeszéd az ablak mögé kerül

ofd.ShowDialog();   // owner nélkül
Hiba és javítás

Add át a tulajdonos ablakot: ofd.ShowDialog(this). Így a párbeszéd az ablak előtt marad, és a háttér letiltott.

2. A beállítás-fájl rossz helyre kerül a párbeszéd után

Fájl megnyitása után a relatív útvonalú mentés „eltűnik”.

Ok és javítás

A RestoreDirectory hamis: a munkamappa megváltozott. Állítsd true értékre, és teljes útvonalat használj (10.1 lecke).

3. Dupla kiterjesztés: jegyzet.txt.txt

string ut = sfd.FileName + ".txt";   // a felhasználó már beírta a .txt-t
Hiba és javítás

Az AddExtension = true és a DefaultExt a hiányzó kiterjesztést adja; kézi hozzáfűzés előtt ellenőrizd a Path.GetExtension(ut) értékét.

4. A felhasználó a „Minden fájl” szűrőnél nem támogatott fájlt nyit meg

A program egy .exe fájlt próbál szövegként beolvasni.

Ok és javítás

A „Minden fájl” szűrő bármit enged. A betöltés előtt ellenőrizd a kiterjesztést vagy a tartalmat (10.9 lecke), és érthető hibát mutass.

5. Nincs Mégse kezelés

ofd.ShowDialog(this);
Betolt(ofd.FileName); // Mégse esetén üres útvonal
Hiba és javítás

Vizsgáld a DialogResult.OK eredményt (vagy a segédosztály null visszatérését).

6. A legutóbbi fájlok lista nem őrződik meg

Újraindítás után üres a lista.

Ok és javítás

A listát csak a memóriában tartod. Mentsd el a beállításokba (10.8 lecke) vagy fájlba a FormClosing-ben, és töltsd be induláskor.

Mini projekt: dokumentumkezelő segédosztállyal

Készíts egyszerű szövegszerkesztőt, amelynek a fájlkezelése tiszta és megbízható:

  1. Felület: menü (Fájl: Új, Megnyitás..., Legutóbbi fájlok, Mentés, Mentés másként..., Kilépés), txtSzoveg (többsoros, Dock = Fill), állapotsor (fájlnév, mappa, módosított jelző).
  2. Segédosztály: a lecke FajlValaszto osztálya (Megnyit, Ment, Mappa); minden párbeszéd ezen megy át.
  3. Megnyitás: kiterjesztés-szűrő (.txt, .md); a betöltés előtt a méret ellenőrzése (> 5 MB megerősítés); a kódolás felismerése (10.3 lecke: BOM → érvényes UTF-8 → Windows-1250); hibatűrés (try–catch).
  4. Mentés és Mentés másként: a Mentés a megnyitott fájlba ír, ha nincs, a Mentés másként ablakot nyitja; a MappaIrhato előzetes teszt; a mentés kódolása UTF-8 (BOM nélkül), a FilterIndex szerint .txt vagy .md; a felülírás kérdése a párbeszéden.
  5. Módosított jelző: a cím * jelet kap, a kilépés/új/megnyitás előtt megerősítés (Igen/Nem/Mégse, 7.4 lecke).
  6. Utolsó mappa: a megnyitás és a mentés közös utolsoMappa értékét a program megjegyzi; ha a mappa megszűnt, az alapértelmezett helyen nyílik.
  7. Legutóbbi fájlok: legfeljebb 8, duplikátum nélkül, előre kerül az új; megszűnt fájlnál a program eltávolítja; a lista a legutobbi.txt fájlban az adatmappában marad (induláskor betöltve, kilépéskor mentve: 10.1 lecke AdatMappa()).
  8. Húzás és parancssor: a fájl az ablakra húzással megnyílik; a program első parancssori argumentumát is megnyitja.
  9. Teszteld: Mégse minden párbeszédnél, nem létező és csak olvasható mappa, fájl megnyitása időközben törölt mappából, ugyanaz a fájl többször (lista), kiterjesztés nélküli név mentése, húzás mappával, nagy fájl, ékezetes és Windows-1250 fájl.
Megoldás megnyitása (próbáld ki előbb önállóan!)

A segédosztály a leckében látható. Itt a Form vezérlése és a legutóbbi fájlok tárolása áll.

Form1.cs (using System.IO; using System.Text; using System.Linq; using System.Collections.Generic;)C#
private string fajl = "";
private bool modosult = false;
private string utolsoMappa = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
private List<string> legutobbi = new List<string>();
private string indulo;

public Form1(string induloFajl)
{
InitializeComponent();
indulo = induloFajl;
this.AllowDrop = true;
}

private string AdatFajl()
{
string mappa = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MiniSzerkeszto");

Directory.CreateDirectory(mappa);

return Path.Combine(mappa, "legutobbi.txt");
}

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

try { if (File.Exists(AdatFajl())) { legutobbi = File.ReadAllLines(AdatFajl()).Where(s => s.Trim().Length > 0).Take(8).ToList(); } }
catch (IOException) { } // a lista nem létfontosságú

LegutobbiMenuFrissit();
CimFrissit();

if (indulo != null && File.Exists(indulo)) { Betolt(indulo); }
}

private void CimFrissit()
{
this.Text = (fajl.Length == 0 ? "Névtelen" : Path.GetFileName(fajl)) + (modosult ? "*" : "") + " – Szerkesztő";
tsslFajl.Text = fajl.Length == 0 ? "Nincs mentve" : fajl;
}

private bool Folytathat()
{
if (!modosult) { return true; }

switch (MessageBox.Show(this, "Mented a változtatásokat?", "Szerkesztő", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning))
{
case DialogResult.Yes: return Ment(false);
case DialogResult.No: return true;
default: return false;
}
}

private void txtSzoveg_TextChanged(object sender, EventArgs e)
{
if (!modosult) { modosult = true; CimFrissit(); }
}

private bool Betolt(string ut)
{
if (!Folytathat()) { return false; }

try
{
FileInfo fi = new FileInfo(ut);

if (fi.Length > 5 * 1024 * 1024 && MessageBox.Show(this, "A fájl nagyobb 5 MB-nál. Betöltsem?", "Nagy fájl", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) != DialogResult.Yes) { return false; }

byte[] bajtok = File.ReadAllBytes(ut);
Encoding kod;

if (bajtok.Length >= 3 && bajtok[0] == 0xEF && bajtok[1] == 0xBB && bajtok[2] == 0xBF) { kod = new UTF8Encoding(true); }
else
{
try { new UTF8Encoding(false, true).GetString(bajtok); kod = new UTF8Encoding(false); }
catch (DecoderFallbackException) { kod = Encoding.GetEncoding(1250); }
}

string szoveg = kod.GetString(bajtok);

if (szoveg.Length > 0 && szoveg[0] == '\uFEFF') { szoveg = szoveg.Substring(1); } // a BOM karakter levágása

txtSzoveg.TextChanged -= txtSzoveg_TextChanged;
txtSzoveg.Text = szoveg;
txtSzoveg.TextChanged += txtSzoveg_TextChanged;

fajl = ut;
modosult = false;
utolsoMappa = Path.GetDirectoryName(ut);
LegutobbiHozzaad(ut);
CimFrissit();
return true;
}
catch (IOException ex) { MessageBox.Show(this, "A fájl nem olvasható: " + ex.Message, "Megnyitás", MessageBoxButtons.OK, MessageBoxIcon.Error); }
catch (UnauthorizedAccessException) { MessageBox.Show(this, "Nincs jogosultság a fájlhoz.", "Megnyitás", MessageBoxButtons.OK, MessageBoxIcon.Error); }

return false;
}

private bool Ment(bool maskent)
{
string ut = fajl;

if (maskent || ut.Length == 0)
{
int szuroIndex;

ut = FajlValaszto.Ment(this, "Szöveg (*.txt)|*.txt|Markdown (*.md)|*.md", utolsoMappa, fajl.Length == 0 ? "jegyzet" : Path.GetFileNameWithoutExtension(fajl), out szuroIndex);

if (ut == null) { return false; }

if (Path.GetExtension(ut).Length == 0) { ut += szuroIndex == 2 ? ".md" : ".txt"; }
}

string mappa = Path.GetDirectoryName(ut);

if (!MappaIrhato(mappa)) { MessageBox.Show(this, "Ebbe a mappába nem lehet írni. Válassz másik helyet.", "Mentés", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; }

try
{
File.WriteAllText(ut, txtSzoveg.Text, new UTF8Encoding(false));
fajl = ut;
modosult = false;
utolsoMappa = mappa;
LegutobbiHozzaad(ut);
CimFrissit();
return true;
}
catch (IOException ex) { MessageBox.Show(this, "A mentés nem sikerült: " + ex.Message, "Mentés", MessageBoxButtons.OK, MessageBoxIcon.Error); }
catch (UnauthorizedAccessException) { MessageBox.Show(this, "Nincs jogosultság a fájlhoz.", "Mentés", MessageBoxButtons.OK, MessageBoxIcon.Error); }

return false;
}

private bool MappaIrhato(string mappa)
{
try
{
string proba = Path.Combine(mappa, Guid.NewGuid().ToString("N") + ".tmp");

using (FileStream fs = new FileStream(proba, FileMode.CreateNew, FileAccess.Write, FileShare.None, 1, FileOptions.DeleteOnClose)) { }

return true;
}
catch (UnauthorizedAccessException) { return false; }
catch (IOException) { return false; }
}

private void miMegnyit_Click(object sender, EventArgs e)
{
string ut = FajlValaszto.Megnyit(this, FajlValaszto.JegyzetSzuro, utolsoMappa);

if (ut != null) { Betolt(ut); }
}

private void miMent_Click(object sender, EventArgs e) { Ment(false); }
private void miMentMaskent_Click(object sender, EventArgs e) { Ment(true); }

private void miUj_Click(object sender, EventArgs e)
{
if (!Folytathat()) { return; }

txtSzoveg.TextChanged -= txtSzoveg_TextChanged;
txtSzoveg.Clear();
txtSzoveg.TextChanged += txtSzoveg_TextChanged;
fajl = "";
modosult = false;
CimFrissit();
}

private void LegutobbiHozzaad(string ut)
{
legutobbi.RemoveAll(x => string.Equals(x, ut, StringComparison.OrdinalIgnoreCase));
legutobbi.Insert(0, ut);

if (legutobbi.Count > 8) { legutobbi.RemoveRange(8, legutobbi.Count - 8); }

LegutobbiMenuFrissit();
}

private void LegutobbiMenuFrissit()
{
miLegutobbi.DropDownItems.Clear();

for (int i = 0; i < legutobbi.Count; i++)
{
string ut = legutobbi[i];
ToolStripMenuItem elem = new ToolStripMenuItem("&" + (i + 1) + " " + Path.GetFileName(ut)) { ToolTipText = ut };

elem.Click += (s, ev) =>
{
if (File.Exists(ut)) { Betolt(ut); }
else { MessageBox.Show(this, "A fájl már nem létezik, eltávolítom a listáról.", "Megnyitás"); legutobbi.Remove(ut); LegutobbiMenuFrissit(); }
};

miLegutobbi.DropDownItems.Add(elem);
}

miLegutobbi.Enabled = legutobbi.Count > 0;
}

private void Form1_DragEnter(object sender, DragEventArgs e)
{
e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
}

private void Form1_DragDrop(object sender, DragEventArgs e)
{
string[] fajlok = (string[])e.Data.GetData(DataFormats.FileDrop);

if (fajlok.Length > 0 && File.Exists(fajlok[0])) { Betolt(fajlok[0]); }
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing && !Folytathat()) { e.Cancel = true; return; }

try { File.WriteAllLines(AdatFajl(), legutobbi); }
catch (IOException) { } // a lista mentése nem akadályozhatja a kilépést
}
📷 Képernyőkép helye

Mit tegyél ide: a kész szövegszerkesztő a Fájl menüvel és az állapotsorral.Javasolt fájlnév: winforms-10-4-kesz.webp · Alt szöveg: A kész szövegszerkesztő

Három önálló gyakorlófeladat

Egyszerre csak egyet csinálj: tervezd meg, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást.

1. Többes fájlválasztás

Több képfájlt választ ki a felhasználó (Multiselect). Hogyan adja vissza a segédosztály, és mit tesz a nagy darabszámmal?

Egy lehetséges megoldás

A MegnyitTobb metódus string[] értéket ad (ofd.FileNames). A hívó korlátozhatja a darabszámot (például 200).

FajlValaszto.csC#
public static string[] MegnyitTobb(IWin32Window tulajdonos, string szuro, string kezdoMappa){    using (OpenFileDialog ofd = new OpenFileDialog())    {        ofd.Filter = szuro;        ofd.Multiselect = true;        ofd.RestoreDirectory = true;        if (kezdoMappa != null && Directory.Exists(kezdoMappa)) { ofd.InitialDirectory = kezdoMappa; }        return ofd.ShowDialog(tulajdonos) == DialogResult.OK ? ofd.FileNames : null;    }}

2. Mentés biztonsági másolattal

Mentés előtt a régi fájl .bak másolatot kap. Mi a sorrend, és mi történik hiba esetén?

Egy lehetséges megoldás

Előbb a régi fájl másolása (File.Copy(ut, ut + ".bak", true)), utána az új tartalom írása; ha az írás hibázik, a .bak a régi állapotot őrzi. A biztonságosabb módszert (ideiglenes fájl + csere) a 10.9 lecke mutatja.

3. Fájltípus megjelenítése

A legutóbbi fájlok menüjében a fájl típusát ikon jelezze. Mit használsz?

Egy lehetséges megoldás

Icon.ExtractAssociatedIcon(ut) (a fájlhoz társított ikon); az Icon erőforrás: a ToolStripMenuItem.Image beállítása után az ikont és a belőle készült képet is fel kell szabadítani (9.8 lecke). Nagy listánál gyorsítótárazd kiterjesztésenként.

Gyakorlótár: 6 további feladat

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

Alap szint

1. Szűrő összeállítása

Írd meg a szűrőt: Képek (png, jpg) és Minden fájl.

Megoldás megnyitása

"Képek (*.png;*.jpg)|*.png;*.jpg|Minden fájl (*.*)|*.*": párosával: leírás|minta.

2. Mégse kezelése

Mit ad vissza az ofd.ShowDialog(this), ha a felhasználó a Mégse gombot nyomja?

Megoldás megnyitása

DialogResult.Cancel; ilyenkor nem szabad a FileName értékét használni.

Közepes szint

3. Kiterjesztés kikényszerítése

A mentés csak .txt és .md fájlt engedjen. Mit teszel, ha a felhasználó .exe nevet ír?

Megoldás megnyitása

A kiterjesztés ellenőrzése (Path.GetExtension, OrdinalIgnoreCase), nem engedélyezett esetén érthető üzenet és a párbeszéd újranyitása, vagy a kiterjesztés cseréje a szűrő szerinti értékre.

4. A legutóbbi lista tárolása

Hol tárolod a legutóbbi fájlok listáját, és miért nem a program mappájában?

Megoldás megnyitása

A felhasználó %AppData% mappájában (10.1 lecke): a program mappája telepítve általában nem írható, és a lista felhasználónként más.

Kihívás szint

5. Rögzített fájlok a listán

A felhasználó „kitűzhet” fájlokat a legutóbbi lista tetejére. Hogyan bővíted a modellt?

Megoldás megnyitása

Kis osztály (LegutobbiElem { Ut, Kituzott }); a rendezés a kitűzöttek elöl, a többi dátum szerint; a limit csak a nem kitűzötteket érinti. Mentéskor a kitűzés jelzője (például * előtag, vagy JSON a 10.6 leckéből).

6. Fájl nyitva tartása közben megváltozik

A megnyitott fájlt egy másik program módosítja. Hogyan veszi észre a szerkesztő?

Megoldás megnyitása

A FileSystemWatcher (Changed esemény) vagy a mentés előtti LastWriteTime összehasonlítás a betöltéskor tárolt értékkel; eltérésnél a program rákérdez: Felülírod a másik programban végzett módosítást?

Gyors önellenőrzés

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

1. Miért segédosztály?

Az ismétlődő beállítások (szűrő, kezdőmappa, owner) egy helyen vannak; egységes és könnyen javítható.

2. Miért kell az owner?

A párbeszéd az ablakhoz kötődik: nem kerül mögé, a háttér letiltott.

3. Mit tesz a RestoreDirectory?

Megakadályozza, hogy a párbeszéd átállítsa a program munkamappáját.

4. Mire jó a FilterIndex?

Megmondja, melyik szűrő (formátum) volt kiválasztva (1-től számozva).

5. Hogyan ellenőrzöl írhatóságot?

Ideiglenes fájl létrehozásával (DeleteOnClose); a tényleges mentést ettől függetlenül try–catch védi.

6. Milyen szabályai vannak a legutóbbi listának?

Előre kerül az új, nincs duplikátum (OrdinalIgnoreCase), korlátozott darabszám, a megszűnt fájl eltávolítása.

7. Miért kell a húzott fájlokat ellenőrizni?

Bármi lehet (mappa, nem támogatott típus): a File.Exists és a típusvizsgálat kötelező.

Mit tudsz már?

  • A fájlválasztást egységes segédosztályba szervezed.
  • Megjegyzed az utolsó mappát és a legutóbbi fájlokat.
  • Ellenőrzöd az útvonalat és az írhatóságot, és kezeled a Mégse esetet.
  • Húzással és parancssorral is megnyitsz fájlt.
  • Elkerülöd a gyakori párbeszéd-hibákat.

Következő lecke: CSV fájl kezelése

A 8.9 lecke mélyítése: idézőjeles és többsoros mezők, elválasztó felismerése, oszlopok megfeleltetése, Excel-barát mentés és a képletbeszúrás elleni védelem.