Document-View architektúra házi feladat (Signals)¶
Bevezetés¶
A feladat megértése szempontjából kulcsfontosságú a Document-View architektúra részletekbe menő ismerete, pl. az előadásanyag, illetve a kapcsolódó gyakorlat alapján.
Kapcsolódó előadások:
- Document-View architektúra elméleti ismerete és alkalmazása egyszerű környezetben
- C# property, delegate, event alkalmazástechnikája
- Windows Forms alkalmazások fejlesztésének alapjai (
Form, vezérlőelemek, eseménykezelés) - Grafikus megjelenítés Windows Forms alkalmazásokban
UserControlés használata
Kapcsolódó laborgyakorlatok:
- A felhasználói felület kialakítása laborgyakorlat
- Document-View architektúra laborgyakorlat
Az házi feladat célja:
- UML alapú tervezés és néhány tervezési minta alkalmazása
- A Document-View architektúra alkalmazása a gyakorlatban
- A
UserControlszerepének bemutatása Windows Forms alkalmazásokban, Document-View architektúra esetén - A grafikus megjelenítés elveinek gyakorlása Windows Forms alkalmazásokban (
Paintesemény,Invalidate,Graphicshasználata)
A szükséges fejlesztőkörnyezet a szokásos.
A beadás menete¶
- 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 neptun.txt fájlba írd bele a Neptun kódod!
- A kiklónozott fájlok között a
Signals.sln-t megnyitva kell dolgozni. 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 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.
A beadott megoldások mellé külön indoklást, illetve leírást nem várunk el, ugyanakkor az elfogadás feltétele, hogy a beadott kódban a Feladat 3 – Jelek grafikus megjelenítése, saját nézet osztály fejezet feladatainak a megoldását kommentekkel kell ellátni. A többi fejezet feladatainak megoldását NEM kell kommentezni.
Feladatok áttekintése¶
Feladatleírás¶
- Egy olyan vastagkliens (Windows Forms) alkalmazást kell elkészíteni, amely képes fájlban időbélyeggel tárolt mérési értékek grafikus megjelenítésére. Az alkalmazásnak a Document-View architektúrát kell követnie.
- Egyszerre több dokumentum is meg lehet nyitva, illetve egy dokumentumnak több nézete is lehet. A főablak egy
TabControl-t tartalmaz, melyen minden nézet egy külön tabfülön jelenik meg. - Egy dokumentum létrehozásakor/megnyitásakor egy nézet (tabfül) jön létre hozzá, de utólag a Window / New View menüelem kiválasztásával új nézet/tabfül is létrehozható. Egy dokumentumhoz azért van értelme több nézetet megjeleníteni, mert az egyes nézetek eltérő nagyításban képesek az adott dokumentum jeleit megjeleníteni.
- A jelek kirajzolása mellett meg kell jeleníteni a koordinátatengelyeket is.
Irányelvek¶
- A megvalósítás során használjunk beszédes változóneveket, pl.
pixelPerSec. - Amennyiben a programozási feladatok megvalósítása során „inconsistent visibility”-re vagy „inconsistent accessibility”-re panaszkodó fordítási hibaüzenetekkel találkozunk, ellenőrizzük, hogy valamennyi típusunk (osztályunk, interfészünk) láthatósága publikus-e, a class/interface kulcsszó előtt adjuk meg a
publicmódosítót. Pl.:
public class MyClass
{ … }
Feladat 1 - A kiindulási környezet megismerése¶
Bevezető feladatok¶
A főablak fejléce a "Signals" szöveg legyen, hozzáfűzve a saját Neptun kódod: (pl. "ABCDEF" Neptun kód esetén "Signals - ABCDEF"), fontos, hogy ez legyen a szöveg! Ehhez az űrlapunk
Texttulajdonságát állítsuk be erre a szövegre.
Kiinduló alkalmazás működése¶
A solutionünk egy Document-View keretet tartalmaz. Futtatva teszteljük a kiindulási alkalmazást:
- A File/New menü egy új dokumentumot hoz létre. Első lépésben bekéri a dokumentum nevét, majd létrehozza a dokumentumot és a nézetet a hozzá tartozó tabfüllel.
- A File/Open és File/Save menüelemekhez lényegi implementáció egyelőre nem tartozik.
- A File/Close bezárja az aktuális dokumentumot/tabfület.
- A Window/New View egy új nézetet/tabfület hoz létre az aktuális dokumentumhoz. Amennyiben egy dokumentumhoz több nézet is tartozik, a 2. nézettel kezdve a tabfülön a nézet sorszáma is megjelenik.
A főablakunk a következőképpen néz ki, ha két dokumentumot hoztunk létre, és a másodikhoz két nézetet:
Kiinduló projekt¶
A megjegyzésekkel ellátott forráskódot nézve ismerkedjünk meg a keret architektúrájával, működésével.
A fontosabb osztályok a következők:
MainFormosztály: Az alkalmazás főablaka. EgyTabControl-t tartalmaz, ahol megjelennek az egyes dokumentumok nézetei. Kezeli aMenuStripeseményeit, a többségük kezelőfüggvényében egyszerűen továbbhív azApposztályba (vagyis a logika nem a form osztályban van megírva).Apposztály: Az alkalmazást reprezentálja. Egy példányt kell létrehozni belőle azInitializehívásával, ez lesz az alkalmazásunk „root” objektuma. Ez bármely osztály számára hozzáférhető azApp.Instancestatikus property-n keresztül (erre több példát is látunk a főablak menü eseménykezelőiben). Tárolja a dokumentumok listáját. Legfontosabb tagjai a következők:documents: Valamennyi megnyitott dokumentumot tartalmazó lista.activeView: Az aktív nézetet adja vissza. Ezt az aktívTabPagehatározza meg. Tabváltáskor mindig frissítésre kerül. ATabPage-ek aTagproperty-jükben tárolják azt a nézet objektumot, melyet megjelenítenek.ActiveDocument: Az aktív dokumentumot adja vissza. Az aktívTabPagemeghatározza, melyik az aktív nézet, a nézet pedig referenciával rendelkezik a dokumentumra, melyhez tartozik.NewDocument: Létrehoz egy új dokumentumot, a hozzá tartozó nézettel. Alaposan tanulmányozzuk át az implementációt, az általa hívott függvényeket is beleértve!CreateViewForActiveDocument: Egy új nézetet hoz létre az aktív dokumentumhoz. A Window/New View menüelem kiválasztásának hatására hívódik meg.CloseActiveView: Bezárja az aktív nézetet.
Documentosztály: Az egyes dokumentum típusok ősosztálya. Bár esetünkben csak egy dokumentum típus létezik, a későbbi bővíthetőség miatt célszerű külön választani. Tartalmazza a nézetek listáját, melyek a dokumentumot megjelenítik. AzUpdateAllViewsművelete valamennyi nézetet értesít annak érdekében, hogy frissítsék magukat. ALoadDocumentésSaveDocumentüres virtuális függvények, melyek a dokumentum betöltésekor és mentésekor kerülnek meghívásra. ADocumentleszármazott osztályunkban kell felüldefiniálni és értelemszerűen megvalósítani őket.IView: Az egyes nézetek közös interfésze. Azért nem osztály, mert a nézetek tipikusan aUserControl-ból származnak le, és egy osztálynak nem lehet több ősosztálya .NET környezetben.DemoView: Egy demo nézet implementáció, mintaként szolgálhat saját nézet létrehozásához. AUserControlosztályból származik, és implementálja azIViewinterfészt.
Az osztályok közötti kapcsolatok jobb megértését segíti a solutionben található ClassDiagram1.cd UML osztálydiagram.
Feladat 2 – Mérési értékek kezelése (dokumentum logikák)¶
Mérési értékek reprezentálása¶
Vezessünk be egy osztályt a jelértékek reprezentálására.
Legyen az osztály neve SignalValue, és egy Value (double) mezőben tárolja a mért értéket, az időbélyeget pedig egy TimeStamp (DateTime) mezőben. Mivel ezeket nem akarjuk a kezdeti inicializálás után megváltoztatni, definiáljuk őket csak olvashatónak (readonly kulcsszó).
Az osztálynak legyen olyan kétparaméteres konstruktora, mely paraméterben megkapja jelértéket és az időbélyeget, és ez alapján inicializálja a tagváltozókat.
Írjuk felül az object-ből örökölt ToString műveletet, hogy formázottan jelenítse meg az objektum tagváltozóit. Segítség:
public override string ToString()
{
return $"Value: {Value}, TimeStamp: {TimeStamp}";
}
Saját dokumentum osztály¶
Vezessünk be egy saját dokumentum osztályt a dokumentumhoz tartozó jelértékek tárolására.
Legyen az osztály neve SignalDocument, származzon a Document osztályból, és egy signals nevű List<SignalValue> típusú tagban tárolja a jeleket.
Document konstruktor
Az ős Document nem rendelkezik default konstruktorral, ezért kell írjunk a leszármazottunkban megfelelő konstruktort:
public SignalDocument(string name)
: base(name)
{
}
Módosítsuk az App.NewDocument függvényt, hogy a leszármazott SignalDocument-et példányosítsa.
Adatok mentése¶
Gondoskodjunk a dokumentum által tárolt adatok elmentéséről.
A tesztelést segítendő inicializáljuk a SignalDocument-ben tárolt jelérték listát úgy, hogy mindig legyen benne néhány elem. Célszerű ezeket egy külön tagváltozóban felvenni. Az alábbi kód arra is példát mutat, hogyan lehet C# nyelven a tömb elemeit az inicializálás során egyszerűen megadni (collection initializer).
Figyelem
A megvalósítás során NE az alábbi példában szereplő értékeket használd:
public class SignalDocument : Document
{
// ...
private List<SignalValue> signals = new List<SignalValue>();
private SignalValue[] testValues = new SignalValue[]
{
new SignalValue(10, new DateTime(2023, 1, 1, 0, 0, 0, 111)),
new SignalValue(20, new DateTime(2023, 1, 1, 0, 0, 1, 876)),
new SignalValue(30, new DateTime(2023, 1, 1, 0, 0, 2, 300)),
new SignalValue(10, new DateTime(2023, 1, 1, 0, 0, 3, 232)),
new SignalValue(-10, new DateTime(2023, 1, 1, 0, 0, 5, 885)),
new SignalValue(-19, new DateTime(2023, 1, 1, 0, 0, 6, 125)),
};
public SignalDocument(string name)
: base(name)
{
// Kezdetben dolgozzunk úgy, hogy a signals
// jelérték listát a testValues alapján inicializáljuk.
signals.AddRange(testValues);
}
// ...
}
Következő lépésben írja meg az App.SaveActiveDocument függvényt a forráskódban található megjegyzéseknek megfelelően. A SaveFileDialog használatára a dokumentációban itt vagy itt talál példát. Az előbb linkelt példa megtévesztő lehet, mert a dialógus meg is nyitja a fájlt. Esetünkben erre semmi szükség, csak egy fájl útvonalat szeretnénk szerezni, hiszen a fájl megnyitása a dokumentum osztályunk feladata.
Segítség a megoldáshoz
/// <summary>
/// Elmenti az aktív dokumentum tartalmát.
/// </summary>
public void SaveActiveDocument()
{
if (ActiveDocument == null)
return;
// Útvonal bekérése a felhasználótól a SaveFileDialog segítségével.
var saveFileDialog = new SaveFileDialog()
{
// Megjelenítés előtt paraméterezzük fel a dialógus ablakot
Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*",
FilterIndex = 0,
RestoreDirectory = true,
};
// Modálisan megjelenítjük a dialógusablakot.
// Ha a felhasználó nem az OK gommbal zárta be az ablakot,
// nem csinálunk semmit (visszatérünk)
if(saveFileDialog.ShowDialog() != DialogResult.OK)
return;
// A dokumentum adatainak elmentése.
// A saveFileDialog.FileName tartalmazza a teljes útvonalat.
ActiveDocument.SaveDocument(saveFileDialog.FileName);
}
A következő lépésben definiáljuk felül a SignalDocument osztályban az örökölt SaveDocument függvényt, melyben írjuk ki a tárolt jelértékeket, időbélyeggel együtt. A mentés során arra törekszünk, hogy tömör, mégis olvasható formátumot kapjunk. Ennek megfelelően a bináris formátum nem javasolt. Kövessük a következő minta által meghatározott szöveges formátumot:
10 2022-12-31T23:00:00.1110000Z
20 2022-12-31T23:00:01.8760000Z
30 2022-12-31T23:00:02.3000000Z
10 2022-12-31T23:00:03.2320000Z
Az első oszlopban a jelérték, a másodikban az időpont található, az oszlopok tabulátor karakterrel szeparáltak (\t). Az időpont legyen UTC idő annak érdekében, hogy ha a fájlt más időzónában töltik be, akkor is a helyes helyi időt mutassa. A megfelelő string konverzió a következő:
var dt = myDateTime.ToUniversalTime().ToString("o");
Szöveges adatok fájlba írására a StreamWriter osztályt használjuk.
Figyelem
A megoldásunkban garantáljuk, hogy kivétel esetén is lezáródjon a fájlunk: használjunk try-finally blokkot, vagy alkalmazzunk using blokkot:
using (StreamWriter sw = new StreamWriter(filePath))
{
}
Az alkalmazást futtatva teszteljük a mentés funkciót. Ennek során ellenőrizzük, hogy a fájlban valóban az elvárásoknak megfelelő formátumban kerülnek-e kiírásra az adatok. Ehhez indítsuk el az alkalmazást, hozzunk létre egy új dokumentumot, majd a File/Save menü kiválasztásával mentsük el.
BEADANDÓ
Készíts egy képernyőmentést Feladat2-3.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, az
App.csmegnyitva, úgy görgetve, hogy függőlegesen apublic void SaveActiveDocument()legyen az oldal közepén (vagyis látszódjon az előző függvény vége és aSaveActiveDocumenteleje), - a VS View/Full Screen menüjével kapcsolj ideiglenesen Full Screen nézetre, hogy a zavaró panelek ne vegyenek el semmi helyet,
- az előtérben pedig az alkalmazásod ablaka.
Adatok betöltése¶
Biztosítsunk lehetőséget dokumentum fájlból betöltésére.
Írjuk meg az App.OpenDocument függvényt a benne szereplő megjegyzéseknek megfelelően, kövessük az ott megadott lépéseket.
A következő lépésben definiáljuk felül a SignalDocument osztályban az örökölt LoadDocument függvényt, melyben töltsük fel a tárolt jelérték listát a fájl tartalma alapján. Szöveges adatok fájlból beolvasására a StreamReader osztályt használjuk, a mentéshez hasonlóan try/finally vagy using blokkban.
Segítségképpen
-
Amennyiben van egy
srnevűStreamReaderobjektumunk, a fájl soronkénti beolvasása a következőképpen lehetséges:while ((line = sr.ReadLine()) != null) { // A line változóban benne van az aktuális sor // ,,, } -
Az üres, vagy csak whitespace karaktereket tartalmazó sorokat át kell ugrani. A
string.Trimhasználható a whitespace karakterek kiszűrésére, pl.:s = s.Trim(); -
Az oszlopok tab karakterrel szeparáltak. Egy sztring adott karakter szerinti vágására kényelmesen használható a
stringosztálySplitművelete, pl.:string[] columns = line.Split(’\t’); -
Sztringből
double-t, illetveDateTimeobjektumot a<típusnév>.Parse(str)függvénnyel lehet pl. kinyerni:double d = double.Parse(strValue); DateTime dt = DateTime.Parse(strValue); -
A fájlban UTC időbélyegek szerepelnek, ezt a dokumentum osztályban tárolás előtt konvertáljuk lokális időre:
DateTime localDt = utcDt.ToLocalTime(); -
Miután beolvastuk az adott sort, hozzunk létre egy új
SignalValueobjektumot a beolvasott értékekkel inicializálva, és vegyük fel asignalslistába.
A LoadDocument függvény elején a signals feltöltése előtt töröljük ki a Clear művelettel a benne levő elemeket. Enélkül ugyanis a konstruktorban hozzáadott teszt jelértékek benne maradnának.
BEADANDÓ
Készíts egy képernyőmentést Feladat2-4.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,
- „háttérben” a Visual Studio legyen, az App.cs megnyitva, úgy görgetve, hogy az
OpenDocumentfüggvény törzséből minél több látszódjon. - a VS View/Full Screen menüjével kapcsolj ideiglenesen Full Screen nézetre, hogy a zavaró panelek ne vegyenek el semmi helyet,
- az előtérben pedig az alkalmazásod ablaka.
Betöltés ellenőrzése¶
A betöltést követően ellenőrizzük a betöltés sikerességét.
Mivel grafikus megjelenítéssel még nem rendelkezik az alkalmazás, más megoldást kell választani. Nyomkövetésre, diagnosztikára a System.Diagnostics névtér osztályai használhatók. A Trace osztály „Debug” build esetén a Write/WriteLine utasítással kiírt adatokat trace-eli: az alapértelmezésben azt jelenti, hogy megjeleníti a Visual Studio Output ablakában. Írjunk egy TraceValues segédfüggvényt a SignalDocument osztályba, mely trace-eli a tárolt jeleket:
private void TraceValues()
{
foreach (var signal in signals)
Trace.WriteLine(signal.ToString());
}
Hívjuk meg a TraceValues-t a betöltő függvényünk (LoadDocument) végén, és ellenőrizzük a működést: az F5 billentyű lenyomásával debug módban indítsuk el az alkalmazást, a File/Open kiválasztásával töltsünk be egy korábban elmentett fájlt. A művelet végén ellenőrizzük, hogy a Visual Studio Output ablakában (View/Output menüvel jeleníthető meg) kiíródnak-e a fájlból betöltött jelek adatai.
BEADANDÓ
Készíts egy képernyőmentést Feladat2-5.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
SignalDocument.csmegnyitva, melyben látszik aTraceValuesimplementációja, valamint az Output ablakban a trace-elt jelértékek, - az előtérben pedig az alkalmazásod ablaka.
Feladat 3 – Jelek grafikus megjelenítése, saját nézet osztály¶
Lényeges
Ezen főfejezet feladatainak megoldását kommentekkel kell ellátni!
Új nézet osztály¶
Vezessünk be egy új nézet osztályt UserControl formájában.
A nézetet UserControl-ként valósítjuk meg. A téma elméleti háttere az előadásanyagban megtalálható. Következzen pár fontosabb gondolat ismétlésképpen. A UserControl alapú megközelítéssel olyan saját vezérlőt készíthetünk, melyek az űrlapokhoz (Form) hasonlóan más vezérlőket tartalmazhatnak.
Számos pontban nagyon hasonlítanak az űrlapokhoz, pl.:
- Két forrásfájl tartozik hozzájuk. Egy, amiben mi dolgozunk, és egy
designer.csvégződésű, melybe a Visual Studio generál kódot. A fejlesztők számára dedikált forrásfájlt többféleképpen lehet megnyitni:- A Solution Explorer összevontan jeleníti meg a forrásfájlokat: ezen jobb gombbal kattintva a View Code elemet válasszuk a menüben.
- Amennyiben duplakattal megnyitottuk a
UserControl-t szerkesztésre, a szerkesztőfelületen jobb gombbal kattintva válasszuk a View Code menüt. - F7 billentyű használatával.
- Amikor saját űrlapot készítünk, a beépített
Formosztályból kell egy saját osztályt leszármaztatni. SajátUserControlesetében a beépítettUserControlosztályból kell származtatni. Ezt ritkán szoktuk manuálisan megtenni, általában a Visual Studio-ra bízzuk (pl. Project/Add UserControl menü). - Hasonlóan a Solution Explorerben duplán kattintva rajtuk tudjuk megnyitni a felületüket szerkesztésre, a Toolbox-ból tudunk más vezérlőket elhelyezni a felületükön, melyekből a
UserControlosztályunkban tagváltozók lesznek. - Hasonló módon tudunk eseménykezelőket készíteni (magához a
UserControl-hoz, vagy a rajta levő vezérlőkhöz). - Ugyanúgy tudunk felületére rajzolni. Vagy a
Painteseményhez rendelünk eseménykezelőt, vagy felüldefiniáljuk azOnPaintvirtuális függvényt.
Abban természetesen különbözik az űrlapoktól, hogy míg az űrlapok, mint önálló ablakok a Show vagy ShowDialog műveletekkel megjeleníthetők, a UserControl-ok vezérlők, melyeket űrlapokon vagy más vezérlőkön kell elhelyezni.
Visszatérve a feladatra a megvalósítás főbb lépései a következők:
- Az új nézet a fentieknek megfelelően egy
UserControllegyen. SajátUserControl-t felvenni pl. a Project/Add UserControl menüvel lehet. Legyen a neveGraphicsSignalView(jelezve, hogy ez egy grafikus nézet, és nem karakteresen jeleníti meg a jeleket). - Bővítsük az osztályt a
DemoViewmintájára (többek között implementálja azIViewinterfészt). ADemoViewa dokumentumra ősDocumenttípusként hivatkozik, lásdDocument document;tagváltozó. AGraphicsSignalView-ban célszerű a specifikusabb,SignalDocumenttípusúnak definiálni a tagváltozót! -
Módosítsuk az
App.CreateView()-t, hogyDemoViewhelyettGraphicsSignalView-t hozzon létre. Hogy ez működhessen, aGraphicsSignalView-ba fel kell venni egy konstruktort a következőnek megfelelően (hagyjuk meg a default konstruktort és hívjuk is meg):public GraphicsSignalView(SignalDocument document) : this() { this.document = document; }Az
App.CreateViewmódosításának van még egy trükkje. Mivel adocumentreferenciánk típusaDocument, aGraphicsSignalViewpedig a leszármazottját várja, a konstruktor hívásakor explicit le kell castoljukSignalDocument-re:var view = new GraphicsSignalView((SignalDocument)document);
A koordináta tengelyek kirajzolása¶
Rajzoljuk ki a koordináta tengelyeket. Legyen az alapelv a következő:
- A rajzolófelületünk (vagyis a
GraphicsSignalViewUserControl) kliens területének szélességét aClientSize.Width, a magasságát aClientSize.Heightlekérdezésével kaphatjuk meg. Vonalat rajzolni aGraphicsosztályDrawLineműveletével lehet. - Az Y tengelyt a nulla y pixelpozícióba rajzoljuk.
- Az X tengelyt mindig a rajzolófelületünk közepére igazítva rajzoljuk, akárhogy méretezi is a felhasználó az ablakot (segítségképpen: a teljes aktuális magasságot a
ClientSize.Heightadja meg számunkra). -
A koordináta tengelyek színe legyen kék, és legyenek 2 pixel vastagok. A tengelyeket pontozott vonallal rajzoljuk, és a végükön legyen egy kisméretű nyíl. Erre a beépített
Pentámogatást nyújt:var pen = new Pen(Color.Blue, 2) { DashStyle = DashStyle.Dot, EndCap = LineCap.ArrowAnchor, }; -
A függőleges tengelyt nem a 0, hanem a 2 koordinátába érdemes rajzolni (különben csak 1 pixel vastagnak fog látszódni).
A munkánk eredményeképpen valami hasonlót kell lássunk futás közben (a szín és minta nem biztos, hogy egyezik), persze csak ha megnyitunk egy létező vagy létrehozunk egy új dokumentumot, máskülönben nincs is nézetünk:
BEADANDÓ
Készíts egy képernyőmentést Feladat3-2.png néven az alábbiak szerint:
- Indítsd el az alkalmazást. Nyiss meg vagy hozz létre egy dokumentumot, hogy látszódjanak a koordinátatengelyek. 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
GraphicsSignalView.csmegnyitva, melyben látszik a koordinátatengelyek kirajzolása, - a VS View/Full Screen menüjével kapcsolj ideiglenesen Full Screen nézetre, hogy a zavaró panelek ne vegyenek el semmi helyet,
- az előtérben pedig az alkalmazásod ablaka.
Jelek megjelenítése¶
Valósítsuk meg a jelek megjelenítését!
Az GraphicsSignalView-ban az OnPaint-et felüldefiniálva valósítsuk meg a jelek kirajzolását. Először 3*3 pixeles „pontokat” rajzoljunk (pl. Graphics.FillRectangle-lel), majd a pontokat kössük össze vonalakkal (Graphics.DrawLine).
Segítségképpen
A megvalósításban segíthet a következő:
-
Az
OnPaintművelet a megjelenítés során el kell érje aSignalDocument-ben tároltSignalValueobjektumokat. Ehhez aSignalDocumentosztályban vezessünk be egy publikus property-t (aSignalDocument-ben asignalstag privát, és ez maradjon is így):public IReadOnlyList<SignalValue> Signals { get { return signals; } }Figyeljük meg, hogy az objektumokat nem
List<SignalValue>-ként, hanemIReadOnlyList<SignalValue>formában adjuk vissza: így a hívó nem tudja módosítani az eredeti listát, nem tudja véletlenül se elrontani a tartalmát. -
Két
DateTimeérték különbsége egyTimeSpan(időtartam) típusú objektumot eredményez. - Egy
DateTimeobjektum aTicksproperty-jében adja vissza legjobb felbontással az általa tárolt időértéket (1 tick = 100 nsec felbontás). - A rajzolófelületünk (vagyis a
GraphicsSignalViewUserControl) nulla x koordinátájában jelenítsük meg a listánkban levő első jelet. - A megjelenítés során semmiféle követelmény nincs arra vonatkozóan, hogy a jeleket mindig olyan skálatényezőkkel jelenítsük meg, hogy pont kiférjenek a rajzolás során. Helyette a nézet osztályunkban vezessünk be és használjunk olyan
pixelPerSecéspixelPerValueskálatényezőket, melyek érzésre, vagy pár próbálkozás után úgy jelenítsék meg a jeleket az alapértelmezett megjelenítés során, hogy a nézetbe beférjenek, de ne is legyen a rajz túl kicsi (de az ablak átméretezésekor már nem kell beférjenek a jelek az ablakba). - Amennyiben a rajzunk „nem akar” megjelenni, tegyünk töréspontot az
OnPaintműveletbe, és a kódunkat lépésenként végrehajva a változók értékét tooltipben vagy a Watch ablakban megjelenítve nyomozzuk, hol csúszik félre a számításunk.
Ha jól dolgoztunk, a következőhöz hasonló kimenetet kapunk:
Nagyítás, kicsinyítés¶
Biztosítsunk lehetőséget a nézet nagyításra és kicsinyítésére. Ehhez helyezzünk el egy kisméretű, "+" és "–" szöveget tartalmazó nyomógombot a nézeten.
Lépések:
- Nyissuk meg a
GraphicsSignalViewUserControl-t szerkesztésre. - A Toolbox-ról drag&drop-pal helyezzünk el rajta két gombot (
Button). - Nevezzük el a gombokat megfelelően és állítsuk be a szövegüket (
Textproperty). - Rendeljünk eseménykezelőt a gombok
Clickeseményéhez (ehhez csak duplán kell a gombokon kattintani a szerkesztőben). - Vezessünk be a nézetben egy
doubletípusú skálatényezőt, melynek kezdőértéke legyen 1. Nagyításkor ezt növeljük (pl. 1,2-szeresére), kicsinyítéskor csökkentsük (pl. osszuk 1,2-vel). AzOnPaintműveleteben, mikor az y és x pixelkoordinátákat számoljuk, a végső eredmény számításakor a koordinátákat szorozzuk be az aktuális skálatényezővel. A skálatényező változtatása után ne felejtsük el meghívni azInvalidateműveletet!
A következőhöz hasonló kimenetet a cél (némi nagyítást követően):
Az alkalmazást futtatva a Window menüből ugyanahhoz a dokumentumhoz hozzunk létre egy új nézetet, és a nagyítás/kicsinyítés gombokat használva, valamint a nézetek között váltogatva ellenőrizzük, hogy a nézetek ugyanazokat az adatokat jelenítik meg, de eltérő nagyításban.
BEADANDÓ
Készíts egy képernyőmentést Feladat3-4.png néven az alábbiak szerint:
- Indítsd el az alkalmazást. Nyiss meg vagy hozz létre egy dokumentumot, hogy látszódjanak a koordinátatengelyek és a kirajzolt jelek. 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
GraphicsSignalView.csmegnyitva, melyben látszik a jelek kirajzolása, - a VS View/Full Screen menüjével kapcsolj ideiglenesen Full Screen nézetre, hogy a zavaró panelek ne vegyenek el semmi helyet,
- az előtérben pedig az alkalmazásod ablaka.
Opcionális feladatok¶
IView kódduplikációja¶
Az IView egy interfész, ezért a GetDocument/Update stb. kódját nem lehet implementálni benne. Helyette minden nézetben „copy-paste”-tel duplikálni kell a megfelelő kódot. Szüntessük meg ezt a kódduplikációt az alkalmazásban! A megoldást előbb mindenképpen magad próbáld kitalálni, csak ha elakadsz, akkor fordulj az alábbi kinyitható segítséghez:
Segítség
Egy ViewBase nevű osztályt kell írni, mely a UserControl-ból származik, és implementálja az IView interfészt. A nézeteinket a UserControl helyett a ViewBase osztályból kell származtatni.
Grafikon görgetése¶
Biztosítsunk lehetőséget a grafikon görgetésére!
A megvalósításban használhatunk egyedi scrollbar-t is, de ennél egyszerűbb a UserControl autoscroll támogatását felhasználni (UserControl.AutoScroll és UserControl.AutoScrollMinSize).
A kirajzolás során a rajzot az aktuális scroll pozíciónak megfelelően el kell tolni. Erre a legegyszerűbb megoldás, ha egy, a scroll pozíciónak megfelelő eltolást eredményező transzformációs mátrixot állítunk be a Graphics objektumra a kirajzolás előtt (g.Transform beállítása egy new Matrix(1, 0, 0, 1, AutoScrollPosition.X, AutoScrollPosition.Y) objektumra).
A megközelítés előnye a viszonylagos egyszerűsége. Hátránya, hogy ha nagyon sok jelünk van, de annak csak egy kis szelete látható egy adott pillanatban, attól még a Paint függvényünkben a nem látható jeleket is kirajzoljuk. Egy optimalizált megoldásban csak a látható tartományt célszerű megjeleníteni.
BEADANDÓ
Készíts egy képernyőmentést FeladatIMSc-2.png néven az alábbiak szerint:
- Indítsd el az alkalmazást. Nyiss meg vagy hozz létre egy dokumentumot, hogy látszódjanak a koordinátatengelyek és a kirajzolt jelek, valamint a görgetősáv (scrollbar). 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
GraphicsSignalView.csreleváns része megnyitva, - a VS View/Full Screen menüjével kapcsolj ideiglenesen Full Screen nézetre, hogy a zavaró panelek ne vegyenek el semmi helyet,
- az előtérben pedig az alkalmazásod ablaka.



