Kihagyás

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:

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

  1. ❗ 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ó alkalmazás

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. Egy TabControl-t tartalmaz, ahol megjelennek az egyes dokumentumok nézetei. Kezeli a MenuStrip eseményeit, a többségük kezelőfüggvényében egyszerűen továbbhív az App 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 az Initialize hívásával, ez lesz az alkalmazásunk „root” objektuma. Ez bármely osztály számára hozzáférhető az App.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ív TabPage határozza meg. Tabváltáskor mindig frissítésre kerül. A TabPage-ek a Tag property-jükben tárolják azt a nézet objektumot, melyet megjelenítenek.
    • ActiveDocument: Az aktív dokumentumot adja vissza. Az aktív TabPage 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. Az UpdateAllViews művelete valamennyi nézetet értesít annak érdekében, hogy frissítsék magukat. A LoadDocument és SaveDocument üres virtuális függvények, melyek a dokumentum betöltésekor és mentésekor kerülnek meghívásra. A Document 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 a UserControl-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. A UserControl osztályból származik, és implementálja az IView 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 a public void SaveActiveDocument() legyen az oldal közepén (vagyis látszódjon az előző függvény vége és a SaveActiveDocument 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ály Split művelete, pl.:

    string[] columns = line.Split(’\t);
    
  • Sztringből double-t, illetve DateTime 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 a signals 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 a TraceValues 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át UserControl esetében a beépített UserControl 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 az OnPaint 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át UserControl-t felvenni pl. a Project/Add UserControl menüvel lehet. Legyen a neve GraphicsSignalView (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 az IView interfészt). A DemoView a dokumentumra ős Document típusként hivatkozik, lásd Document document; tagváltozó. A GraphicsSignalView-ban célszerű a specifikusabb, SignalDocument típusúnak definiálni a tagváltozót!
  • Módosítsuk az App.CreateView()-t, hogy DemoView helyett GraphicsSignalView-t hozzon létre. Hogy ez működhessen, a GraphicsSignalView-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 a document referenciánk típusa Document, a GraphicsSignalView pedig a leszármazottját várja, a konstruktor hívásakor explicit le kell castoljuk SignalDocument-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 a ClientSize.Width, a magasságát a ClientSize.Height lekérdezésével kaphatjuk meg. Vonalat rajzolni a Graphics osztály DrawLine 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:

Tengelyek

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 a SignalDocument-ben tárolt SignalValue objektumokat. Ehhez a SignalDocument osztályban vezessünk be egy publikus property-t (a SignalDocument-ben a signals 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, hanem IReadOnlyList<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 egy TimeSpan (időtartam) típusú objektumot eredményez.

  • Egy DateTime objektum a Ticks 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 és pixelPerValue 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:

Grafikon

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). Az OnPaint 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 az Invalidate műveletet!

A következőhöz hasonló kimenetet a cél (némi nagyítást követően):

Nagított grafikon

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.

2024-11-02 Szerzők