Custom Controls i C#

Tags:    c#
Skrevet af Bruger #345 @ 23.05.2005
Denne artikel vil omhandle udvikling af custom controls i C#, rettet mod desktop applikationer. Versionen af .NET som bliver brugt er v2 Beta 2. Den kan hentes gratis fra Microsofts side. I løbet af artiklen vil jeg gøre brug af nogle af de nye ting fra 2.0, såsom generics, og anonymous functions, kendt som lambda-funktioner for nogle af jer. Jeg skal forsøge at være udførlig i min forklaring når jeg bruger dem, for de af jer der ikke er helt med hvad de går ud på. Men husk på, dette er ikke en decideret artikel om C# sproget.
Artiklen fokuserer desuden heller ikke på noget specielt udviklingsmiljø, så du kan følge med, selv med en simpel teksteditor.

Controllen


Den specifikke control vi vil udvikle her er en graf, som kan bruges til at vise periodiske værdier, og skulle gerne ende med at ligne den her:



Det første vi skal gøre er at lave en klasse der nedarver fra System.Windows.Forms.Control:

Fold kodeboks ind/udKode 


Jeg har valgt Control i stedet for UserControl, fordi jeg ikke gør brug af nogle af de ekstra features som denne sidstnævnte tilbyder. Men kunne du f.eks. tænke dig at have en border på grafen, kan du vælge at nedarve fra UserControl i stedet, da den har en BorderStyle property du kan sætte. Du vil stadig kunne følge artiklen her.

Men lad os vende tilbage til koden, og hvad den foretager sig. Der bliver defineret tre variabler, den første af dem en float, _largestValue, som bliver initialiseret til Single.NaN (Single er .NET klassen som float repræsenter, og NaN er en speciel værdi som betyder "Not a Number"), der bruges til at indikere maksimumværdien der bliver vist i grafen. Hvis den er NaN, vil maksimumværdien være den største værdi i _samples, hvilket bringer os videre til den; en brugerdefineret klasse kaldet SamplesCollection, som vi vender tilbage til lige om lidt. Til sidst er _formatString defineret som en string. Værdien af den bestemmer hvordan teksten vil blive outputtet i de øverste hjørner. Formatet er det samme som bruges i Single.ToString().

Dernæst kommer constructoren, som indeholder et kald til SetStyle(). SetStyle bruges til at sætte forskellige parametre om ens control, f.eks. - som i dette tilfælde - om den skal gentegne når controllens størrelse ændres, eller om man selv håndterer al tegningen.
Efter det bliver DoubleBuffered sat til true, dette er en ny property i .NET 2, og dens effekt er at controllens ikke blinker når den bliver opdateret. Det er det samme som at kalde SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true). Så hvorfor ikke bare have inkluderet de to styles i det første kald til SetStyle()? Primært for at vise jer DoubleBuffered propertien, som er nemmere at huske på.

Moving on, så sætter vi en default størrelse på controllen, og initialiserer vores generiske liste af samples.

SamplesCollection


Lad os tage et kig på SamplesCollection klassen:

Fold kodeboks ind/udKode 


Som du kan se nedarver den fra en klasse med det lidt besynderlige navn Collection<float>. Denne klasse hører hjemme i System.Collections.ObjectModel namespacet, og er ment som en base-klasse for typestærke collections. Collection<T> er en generic klasse, hvis ikke-generiske sidestykke er CollectionBase. Forskellen på de to er, og dette er generics fordel og formål, at Collection<T> er typestærk. Det vil sige, at i stedet for at acceptere en object-værdi, som CollectionBase, så kan man nu få klassen til direkte at forvente en float, som i dette tilfælde. Dette har ydelsesfordele, da en value type, som f.eks. float, først skal igennem en proces der kaldes boxing, for at kunne gemmes i et object. Men den præcise forklaring på hvad dette involverer, er uden for denne artikels omfang.

Hvis vi fortsætter, ser vi at SamplesCollection definerer to events, AddedSample og RemovedSample. Disse bliver brugt af Graph-klassen, så den er klar over hvornår der sker ændringer.
Men for at kunne kalde disse events, skal vi have en måde at finde ud af, hvornår noget bliver hhv. tilføjet eller fjernet fra vores collection. Dette gøres ved at at override de to virtuelle metoder, InsertItem(int, float) og RemoveItem(int). Det eneste vi gør, er at checke om der nogen delegates associeret med eventen, for så at kalde dem hvis det er tilfældet. Læg mærke til at eventen bliver gemt i en variabel, dette er for at sikre os at den ikke bliver null, imellem vi checker for null, og kalder den; noget der er muligt i en multithreaded applikation.
Bemærk også at SetItem(int, float) ikke er blevet defineret, da chancen for den bliver brugt er minimal, og for at holde kode-eksemplerne på et rimeligt niveau, men du kan inkludere den sammen med de andre hvis du ønsker.

Til sidst ser vi at der er en RemoveRange(int, int) metode, som gør det lettere at fjerne flere samples på en gang.

Properties


