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
UserControl
szerepé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 (
Paint
esemény,Invalidate
,Graphics
haszná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
public
mó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
Text
tulajdonsá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:
MainForm
osztály: Az alkalmazás főablaka. EgyTabControl
-t tartalmaz, ahol megjelennek az egyes dokumentumok nézetei. Kezeli aMenuStrip
eseményeit, a többségük kezelőfüggvényében egyszerűen továbbhív azApp
osztályba (vagyis a logika nem a form osztályban van megírva).App
osztály: Az alkalmazást reprezentálja. Egy példányt kell létrehozni belőle azInitialize
hívásával, ez lesz az alkalmazásunk „root” objektuma. Ez bármely osztály számára hozzáférhető azApp.Instance
statikus 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ívTabPage
határozza meg. Tabváltáskor mindig frissítésre kerül. ATabPage
-ek aTag
property-jükben tárolják azt a nézet objektumot, melyet megjelenítenek.ActiveDocument
: Az aktív dokumentumot adja vissza. Az aktívTabPage
meghatá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.
Document
osztá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. AzUpdateAllViews
mű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. ADocument
leszá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. AUserControl
osztályból származik, és implementálja azIView
interfé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.cs
megnyitva, ú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 aSaveActiveDocument
eleje), - 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
sr
nevűStreamReader
objektumunk, 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.Trim
haszná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
string
osztálySplit
művelete, pl.:string[] columns = line.Split(’\t’);
-
Sztringből
double
-t, illetveDateTime
objektumot 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
SignalValue
objektumot a beolvasott értékekkel inicializálva, és vegyük fel asignals
listá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
OpenDocument
fü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.cs
megnyitva, melyben látszik aTraceValues
implementá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.cs
vé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
Form
osztályból kell egy saját osztályt leszármaztatni. SajátUserControl
esetében a beépítettUserControl
osztá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
UserControl
osztá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
Paint
eseményhez rendelünk eseménykezelőt, vagy felüldefiniáljuk azOnPaint
virtuá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
UserControl
legyen. 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
DemoView
mintájára (többek között implementálja azIView
interfészt). ADemoView
a dokumentumra ősDocument
típusként hivatkozik, lásdDocument document;
tagváltozó. AGraphicsSignalView
-ban célszerű a specifikusabb,SignalDocument
típusúnak definiálni a tagváltozót! -
Módosítsuk az
App.CreateView()
-t, hogyDemoView
helyettGraphicsSignalView
-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.CreateView
módosításának van még egy trükkje. Mivel adocument
referenciánk típusaDocument
, aGraphicsSignalView
pedig 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
GraphicsSignalView
UserControl
) kliens területének szélességét aClientSize.Width
, a magasságát aClientSize.Height
lekérdezésével kaphatjuk meg. Vonalat rajzolni aGraphics
osztályDrawLine
mű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.Height
adja 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
Pen
tá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.cs
megnyitva, 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
OnPaint
művelet a megjelenítés során el kell érje aSignalDocument
-ben tároltSignalValue
objektumokat. Ehhez aSignalDocument
osztályban vezessünk be egy publikus property-t (aSignalDocument
-ben asignals
tag 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
DateTime
objektum aTicks
property-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
GraphicsSignalView
UserControl
) 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
éspixelPerValue
ská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
OnPaint
mű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
GraphicsSignalView
UserControl
-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 (
Text
property). - Rendeljünk eseménykezelőt a gombok
Click
eseményéhez (ehhez csak duplán kell a gombokon kattintani a szerkesztőben). - Vezessünk be a nézetben egy
double
tí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). AzOnPaint
mű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 azInvalidate
mű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.cs
megnyitva, 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.cs
relevá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.