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:
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:
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¶
- Húzzunk be a felületre egy
MenuStripvezérlőt. - A
MenuStripvezérlő bal szélén megjelenő szövegdobozba írjuk be, hogy "File”, ezzel létrehoztuk a főmenüt. - Az újonnan létrehozott főmenüt kijelölve hozzuk létre a két almenüt.
-
Egyesével kijelölgetve a menüelemeket, töltsük ki a nevüket (
miOpen,miExit).A vezérlőknek csak a
Nametulajdonságát állítsd, azAccessibleName-t ne. Ez a későbbi feladatokra is vonatkozik. -
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:
TextBoxszé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
-
Adjunk hozzá a projektünkhöz egy új űrlap típust (projekten jobb klikk, majd Add / Form (Windows Forms), a neve legyen InputDialog.
-
Adjunk az űrlaphoz egy
TextBox, egyLabelés kétButtonvezérlőt. Rendezzük el őket a felületen és állítsuk be a tulajdonságaikat:TextBoxName:tPath
ButtonName:bOkText: "Ok"DialogResult:OK
ButtonName:bCancelText: "Cancel"DialogResult:Cancel
LabelText: "Path"
InputDialog(maga aForm)AcceptButton:bOkCancelButton:bCancel
A dialógusablak elkészítésekor kihasználjuk azt, hogy egy modális dialógusablakot nem csak a
Closeutasítással lehet bezárni, hanem úgy is, ha értéket adunk aDialogResulttulajdonságának. Ezt kódból is megtehettük volna, de mi most a gombok erre szolgáló mechanizmusát használtuk aFormAcceptésCancelbutton tulajdonságaival. -
Az egyes vezérlők
Anchortulajdonságainak beállításaival érjük el, hogy az ablak tartalma arányosan változzon az átméretezés során: aTextBoxszé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). -
Vegyünk fel egy
Pathnevű tulajdonságot azInputDialog.csfájlba, mely aTextBoxtartalmá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; } } -
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
tPathszövegdoboz és aPathtulajdonsá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.csmegnyitva, - 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ítettInputDialogfelhasználásával, a bal oldalon egyListViewvezé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ű
Labeltí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
lNameszövege pontosan a fájl neve legyen, mindenféle prefix (pl. "Name:” és hasonlók) nélkül. Ugyanez igaz azlCreatedvonatkozásában. A "prefixek”-hez különLabelvezérlőt használj a name és a created vonatkozásában is. -
A
ListViewFullRowSelecttulajdonságát állítsdtruera (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
SplitContainervezérlőt (a neve maradjon az alapértelmezettsplitContainer1) - A
ListViewoszlopainak felvételekor csak aTexttulajdonságot változtasd, aName-et ne. Ugyanitt, az oszlopok szélességét is növeld meg. - Ha a
ListViewnem mutatja a 2 oszlopot, csak a fájlok neveit, aViewtulajdonságát állítsd átDetails-re. - A
ListViewFullRowSelecttulajdonságát állítsdtruera (enélkül a tesztek nem futnak le jól majd). - Az aktuálisan kiválasztott elem adatainak megjelenítését a
ListViewSelectedIndexChangedeseményével célszerű megoldani. - A
detailsPanelDocktulajdonságát megfelelően be kell állítani. -
Ahhoz, hogy a
TextBoxvezérlő kitölthesse a rendelkezésére álló teret, nem elég aDocktulajdonságátFill-re állítani, szükséges aMultilinetulajdonságtrue-ra állítása is.Tipp
Ha az ablak jobb oldalán a
Textboxteteje bekerül a panel mögé, annak valószínűleg az oka az, hogy aSplitContainerkettes paneljéhez adetailsPanelés atContentszövegdoboz nem jó sorrendben kerül hozzáadásra (a jó sorrend atContent, utánadetailsPanel). 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
Filestatikus osztályReadAllText(filename)függvényével. - A
FileInfoosztályNametulajdonsága megadja egy fájl teljes nevét, aCreationTimepedig létrehozásának idejét (melyet aToString()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
ListViewelemeinek eltávolítására ne aListViewosztályClearműveletét, hanem aListViewosztályItemstulajdonságánakClearműveletét használd!
Az elkészült alkalmazás képe:
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.csmegnyitva, - 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.
-
Húzzunk egy
Timerkomponenst (Toolbox / Componensts)MainForm-ra! Figyeljük meg, hogy a komponens csupán aFormalatti szürke területen jelenik meg. Itt tudjuk kijelölni a későbbi lépésekhez. -
Ellenőrizzük, hogy az
Intervaltulajdonsága 100-ra van állítva. Ez 100 milliszekundumonként, vagyis minden tizedmásodpercben kiváltja aTickeseményt. -
Állítsuk a
NametulajdonságotreloadTimer-re! -
Vezessünk be néhány új tagváltozót a
MainFormosztályban:loadedFileaz utoljára betöltött fájl adatait tartalmazza,counteraz ú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,counterInitialValueacounterszá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; // .. } -
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 aztimerIntervalalapján!public MainForm() { InitializeComponent(); counterInitialValue = ; // TODO a frissítési időköznek megfelelő érték } -
Egészítsük ki a duplakattintást kezelő eseménykezelőnket, hogy ne csak betöltse a fájlt, hanem:
- Indítsa el a
Timer-t areloadTimer.Start()hívással, - állítsa be
counterértékétcounterInitialValue-ra, - á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
TimerosztályStartfüggvényét. Ez nem jelent gondot, mivel ilyenkor a már elindítottTimeregyszerűen fut tovább és figyelmen kívül hagyja a továbbiStarthívásokat. - Indítsa el a
-
Iratkozzunk fel a
TimerkomponensTickeseményére. Ehhez areloadTimerkijelölése után a Property Editor-ban az Events fülön kattintsunk duplán aTickesemé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
Tickeseményre csökkenti acounteré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)
Invalidateműveletében váltjuk ki aPaintesemé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)
Painteseménye felelős.
- Tényleges rajzolást az állapotot megváltoztató műveletben nem végzünk, hanem a form/vezérlő (esetünkben panel)
-
Iratkozzunk fel a
detailsPanelkomponensPainteseményére. Ehhez a panel kijelölése után a Property Editor-ban az Events fülön kattintsunk duplán aPaintesemé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
FillRectanglepontos 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.
-
Teszteljük a megoldásunkat (az alábbi ábrán a színes téglalap lehet eltér a feladatban elvártaktól):
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.