Men vi skulle jo også gerne kunne tilgå de variabler, som vi har defineret tidligere, udenfor klassen, hvis vi vil have nogen chance for at ændre deres værdier. Derfor tilføjer vi tre properties:

Fold kodeboks ind/udKode 


Bemærk at vi i LargestValue propertiens set accessor kalder Invalidate(), som får controllen til at gentegne sig selv. Læg også mærke til, at vi ikke checker om den nye - og den gamle værdi er ens, som vi gør i FormatString, inden der bliver kaldt Invalidate().
Grunden hertil er, at floating-point-numre aldrig bør checkes for enshed, da det sjældent vil give et korrekt resultat, grundet deres afrundning.

Den lettere opmærksomme læser har måske set, at vi ikke har en set accessor på Samples propertien, så hvordan finder vi ud af hvornår der er tilføjet nye samples? Det er her de to events i SamplesCollection spiller ind.

Anonymous Functions


Som nævnt i starten af artiklen, er det .NET 2 der bliver brugt, og en af de nye ting i C#, er anonymous functions. Hvis nogle af jer er bekendt med Lisp eller JavaScripts closures, så er her tale om nogenlunde det samme. De kan bruges hvor end man ville bruge en delegate, hvilket jo er perfekt til vores formål.
Indsæt de følgende linjer i din constructor:

Fold kodeboks ind/udKode 


Som du kan se bruges delegate keywordet til at indikere, at der er tale om en anonymous function. Signaturen skal stadig matche den delegate som den knyttes til, EventHandler(object, EventArgs) i dette tilfælde.
I funktionen tilknyttet AddedSample checker vi, om der er flere samples end vi er i stand til at vise, og hvis det er tilfældet fjerner vi dem. Begge funktioner kalder Invalidate(), som vi stødte på tidligere.

GetLargestSample


Inden vi går i gang med den sidste del af artiklen, OnPaint() metoden, så mangler vi lige en metode til at finde den største værdi i vores _samples collection. Til trods for dens lille størrelse, bliver der brugt nogle avancerede teknikker, som vi er stødt på før, men som jeg nok skal forklare yderligere.

Fold kodeboks ind/udKode 


Den eneste interessante linje er nummer to. Her sker en hel del, som måske kan virke forvirrende, så lad mig forsøge at forklare det. Vi instantierer en instans af List<T> klassen, og giver den vores _samples collection som argument. Grunden til vi bruger en List<T> klasse her, er at vi gerne vil bruge dens ForEach(Action<T>;) metode. Men i stedet for at give den en instans af en Action<T> delegate, vælger vi at bruge en anonymous function. Anonymous functions er smarte, fordi at de gemmer den state de blev instantieret i. Så i dette tilfælde kan den tilgå den lokale variabel largest, læse fra den og skrive til den. Resten burde give sig selv.

OnPaint


Så er vi endelig nået frem til den sjove del, der hvor at vi rent faktisk skal til at tegne grafen. Start ud med at lave en OnPaint() metode som her:

Fold kodeboks ind/udKode 


Det der sker her, er at vi først kalder base-klassens OnPaint() metode, I tilfælde af at den har noget den vil udføre. Derefter gemmer vi Graphics objektet i en lokal variabel, den eneste grund hertil er nemmere tilgang. Herefter opretter vi to arrays, en med farver, som skal bruges til grafen, og en anden med floating point numbers. Dem vender vi tilbage til. Baggrunden skal jo også have en farve, og hvis I har kigget efter, vil I bemærke at baggrunden er den samme som graf farverne, bare i en mørkere udgave, så det er det første vi vil gøre. Tilføj følgende kode til OnPaint() metoden:

Fold kodeboks ind/udKode 


Det burde være rimeligt simpelt hvad der sker her, vi laver et array der er ligeså stort som colors, itererer over hver farve, og kalder ControlPaint.Dark() på farven. Den returnerer en mørkere udgave af farven man giver den som argument.
Nu er vi så klar til at tegne baggrunden:

Fold kodeboks ind/udKode 


Her støder vi på to nye klasser, LinearGradientBrush og ColorBlend. LinearGradientBrush bruges til, som navnet antyder, at lave lige overgange imellem farver. Da vi instantierer den, sender vi arealet på controllens klient område med, to tomme farver, og siger at overgangen skal foregå vertikalt. Grunden til vi bruger to tomme farver, er fordi vi definerer farverne via ColorBlend klassen. Det er en klasse som bruges til at definere flerfarvet overgange, som vi jo er ude efter i vores tilfælde. Og det er her vi bruger vores floating point array, positions. Værdierne i array bestemmer positionerne for de farver man tildeler til Colors propertien. 0 og 1 skal være repræsenteret i arrayet.
Hvis du ikke er bekendt med using(), så bruges den til automatisk at kalde Dispose() på klasser der implementerer IDisposable.

Hvis i tager et kig på billedet, kan i se at der er tegnet et gitter. Det er en forholdsvist simpel opgave:

Fold kodeboks ind/udKode 


Vi laver et Pen objekt, og giver den en hvid farve, som er gennemsigtig. Derefter sætter vi dens DashStyle til Dot, så vi tegner prikker, i stedet for en lige linje. Derefter løber vi igennem to for løkker, som begge kalder Graphics.DrawLine(), på hver deres akse.

