Kihagyás

3. HF - Felhasználói felület kialakítása

Bevezetés

A feladatok gyakorlati hátteréül a kapcsolód előadások és a A felhasználói felület kialakítása laborgyakorlat szolgál.

A fentiekre építve, jelen önálló gyakorlat feladatai a feladatleírást követő rövidebb iránymutatás segítségével elvégezhetők.

Az önálló gyakorlat célja:

  • Windows Forms tervező használatának gyakorlása
  • Alapvető vezérlők (gomb, szövegdoboz, menük, listák) használatának gyakorlása
  • Eseményvezérelt programozás gyakorlása
  • Grafikus megjelenítés gyakorlása Windows Forms technológiával

A szükséges fejlesztőkörnyezet: Visual Studio (a ".NET Desktop development” Workloadnak telepítve kell lennie az installerében).

A beadás menete

❗ Bár az alapok hasonlók, vannak lényeges, a folyamatra és követelményekre vonatkozó eltérések a korábbi házi feladatokhoz képest, így mindenképpen figyelmesen olvasd el a következőket.

  • Az alapfolyamat megegyezik a korábbiakkal. GitHub Classroom segítségével hozz létre magadnak egy repository-t. A meghívó URL-t Moodle-ben találod (a tárgy nyitóoldalán a "GitHub classroom hivatkozások a házi feladatokhoz" hivatkozásra kattintva megjelenő oldalon látható). Fontos, hogy a megfelelő, ezen házi feladathoz tartozó meghívó URL-t használd (minden házi feladathoz más URL tartozik). Klónozd le az így elkészült repository-t. Ez tartalmazni fogja a megoldás elvárt szerkezetét. A feladatok elkészítése után commit-old és push-old a megoldásod.
  • A kiklónozott fájlok között a WinFormExpl.sln-t megnyitva kell dolgozni.
  • ❗ Az egyes feladatok leírásánál Külön megjelöltük (olyan stílusban, mint ahogy itt az előző szövegrészt látod) azokat az azonosítókat, szövegeket, melyeknél fontos, hogy a beadott feladatban a megadott érték szerepeljen.
  • ❗ A feladatok kérik, hogy készíts képernyőképet a megoldás egy-egy részéről, mert ezzel bizonyítod, hogy a megoldásod saját magad készítetted. A képernyőképek elvárt tartalmát a feladat minden esetben pontosan megnevezi. A képernyőképeket a megoldás részeként kell beadni, a repository-d gyökérmappájába tedd (a neptun.txt mellé). A képernyőképek így felkerülnek GitHub-ra a git repository tartalmával együtt. Mivel a repository privát, azt az oktatókon kívül más nem látja. Amennyiben olyan tartalom kerül a képernyőképre, amit nem szeretnél feltölteni, kitakarhatod a képről.

Elnevezések

❗ Az alábbiakban, a feladatok leírása során bizonyos elnevezések ennek a mintának megfelelő kiemelt szövegstílussal szerepelnek. Lényeges, hogy ezeknél pontosan kövesd az elnevezést, máskülönben a megoldás nem lesz elfogadható (a megoldások részben automata ellenőrzővel kerülnek majd kiértékelésre, mely épít ezekre, emiatt van ennek jelentősége).

Visual Studio designer hiba

Az alábbiakat csak akkor érdemes kinyitni és megnézni, ha valamiért nem nyílik meg Visual Studioban az űrlap szerkesztőfelülete.

Ha nem nyílik meg az űrlap szerkesztésre

A Visual Studio 2022 a Git-ből frissen kiklónozott forrás esetén (amikor még nem létezik egy .csproj.user kiterjesztésű fájl) az űrlapokat - valószínűsíthetően egy bug miatt – időnként nem hajlandó megnyitni szerkesztő módban (szerencsére ez nagyon ritka). A solution megnyitása után ez esetben ezt látjuk:

VS designer probléma

A probléma az, hogy a Form1.cs előtti ikon (pirossal bekeretezve) nem egy űrlap, hanem egy zöld C# ikon. Ez esetben hiába kattintunk duplán a fájlon, nem az űrlap szerkesztő nyílik meg, hanem csak a forrásfájl. A megoldás ez esetben a következő: a Build menüben válasszuk ki a „Rebuild solution” menüt, majd a Build menüben a „Clean solution” menüt, és várjunk egy kicsit. Ekkor pár másodperc múlva a Solution Explorerben az űrlapunk ikonja megváltozik:

VS designer probléma javítva

Most már meg tudjuk nyitni az űrlapot szerkesztésre, ha duplán kattintunk a Solution Explorerben a fenti csomóponton.

Feladat 1- Menü

Bevezető feladat

❗ A főablak fejléce a "MiniExplorer" szöveg legyen, hozzáfűzve a saját Neptun kódod: (pl. "ABCDEF" Neptun kód esetén "MiniExplorer - ABCDEF”), fontos, hogy ez legyen a szöveg! Ehhez az űrlapunk Text tulajdonságát állítsuk be erre a szövegre.

Feladat

Vezessünk be egy menüsort a főablakunk (MainForm) tetején. A menüben egyetlen elem legyen "File” néven, két almenüvel:

  • Open: később adunk neki funkciót
  • Exit: kilép az alkalmazásból

❗ Lényeges, hogy a menük szövegei a fent megadottak legyenek!

Megoldás

  1. Húzzunk be a felületre egy MenuStrip vezérlőt.
  2. A MenuStrip vezérlő bal szélén megjelenő szövegdobozba írjuk be, hogy "File”, ezzel létrehoztuk a főmenüt.
  3. Az újonnan létrehozott főmenüt kijelölve hozzuk létre a két almenüt.
  4. Egyesével kijelölgetve a menüelemeket, töltsük ki a nevüket (miOpen, miExit).

    ❗ A vezérlőknek csak a Name tulajdonságát állítsd, az AccessibleName-t ne. Ez a későbbi feladatokra is vonatkozik.

  5. Valósítsuk meg a kilépés funkciót a kapcsolódó gyakorlathoz hasonlóan.

Feladat 2 – Dialógusablak

A Windows Forms világban gyakran fordul elő, hogy egyedi vezérlőket, vagy űrlap típusokat akarunk definiálni, továbbá ezek és a programunk többi része között információt akarunk átadni. A következő feladat erre mutat példát.

Feladat

Készíts egy új űrlap/ablak (Form) típust InputDialog néven (a fejléce is legyen InputDialog), mely egy szövegdobozt (TextBox) és Path feliratú Label-t, továbbá egy Ok és egy Cancel feliratú gombot tartalmaz. Az űrlap gombokkal történő bezáráshoz állítsd be a két gomb DialogResult tulajdonságát DialogResult.OK és DialogResult.Cancel értékre, majd az űrlap AcceptButton és CancelButton tulajdonságait a nekik megfelelő értékekre. Az űrlap ezen felül tartalmazzon egy publikus, string típusú, Path nevű tulajdonságot (mellyel a szövegdoboz szövegét lehet lekérni és változtatni)!

Az űrlap tartalma arányosan változzon az átméretezés során:

  • TextBox szélessége növekedjen (a helye és magassága ne változzon).
  • Az űrlap átméretezésekor a gombok a hozzájuk közelebbi sarokhoz képest rögzített pozícióban maradjanak (mind x mind y koordináta tekintetében, az ablak szélességének és magasságának állításakor is). Az Ok gomb legyen bal alsó, a Cancel pedig jobb alsó sarokhoz rögzítve.

Kössük be az elkészített ablakunkat a főablakba! Az Open feliratú almenü kattintásra modálisan (ShowDialog) nyisson meg egy példányt az új ablakból.

Megoldás

A feladatot próbáld meg önállóan megoldani, majd a lenti leírás alapján ellenőrizd a megoldásod!

Megoldás
  1. Adjunk hozzá a projektünkhöz egy új űrlap típust (projekten jobb klikk, majd Add / Form (Windows Forms), a neve legyen InputDialog.

  2. Adjunk az űrlaphoz egy TextBox, egy Label és két Button vezérlőt. Rendezzük el őket a felületen és állítsuk be a tulajdonságaikat:

    • TextBox
      • Name: tPath
    • Button
      • Name: bOk
      • Text: "Ok"
      • DialogResult: OK
    • Button
      • Name: bCancel
      • Text: "Cancel"
      • DialogResult: Cancel
    • Label
      • Text: "Path"
    • InputDialog (maga a Form)
      • AcceptButton: bOk
      • CancelButton: bCancel

    A dialógusablak elkészítésekor kihasználjuk azt, hogy egy modális dialógusablakot nem csak a Close utasítással lehet bezárni, hanem úgy is, ha értéket adunk a DialogResult tulajdonságának. Ezt kódból is megtehettük volna, de mi most a gombok erre szolgáló mechanizmusát használtuk a Form Accept és Cancel button tulajdonságaival.

  3. Az egyes vezérlők Anchor tulajdonságainak beállításaival érjük el, hogy az ablak tartalma arányosan változzon az átméretezés során: a TextBox szélessége növekedjen, a gombok pedig a hozzájuk közelebbi sarokhoz képest rögzített pozícióban maradjanak (mind x mind y koordináta tekintetében, az ablak szélességének és magasságának állításakor is).

  4. Vegyünk fel egy Path nevű tulajdonságot az InputDialog.cs fájlba, mely a TextBox tartalmát teszi elérhetővé az osztályon kívülről is. (A tervezői nézet és a forrásnézet között az F7 billentyűvel válthatunk.)

    public string Path
    {
        get { return tPath.Text; }
        set { tPath.Text = value; }
    }
    
  5. Kössük be a dialógusablakot a főablakba! Ehhez kattintsunk duplán a Open menüelemre és írjuk meg a dialógusablak létrehozásának és megjelenítésének kódját.

    private void miOpen_Click(object sender, EventArgs e)
    {
        var dlg = new InputDialog();
        if (dlg.ShowDialog() == DialogResult.OK)
        {
            string result = dlg.Path;
            MessageBox.Show(result);
            // TODO: további lépések...
        }
    }
    

    Elnevezések

    A WinForms világban rendkívül gyakori, hogy egy adott információ különböző szintű elérésért egy vezérlő és egy tulajdonság is felel (mint esetünkben a tPath szövegdoboz és a Path tulajdonság). A vezérlők neveinek prefixálásával (amit itt is alkalmaztunk) elkerülhetjük a nem kívánt névütközéseket.

    ❗ A MessageBox.Show(result); sort kommentezzük is ki, a későbbiekben zavaró lenne.

BEADANDÓ

❗ Mielőbb továbbmennél a következő feladatra, egy képernyőmentést kell készítened Feladat2.png néven az alábbiaknak megfelelően:

  • Indítsd el az alkalmazást. Ha szükséges, méretezd át kisebbre, hogy ne foglaljon sok helyet a képernyőn,
  • a „háttérben” a Visual Studio legyen, a MainForm.cs megnyitva,
  • a VS View / Full Screen menüjével kapcsolj ideiglenesen Full Screen nézetre, hogy a zavaró panelek ne vegyenek el semmi helyet,
  • VS-ben zoomolj úgy, hogy a fájl teljes tartalma, az előtérben pedig az alkalmazásod ablaka legyen látható.

Amiatt ne aggódj, ha a képen a szöveg esetleg nehezen kiolvasható.

Feladat 3 – Fájlkezelő

Feladat

A meglévő kódunkból kiindulva valósíts meg egy fájl nézegető alkalmazást.

  • Az alkalmazás felületét osszuk két részre (erre SplitContainer-t használjunk, a neve maradjon az alapértelmezett splitContainer1).

  • Miután a felhasználó az Open menüponttal bekért egy mappa útvonalat (pl. c:\windows) a korábban elkészített InputDialog felhasználásával, a bal oldalon egy ListView vezérlő segítségével listázzuk ki az adott mappában található fájlok neveit és méreteit két külön oszlopban (Name és Size fejlécű oszlopok). A méret oszlop a fájl méretét jelenítse meg byte-ban, csak a számot, mindenféle mértékegység hozzáfűzése nélkül.

  • A form jobb oldalát egy fix magasságú – vagyis az ablak átméretezésekor a magassága ne változzon - Panel (a neve legyen: detailsPanel) és egy alatta (és nem rajta!) elhelyezkedő többsoros szövegdoboz (neve tContent) töltse ki. A szövegdoboz akkor is töltse ki a teret, ha az ablakot a felhasználó nagyobbra/kisebbre méretezi át!

  • A panelen mindig az aktuálisan kiválasztott fájl nevét és létrehozásának dátumát mutassuk egy lName illetve lCreated nevű Label típusú vezérlő segítségével.

    Lényeges, hogy a kiválasztás nem dupla egérkattintást jelent (egy elemet ki lehet választani pl. szimpla egér kattintással, billentyűvel stb.). Az lName szövege pontosan a fájl neve legyen, mindenféle prefix (pl. "Name:” és hasonlók) nélkül. Ugyanez igaz az lCreated vonatkozásában. A "prefixek”-hez külön Label vezérlőt használj a name és a created vonatkozásában is.

  • A ListView FullRowSelect tulajdonságát állítsd true ra (enélkül a tesztek nem futnak le jól majd).

  • Amennyiben a felhasználó a fájllistából egy fájlon duplán kattint, a többsoros szövegdobozban jelenítsük meg a fájl tartalmát szöveges formátumban. Lényeges, hogy csak a dupla kattintás számít ebben tekintetben, tehát ha a felhasználó simán (duplakattintás nélkül) más fájlt választ ki, a szövegdoboz tartalma nem változhat.

Megoldás

A feladat megoldásához a kapcsolódó gyakorlatban már alkalmazott, illetve az itt korábban megismert elemeket kell alkalmazni és kombinálni. A megoldás lépéseit csak nagy vonalakban adjuk meg, néhány kiegészítő segítséggel:

  • Az ablak területének kettéosztására használjuk ismét a SplitContainer vezérlőt (a neve maradjon az alapértelmezett splitContainer1)
  • A ListView oszlopainak felvételekor csak a Text tulajdonságot változtasd, a Name-et ne. Ugyanitt, az oszlopok szélességét is növeld meg.
  • Ha a ListView nem mutatja a 2 oszlopot, csak a fájlok neveit, a View tulajdonságát állítsd át Details-re.
  • A ListView FullRowSelect tulajdonságát állítsd true ra (enélkül a tesztek nem futnak le jól majd).
  • Az aktuálisan kiválasztott elem adatainak megjelenítését a ListView SelectedIndexChanged eseményével célszerű megoldani.
  • A detailsPanel Dock tulajdonságát megfelelően be kell állítani.
  • Ahhoz, hogy a TextBox vezérlő kitölthesse a rendelkezésére álló teret, nem elég a Dock tulajdonságát Fill-re állítani, szükséges a Multiline tulajdonság true-ra állítása is.

    Tipp

    Ha az ablak jobb oldalán a Textbox teteje bekerül a panel mögé, annak valószínűleg az oka az, hogy a SplitContainer kettes paneljéhez a detailsPanel és a tContent szövegdoboz nem jó sorrendben kerül hozzáadásra (a jó sorrend a tContent, utána detailsPanel). A vezérlők hozzáadási sorrendje a Document Outline ablakban ellenőrizhető, és a sorrend itt változtatható meg drag&droppal.

  • Egy fájl tartalmát egyszerűen betölthetjük egy stringbe a File statikus osztályReadAllText(filename) függvényével.

  • A FileInfo osztály Name tulajdonsága megadja egy fájl teljes nevét, a CreationTime pedig létrehozásának idejét (melyet a ToString() művelettel alakítsunk stringé).
  • Ne felejtsük el, hogy a felhasználó többször egymás után is választhat mappát az Open menüponttal. Az új mappa tartalmának betöltése előtt az aktuális fájl listát mindig üríteni kell.

    Tipp

    A ListView elemeinek eltávolítására ne a ListView osztály Clear műveletét, hanem a ListView osztály Items tulajdonságának Clear műveletét használd!

Az elkészült alkalmazás képe:

Feladat 3 Megoldás

Túl régi dátum

Ha a létrehozási dátumnak nagyon régi (1601-es évhez tartozó) dátumot kapsz, akkor lehet, hogy a FileInfo objektumot nem a fájl teljes útvonalával, hanem csak a fájl nevével hozod létre, és ez okozza.

BEADANDÓ

❗ Mielőbb továbbmennél a következő feladatra, egy képernyőmentést kell készítened, ennek módját az alábbi.

Készíts egy képernyőmentést Feladat3.png néven az alábbiak szerint:

  • Indítsd el az alkalmazást. Ha szükséges, méretezd át kisebbre, hogy ne foglaljon sok helyet a képernyőn,
  • a „háttérben” a Visual Studio legyen, a MainForm.cs megnyitva,
  • a VS View / Full Screen menüjével kapcsolj ideiglenesen Full Screen nézetre, hogy a zavaró panelek ne vegyenek el semmi helyet,
  • görgess le a forrásfájlod legaljára, használj kb. normál zoom értéket, most fontos, hogy ami a képernyődön lesz, legyen jól olvasható (az nem baj, ha nem fér ki minden, nem is fog), az előtérben pedig az alkalmazásod ablaka.

Feladat 4 – Rajzolás

Feladat

Amennyiben a felhasználó megnyitott egy fájlt, akkor a megnyitott fájl tartalmát adott időközönként frissítsük. A frissítési időköz 6 másodperc legyen. A frissítés jelzésére a kijelölt fájl adatait (név és létrehozás dátuma) tartalmazó panel felső felére (0,0 koordinátából kezdve) rajzoljunk ki barna (Color.Brown) színnel egy 5 pixel magas, kezdetben 125 pixel széles kitöltött téglalapot. A téglalap hossza a következő frissítésig hátralevő idővel legyen arányos: ennek megfelelően minden tizedmásodpercben arányosan csökkentsük a hosszát. Így minden frissítési időköz végén a téglalap hossza nulla lesz. A frissítési időköz végén (amikor a téglalap hossza elérte a 0-t) a korábban kiválasztott fájl tartalmát töltsük be újból, és kezdjük elejéről a folyamatot. Az időzítésre Timer komponenst használjunk!

❗ A feladat csak akkor elfogadható, ha a fenti, kiemelt szövegstílussal jelölt paraméterekkel dolgozol. Arra figyelj, hogy a kirajzolt téglalap ne lógjon bele vezérlőkbe és ne lógjon túl az űrlapon (ha szükséges, mozgasd kicsit lentebb a vezérlőket, illetve vedd kicsit szélesebbre az űrlap alapértelmezett méretét).

Megoldás

A feladatot próbáld meg önállóan megoldani, majd a lenti leírás alapján ellenőrizd a megoldásod!

Megoldás

A megoldás alapját egy Timer komponens fogja adni. Ez egy olyan vezérlő, mely nem rendelkezik vizuális felülettel, csupán néhány testre szabható tulajdonsággal és egy Tick eseménnyel, mely az Interval tulajdonságban (milliszekundumban) megadott időközönként automatikusan meghívódik. Első lépésként ezt az ütemezést állítjuk be.

  1. Húzzunk egy Timer komponenst (Toolbox / Componensts) MainForm-ra! Figyeljük meg, hogy a komponens csupán a Form alatti szürke területen jelenik meg. Itt tudjuk kijelölni a későbbi lépésekhez.

    Timer

  2. Ellenőrizzük, hogy az Interval tulajdonsága 100-ra van állítva. Ez 100 milliszekundumonként, vagyis minden tizedmásodpercben kiváltja a Tick eseményt.

  3. Állítsuk a Name tulajdonságot reloadTimer-re!

  4. Vezessünk be néhány új tagváltozót a MainForm osztályban:

    • loadedFile az utoljára betöltött fájl adatait tartalmazza,
    • counter az újratöltésig szükséges tizedmásodpercek számát tartalmazza, a későbbiekben minden tizedmásodpercben eggyel csökkentjük az értékét egy időzítő segítségével, míg el nem éri a nullát,
    • counterInitialValue a counter számláló kezdőértéke (ahonnan visszaszámol).

    A tagváltozókat az osztály elejére szoktuk beszúrni:

    public partial class MainForm: Form
    {
        private FileInfo loadedFile = null;
        int counter;
        readonly int counterInitialValue;
    
        // ..
    }
    
  5. A konstruktorban állítsuk be a counterInitialValue értékét (később ez nem is változik).

    A counterInitialValue értékét a fenti kódban neked kell meghatározni: számítsd ki a frissítési időköz és az timer Interval alapján!

    public MainForm()
    {
        InitializeComponent();
        counterInitialValue = ; // TODO a frissítési időköznek megfelelő érték
    }
    
  6. Egészítsük ki a duplakattintást kezelő eseménykezelőnket, hogy ne csak betöltse a fájlt, hanem:

    1. Indítsa el a Timer-t a reloadTimer.Start() hívással,
    2. állítsa be counter értékét counterInitialValue-ra,
    3. állítsa be loadedFile értékét a mindenkori kiválasztott fájl leírójára.

    Megjegyzés

    A megoldás minden egyes új fájl megnyitásakor meghívja a Timer osztály Start függvényét. Ez nem jelent gondot, mivel ilyenkor a már elindított Timer egyszerűen fut tovább és figyelmen kívül hagyja a további Start hívásokat.

  7. Iratkozzunk fel a Timer komponens Tick eseményére. Ehhez a reloadTimer kijelölése után a Property Editor-ban az Events fülön kattintsunk duplán a Tick eseményre, ezzel létrejön a kapcsolódó eseménykezelő (reloadTimer_Tick). Töltsük ki a kódját:

    private void reloadTimer_Tick(object sender, EventArgs e)
    {
        counter--;
    
        // Fontos! Ez váltja ki a Paint eseményt
        // és ezzel a téglalap újrarajzolását
        detailsPanel.Invalidate();
    
        if (counter <= 0)
        {
            counter = counterInitialValue;
            tContent.Text = File.ReadAllText(loadedFile.FullName);
        }
    }
    

    A fenti megoldás minden egyes Tick eseményre csökkenti a counter értékét, egészen addig, amíg el nem éri a 0 értéket, ilyenkor ugyanis visszaállítjuk a kezdőértékre, és újra betöltjük a fájlt.

    A megoldás jól szemlélteti a Windows Forms alkalmazásokban a grafikus megjelenítés tipikus mechanizmusát:

    • Tényleges rajzolást az állapotot megváltoztató műveletben nem végzünk, hanem a form/vezérlő (esetünkben panel) Invalidate műveletében váltjuk ki a Paint eseményt.
    • A konkrét téglalap (aktuális állapotnak megfelelő) megjelenítéséért/kirajzolásáért az űrlap/vezérlő (esetünkben a panel) Paint eseménye felelős.
  8. Iratkozzunk fel a detailsPanel komponens Paint eseményére. Ehhez a panel kijelölése után a Property Editor-ban az Events fülön kattintsunk duplán a Paint eseményre, ezzel létrejön a kapcsolódó eseménykezelő (detailsPanel_Paint). Töltsük ki a kódját:

    private void detailsPanel_Paint(object sender, PaintEventArgs e)
    {
        if (loadedFile!=null)
        {
            // A téglalap szélessége a téglalap kezdőhosszúságából (adott a feladatkiírásban) számítható,
            // szorozva a számláló aktuális és max értékének arányával
            e.Graphics.FillRectangle(/*TODO paraméterek*/); 
        }
    }
    

    A FillRectangle pontos paraméterezést a fenti példakód megjegyzésben szereplő segítség alapján tudod meghatározni.

    Lebegőpontos számítások

    Tipikus probléma szokott lenni, ha egész értékű osztást végzel a szélesség számításakor (ekkor az eredmény jó eséllyel nulla lesz): az osztót vagy osztandót castold előbb lebegőpontos számra és így dolgozz.

  9. Teszteljük a megoldásunkat (az alábbi ábrán a színes téglalap lehet eltér a feladatban elvártaktól):

    Feladat 4 Megoldás

Kiegészítő gyakorló feladat

Egészítsük ki az alkalmazásunkat úgy, hogy a fájlok közt "Total Commander"-szerűen tudjunk mozogni, vagyis:

  • A listában jelenjenek meg a mappák nevei is. Ezekre duplán kattintva a teljes fájl lista cserélődjön le az aktuális mappa tartalmára. A mappanevek eredeti formájukban jelenjenek meg (pl. ne legyenek körbevéve szögletes vagy egyéb zárójelekkel).
  • A lista elejére kerüljön be egy speciális ".." nevű elem, mely mindig az aktuális mappa szülőmappájának tartalmát listázza ki.
  • Amikor gyökérelemben vagyunk (pl.: "C:\"), ne jelenjen meg a ".." elem.

2024-09-28 Szerzők