For at sikre os, at vi ikke udfører nogle unødvendige, resourcekrævende handlinger, hvis der ikke er noget at tegne, indsætter vi følgende stump kode:

Fold kodeboks ind/udKode 


Al efterfølgende kode skal blive placeret inden i denne if blok.

Det første vi skal, er at beregne de værdier vi skal bruge, såsom hvor grafen starter, hvad den højeste er, hvis det ikke eksplisivt er sat osv. Det gør vi med følgende kode:

Fold kodeboks ind/udKode 


Det første vi gør er at beregne X-positionen hvor vores graf starter, ved at trække antallet af samples fra klient vidden. Derefter ser vi om en værdi er blevet sat, ved at se om largest stadig er NaN, og hvis den er, kalder vi GetLargestSample() metoden fra tidligere.
Delta værdien er den værdi, som de forskellige samples skal ganges med, for at få det rigtige størrelsesforhold i forhold til den største værdi, og opnås ved at dividere klient højden med den største værdi.

Fold kodeboks ind/udKode 


Dette er så koden der tegner grafen. De første linjer bør være lette at forstå, de er de samme som vi brugte til at tegne baggrunden, med den undtagelse af bruger colors arrayet denne gang. Derefter laver vi et Pen objekt som er sort, og lettere gennemsigtigt, som vi bruger til at tegne en skygge på grafen. Inde i løkken beregner vi højden på samplen vi skal tegne, og tjekker lige for en sikkerhedsskyld at vi ikke overskrider højden på controllens klient område. Så er det bare at beregne y positionen for samplen, og tegne skyggen med en offset på 1, på både x - og y-aksen. Til sidst tegner vi selve grafen ved at fylde et rektangel der er 1 pixel bred.

Tekst


Det eneste vi nu mangler er at tegne teksten i de øverste hjørner, dette gøres meget simpelt med følgende kode:

Fold kodeboks ind/udKode 


Det er her vi gør brug af _formatString, som vi deklarerede i starten af artiklen. Den er som default 0.0, hvilket bare får den til at skrive værdien ud som et kommatal. Det næste vi foretager os, er at beregne X-koordinatet for tekst strengen i øverste højre hjørne, ved at måle dens vidde. Den tredje parameter fortæller MeasureString() maksimumvidden for resultatet den returnerer.

De sidste linjer bør være til at forstå, vi kan lige løbe igennem dem hurtigt. Vi laver et Brush objekt, igen med en lettere gennemsigtig sort farve, for derefter at tegne skyggen på vores tekst, og så tegne teksten lidt forskudt til sidst.

Det var jo så det, kompiler det med C# 2, Beta 2 compileren, og så skulle det jo helst gerne virke. For den dovne, har jeg lavet en rar fil med koden og en test form, samt inkluderet en batch fil der kompilerer det for dig, som kan hentes på følgende addresse: http://www.udvikleren.dk/articlefiles/Graph.rar
God fornøjelse.

Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (5)

User
Bruger #5688 @ 23.05.05 16:11
Nu forstår jeg det meget bedre! Tak for hjælpen, Custom Controls i C#!
User
Bruger #2730 @ 24.05.05 10:01
Suveræn artikel, synes det er fedt der kommer nogle lidt mere avancerede artikler.... synes det er dejligt at se du gennemgår mange nye ting ved NET 2.0. Jeg skal helt sikkert til at lve graf komponenter nu :-) en 5'er herfra... ´jeg ser frem til flere artikler fra din side
User
Bruger #5779 @ 24.05.05 12:43
Udemærket
User
Bruger #3491 @ 14.06.05 15:32
Når baggrunds billedet er det samme hele tiden, ville det så ikke være smartere kun at tegne baggrunden en enkelt gang til et Image/Bitmap og så bare tegne baggrunden med DrawImage i OnPaint metoden?

(baggrunden skal selvfølgelig også gentegnes hvis størrelsen på kontrollen ændres)

Jeg ville forvente at det ville give en bedre performance, men jeg kan ikke sige om der er nogen bagsider ved det så det f.eks. giver en dårligere performance idet der først skal læses fra Bitmap'et.
User
Bruger #15341 @ 29.09.09 17:48
Hej Brian

Jeg sidder med Vstudio 2008 express edition, og forsøger at genskabe dit projekt, jeg har skrevet alt fra hånd og bruger den sidste nye .net framework 3.5, jeg har ikke siddet med microsoft studio produktet i mange år, da jeg har udviklet i Java det meste af tiden, jeg undrer bare om hvorfor jeg får en compile fejl når jeg kompilere dit eksempel i studio og ikke i dosprompt ?

Jeg får følgende fejl:

Error 1 Inconsistent accessibility: property type 'UdviklerenGrafControl.SamplesCollection' is less accessible than property 'Graph.Samples' C:\Users\David\AppData\Local\Temporary Projects\UdviklerenGrafControl\Graph.cs 65 30 UdviklerenGrafControl
Du skal være logget ind for at skrive en kommentar.
t