Jurgen Vandendriessche - jurgen.vandendriessche@vub.be
Thibaut Vandervelden- thvdveld@vub.be
Hieronder vinden jullie de oefeningen voor de WPO's informatica 1. De oefeningen zijn onderverdeeld in 4 categorieën: A, E en X. A zijn de algemene basisoefeningen en zijn het minimum dat verwacht wordt. De oefeningen van de E reeks zijn het niveau dat verwacht wordt op het examen. De X-oefeningen (Xtreme) zijn voor de echte durvers. Na elke les worden de opgeloste oefeningen via Canvas ingestuurd.
Tijdens de lessen zijn er 2 aangekondigde testen (elk 10%) gepland. Tijdens deze testen ondervragen wij de aangeleerde topics! Alle testen en examens worden hierbij gesloten boek afgenomen. Internet, theorieboek en opgeloste oefgeningen op papier of via een digitaal medium zullen niet toegelaten worden.
Tijdens de oenfeningensessies wordt er van je verwacht dat je actief meewerkt. Is dit niet het geval en verstoor je de groep of kom je te laat binnen, dan kan de assistent je de toegang tot die sessies weigeren.
Oefening baart kunst! Iedereen kan leren programmeren. Programmeren kan je alleen door veel te oefenen!
Als technologie(ver)kenner zal je regelmatig nieuwe stukken code schrijven. In je opleiding zal je je eerste programmeerstappen in Visual Studio zetten (C#). Deze omgeving wordt door veel ervaren en professionele programmeurs gebruikt en wordt heel actief door Microsoft onderhouden. Visual Studio kan je als student gratis bekomen via deze link. Zorg ervoor dat je de Community Edition downloadt en installeert. Deze is zeker voldoende om de lessen informatica te kunnen bijwonen. De minimale versie tijdens de lessen is Visual Studio 2019. Oudere versies (2017) zijn zeker OK. Instructies over de installatie van visual studio kan je hier vinden.
Zowel de opgave als de oplossingen blijven beschikbaar tot het examen. Op de dag van het examen woden deze even van de pagina gehaald, net zoals de screensavers.
In dit werkcollege worden de beginselen van het programmeren in C# aangeleerd. Hierbij wordt het gebruik van variabelen, de grafische interface en enkele bijhorende elementen aangetoond. Hiernaast wordt ook het gebruik van Visual Studio aangetoond. De variabelen die we zullen gebruiken kunnen onderverdeeld worden in 4 grote groepen:
Het gebruik van elk type variabele wordt hier geïllustreerd.
// declareer eerst de variabelen // dit gebeurt door de naam van de variabele te laten voorafgaan // door het type double value_double; float value_float; int value_int; char value_char; string value_string; bool value_bool; // zorg ervoor dat de variabelen en waarde bevatten value_double = 5.0; // merk de f: deze duidt aan dat het getal als float gezien moet worden value_float = 5.0f; value_int = 5; // geen komma! value_char = 'a'; // enkele aanhalingstekens value_string = "dit is een string"; // dubbele aanhalingstekens // voer enkele bewerkingen uit value_int = 5+8; // levert 13 op value_double = 5.6+3.9; // = 9.5 value_string = "test" + "bla" // = "testbla"
Visual Studio (C#) biedt de mogelijkheid aan om waarden van een formulier uit te lezen. Waarden naar een formulier schrijven is eveneens mogelijk. De gebruikelijke controls (textbox, label, enz.) handelen de in- en uitvoer van waarden af als tekenreeksen (string). Om de waarden van een formulier (control) te gebruiken, moet men eerst de conversie van een string het gepaste type uitvoeren.
// inlezen van een control (.Text) naar een variabele (double) // txtInput.Text is van het type string value_double = double.Parse(txtInput.Text); // equivalent value_double = Convert.ToDouble(txtInput.Text); // inlezen van een control (.Text) naar een variabele (float) value_float = float.Parse(txtInput.Text); // equivalent value_float = Convert.ToSingle(txtInput.Text); // inlezen van een control (.Text) naar een variabele (int) value_int = int.Parse(txtInput.Text); // equivalent value_int = Convert.ToInt32(txtInput.Text); // inlezen van een string gebeurt zonder conversie // string naar string heeft geen conversie nodig value_string = txtInput.Text;
Omgekeerd wordt een waarde steeds eerst naar een string omgezet voordat deze op het scherm weergegeven kan worden. Dit wordt geïllustreerd in onderstaand codefragment.
// wegschrijven van een variabele naar een control (.Text) // txtOutput.Text is van het type string txtOutput.Text = value_double.ToString(); // wegschrijven van een variabele naar een control (.Text) txtOutput.Text = value_float.ToString(); // wegschrijven van een variabele naar een control (.Text) txtOutput.Text = value_int.ToString(); // wegschrijven van een string // string naar string heeft geen conversie nodig txtOutput.Text = value_string;
Tijdens de lessen worden ook verkorte notatie's gebruikt. Deze werken wel enkel voor enkelvoudige datatypes.
txtOutput.Text = "" + value_double; txtOutput.Text = "" + value_float; txtOutput.Text = "" + value_int;
Tijdens dit werkcollege worden de principes van het debuggen en de conditionele if-structuren behandeld. Ook worden de eerste stappen van het debuggen tijdens dit WPO behandeld.
Debuggen is van essentieel belang om goede software te kunnen schrijven! Zorg ervoor dat je deze technieken dus goed onder de knie hebt!
De conditionele if-structuren - keuzestructuren - vormen een belangrijke mijlpaal tijdens het programmeren. De keuzestructuren laten toe om volgens opgelegde voorwaarden een bepaald stukje code al dan niet uit te voeren.
De meest eenvoudige if-structuur is de volgende:
if (condition) { //some code }
De voorwaarde (condition) kan om het even wat zijn, zolang het resultaat een boolean is. Voorbeelden van dergelijke voorwaarden worden hieronder opgelijst:
Het blokje code dat uitgevoerd wordt als aan de condities voldaan is, bevindt zich tussen het daaropvolgende openend accolade en het bijbehorende sluitende accolade. Indien er verschillende condities met bijbehorende resultaten vereist zijn, kan men de if-structuur met een else if- en else-blok (zie hieronder) combineren.
if (condition1) { //some code for condition 1 } else if (condition2) { //some code for condition 2 } else { //some code for else }
De uitleg is vrij gelijkaardig als hierboven. Als regel ga je ervan uit dat de condities met hoogste prioriteit of met hoogste eisen als eerste worden behandeld (condition1, dan condition2, enz.). Geneste if-structuren zijn eveneens mogelijk (zoals hieronder weergegeven). De werking ervan is gelijkaardig aan een enkelvoudige if-structuur.
if (condition) { if (subcondition1) { //some code } }
Ook hier zijn de varianten met else if en else mogelijk. Varianten op de gewone vergelijkende if-structuren bestaan en worden in de opgaves zelf vermeld (checkboxen en radiobuttons).
Oefeningen werkcollege 3 (extra)
Tijdens dit WPO wordt een eerste vorm van loopstructuren behandeld: de for loop. Loops bestaan uit 2 blokken: de code waarover de loop effect heeft, en de conditie(s) waaronder de loop plaatsvindt. De basis van de for-loop wordt hieronder weergegeven:
for (start;end;increment) { //some code }
Over het algmeen wordt er in de for-loop met een integer (index, vandaar i) gewerkt. In dat geval wordt de for-loop:
for (i=0;i<max;i++) { //some code }
Voor de start en eind conditie kunnen willekeurige waarden opgegeven worden. De increment (hier i++ of i=i+1) kan ook vrij bepaald worden (bv. i=i-1; i=i+5,...). De loop kan dus ook van hoog naar laag gaan.
De grootste bron van fouten bij het opstellen van dergelijke loops is de combinatie van het verkeerd kiezen van de start en eind conditie, tesamen met een verkeerde increment. Het is dus belangrijk om de correctheid
van de code na te gaan met de debugger.
Naast enkelvoudige for-loops is het ook mogelijk om for-loops te nesten, een if-structuur binnen een loop te schrijven, of omgekeerd.
In WPO 4 wordt het tekenen van allerhande geometrische figuren behandeld. Sommige elementen binnen de toolbox van Visual Studio (zoals een canvas) laten toe om grafische constructies zelf uit te tekenen. De voorbeelden die hieronder aangehaald worden, zijn gebaseerd op de canvas als tekenveld. Het tekenen van objecten verloopt steeds in stappen: het aanmeken van het object met alle eigenschappen en het achteraf toevoegen van dit object aan de canvas.
Hieronder wordt het aanmaken van een aantal object geïllustreerd. Als canvas wordt hier "cvs" gebruikt.
// create a line object Line ln = new Line(); // give the line the red color -> we use stroke for lines and contours ln.Stroke = new SolidColorBrush(Colors.Red); // give the line a given width of 5 pixels ln.StrokeThickness = 5; // set X and Y coordinates to the line ln.X1 = 0; ln.X2 = cvs.Width; ln.Y1 = cvs.Height; ln.Y2 = cvs.Height;
// create a rectangle object Rectangle rect = new Rectangle(); // set contour width and color rect.Stroke = new SolidColorBrush(Colors.GreenYellow); rect.StrokeThickness = 1; // position the rectangle against a canvas: // here 10,10 pixels from the upper left corner of the canvas Canvas.SetLeft(rect, 10); Canvas.SetTop(rect, 10); // set width and height of our rectangle rect.Height = 100; rect.Width = 100;
// create an ellips object Ellipse el = new Ellipse(); // set contour color and width el.Stroke = new SolidColorBrush(Color.FromArgb(255, 0, 200, 0)); el.StrokeThickness = 5; // set the ellips width and height, if both are equal we obtain a circle el.Width = 25; el.Height = 25; // position the rectangle against a canvas: // here 10,10 pixels from the upper left corner of the canvas Canvas.SetTop(el, 10); Canvas.SetLeft(el, 10);
// create a rectangle object Rectangle rect = new Rectangle(); // set area color rect.Fill = new SolidColorBrush(Colors.GreenYellow); // position the rectangle against a canvas: // here 10,10 pixels from the upper left corner of the canvas Canvas.SetLeft(rect, 10); Canvas.SetTop(rect, 10); // set width and height of our rectangle rect.Height = 100; rect.Width = 100;
// create an ellips object Ellipse el = new Ellipse(); // set area color el.Fill = new SolidColorBrush(Color.FromArgb(255, 0, 200, 0)); // set the ellips width and height, if both are equal we obtain a circle el.Width = 25; el.Height = 25; // position the rectangle against a canvas: // here 10,10 pixels from the upper left corner of the canvas Canvas.SetTop(el, 10); Canvas.SetLeft(el, 10);Nadat deze getekend zijn kunnen die toegevoegd worden aan de canvas. Het toevoegen gebeurt zoals hieronder weergegeven.
// add line to canvas cvs.Children.Add(ln); // add ellips to canvas cvs.Children.Add(el); // add rectangle to canvas cvs.Children.Add(rect);
cvs.Children.Clear();
Merk op dat alle objecten (met uitzondering van de lijn) steeds vanuit het linkerhoekpunt van het object getekend worden. Indien het object een hoogte en breedte heeft, dan zal het object vanuit dat punt naar links en naar onder getekend worden. Het assenstelsel van de canvas loopt zoals in de meeste andere programmeeromgevingen van links naar rechts en van boven naar onder toe.
// get a color from 4 gradients // Alpha = 255, Red = 0, Green = 255, Blue = 255 -> gives a color between blue and green // note that the first gradient specefies the transparancy: // 0 = fully transparant, 255 = fully opaque Color clr = Color.FromArgb(255,0,255,255); // we can also do it without transparancy clr = Color.FromRgb(0,255,255); // we can also choose a predefined color from WPF // note the difference between Color and Colors clr = Colors.Black; // assigning the color rect.Fill = new SolidColorBrush(clr); el.Fill = new SolidColorBrush(clr);
Het is dus volledig mogelijk om zelf een kleur samen te stellen uit 3 of 4 waarden. Indit geval geeft de eerste waarde de doorzichtigheid (alfa) gevolgd door de rode, groene en blauwe kleurcomponent. Alle componenten zijn hierbij begrensd tussen 0 en 255 (inclusief). Het datatype van de componenten is steeds een byte. Het is dus noodzakelijk om de gewenste typecast te doen indien men vanuit een integer of ander type vertrekt. Het is echter ook mogelijk om vanuit de vooropgestelde kleure van WPF te vertrekken.
while (condition) { //some code }
De conditie kan om het even wat zijn, zolang het eindresultaat een boolean is. De (syntax)regels voor de conditie zijn dezelfde als voor een gewone if-structuur. Een bijzondere while-loop is de oneindige loop, en wordt gevormd door:
while (true) //-- true is always true, so loop forever { //some code }
Een for-loop kan altijd naar een while-loop omgevormd worden. De for-loop is nl. een gespecialiseerde vorm van de while-loop. Hierbij moet men echter zelf de index (i) van de for-loop in een while-loop bijhouden. Omgekeerd geldt dat een while loop soms ook herschreven kan worden als een for-loop. Dit is echter niet altijd het geval. Een voorbeeld van een omzetting van een for-loop naar een while-loop wordt hieronder weergegeven.
// write 10 number ins a label for (int i=0;i<10;i++) { lblOutput.Content = lblOutput.Content + " " + i.ToString(); } // do the same with a while loop now int i=0; while(i<10) { lblOutput.Content = lblOutput.Content + " " + i.ToString(); // we need to do the increment of i ourselves now i++; }
Naast de while loop bestaat er ook de do-while loop. De do-while loop begint eerst met do en eindigt met de while conditie. De code tussen de do en de while zal herhaaldelijk uitgeoverd worden zolang de conditie geldig is. In onderstaand codefragment wordt de do-while gedemonstreerd.
do { //some code } while(condition==true);
Merk op dat een do-while loop altijd eindigt met een punt-komma. Het grootste verschil met een while-loop is echter het tijdstip wanneer de conditie nagekeken wordt. Bij een do-while wordt de conditie voor het eerst negekeken wanneer de loop minstens éénmaal al uitgevoerd is geweest. Bij een while-loop wordt eerst de conditie nagekeken alvorens de loop 1 keer is uitgevoerd. Het voorbeeldje van hierboven kan in dit geval ook omgevormd worden tot een do-while loop. Deze code zal altijd minstens 1 maal uitgevoerd worden.
int i=0; do { lblOutput.Content = lblOutput.Content + " " + i.ToString(); i++; } while(i<10);
Uiteraard is het mogelijk om, net zoals in een for-loop, een while-loop in een while-loop te nesten. Ook een for-loop in een while-loop, een if-structuur in een while-loop nesten is ook mogelijk. Omgekeerd kan een while-loop op zijn beurt ook genest worden in de andere structuren.
Naast de klassieke iteratiemethoden die we tot nu toe hebben gezien kunnen we in C# (WPF) ook gebruik maken van timers. Een timer is een object die op regelmatige tijdsintervallen een voorafbepaalde functie zal oproepen. Hoewel er nergens in de code een loop gedeclareerd hoeft te worden, zal de code die in die bepaalde functie geschreven staat regelmatig uitgevoerd worden. In WPF worden timers aangemaakt via de DispatcherTimer. Het aanmaken en gebruik van de timer wordt hieronder weergegeven. Merk op dat er een speciaal directief bovenaan de code toegevoegd moet worden. Dit wordt eveneens hieronder vermeld.
// put this after the last using... statement above in the code using System.Windows.Threading;
public partial class MainWindow : Window { // declare our timer as a global variable DispatcherTimer timer = new DispatcherTimer(); public MainWindow() { InitializeComponent(); } //-- event handler of a button on the form private void btnStart_click(object sender, RoutedEventArgs e) { // first clean our label of any content lblShow.Content = ""; // set function to call to our timer timer.Tick += timer_Tick; // give the interval at which the timer must call our function // in this case: 0 days, 0 hours, 0 minutes, 0 seconds and 200 milliseconds // frequency is thus 5Hz timer.Interval = new TimeSpan(0, 0, 0, 0, 200); // start our timer after the cleaning timer.Start(); } //-- the function which will be called by the timer //-- code inside this function will be repeated periodically //-- (here every 200ms) //-- until the timer is stopped: //-- timer.Stop(); private void timer_Tick(object sender, EventArgs e) { lblShow.Content = lblShow.Content + "X"; } }
Naast de timers zullen we vandaag een nieuwe beslissingsstructuur ontdekken: de switch case. Switch cases zijn naast de if-structuren een manier om het verloop van het programma te bepalen. De switch case structuur is een specialisatie op de if structuur. Bij de switch case is het enkel mogelijk om op gelijkheid te evalueren. Een tweede beperking op de if-structuur wordt gevormd doordat er enkel met voorafbepaalde waarden vergeleken kan worden! Ondanks deze beperkingen wordt de switch case gebruikt om de code meer leesbaar te maken (net zoals de for-loop). M.a.w. het is altijd mogelijk om een switch case om te vormen naar een if-structuur. Omgekeerd kan alleen als aan de beperkingen voldaan wordt. Een voorbeeld van een switch case wordt hieronder weergegeven.
//-- usage of a switch case structure //-- first decalre necessary variables int value = 10; //-- do not forget the brackets! switch (value) { //-- use "case" + value + ":" //-- note that a function can not be used //-- to compare against! case 0: //-- do something when value equals 0 //-- can be a statement of function //-- or else //-- end with the break statement break; case 5: //-- do something when value equals 5 //-- can be a statement of function //-- or else //-- end with the break statement break; case 10: //-- do something when value equals 10 //-- can be a statement of function //-- or else //-- end with the break statement break; default: //-- do something when previous values ar not met //-- can be a statement of function //-- or else //-- end with the break statement break; }
De variabele dat hier gebruikt is, is van het type integer. De programmeur kan hier in principe zelf het (enkelvoudige) type kiezen, maar de integer, string, char en enums worden het vaakst gebruikt. Functies kunnen niet gebruikt worden om mee te vergelijken binnen de switch case (dus case function), enkel een voorafbepaalde waarde!
Als laatste zal een introductie gegeven worden tot de combobox. Een combobox is een dropdown element dat toelaat om een bepaalde selectie te kiezen. Het gebruik van een combobox gebeurt door deze eerst in te vullen. nadien kan men, indien een waarde geselecteerd is, de waarde bepalen via de selected index eigenschap. Het geheel wordt hieronder weergegeven.
// Fill in the combobox with a few values (string only). cmbBox.Items.Add("Print in volgorde af"); cmbBox.Items.Add("Print in omgekeerde volgorde af"); cmbBox.Items.Add("Print randomwaarden af");
De juiste waarde uit de combobox opvissen kan als volgt:
// Code that resides in a button int index = cmbBox.SelectedIndex; switch (index) { case -1: // when no value has been selected (no mouse event on the combobox MessageBox.Show("Ongeldige keuze"); break; case 0: // first element of the itmes in a combobox MessageBox.Show("Print in volgorde af"); break; case 1: MessageBox.Show("Print in omgekeerde volgorde af"); break; case 2: MessageBox.Show("Print randomwaarden af"); break; default: // out ofrange for the combobox MessageBox.Show("Geen geldige waarde gekozen"); break; }
De index van het geselecteerde item van de combobox kan ook gebruikt worden elementen uit een array/lijst aan te spreken (zie later).
Tijdens dit WPO worden de beginselen van meervoudige variabelen gezien. Collecties vormen hierbij een belangrijke constructie om een reeks bij elkaar horende waarden onder dezelfde naam in te kapselen. Hoewel het lijkt dat het om een gewone variabele gaat, bevat deze variabele een bepaald aantal waarden. Onder de noemer van collecties zullen we tijdens de WPO's 2 structuren zien: de arrays en de lijsten.
Arrays kunnen zowel 1D, 2D en hoger dimensionaal zijn. Tijdens dit WPO zullen we ons enkel focussen op 1D arrays. Het gebruik van lijsten en arrays wordt in onderstaande codefragmenten weergegeven. Merk op dat net zoals gewone variabelen, arrays en lijsten van om het even welk datatype aangemaakt kunnen worden. Bij arrays en lijsten geldt dat de elementen aangesproken worden a.d.h.v. een index. Deze index start altijd bij 0 en eindigt bij het aantal elementen -1 (met een loop doorgaan zolang de index kleiner is dan de lengte dus).
// start with declaring an array. // make an array of 10 floating point values float[] float_array = new float[10]; // assign values to each element of the array // the index i goes from 0 to 9, having thus 10 elements // the value equals here i at position i, but can be anything else! for (int i=0;i<10;i++) { float_array[i] = i; } // read the values and print them in a textbox for (int i=0;i<float_array.Length;i++) { txtoutput.Text = txtoutput.Text + " " + float_array[i]; }
// Start with declaring a list of a given type. // when declaring and initializing, the type // is given between the brackets. Do not forget the // round brackets at the end before ';' List<float> float_list = new List<float>(); // Add the values into the list // the index i goes from 0 to 9, having thus 10 elements for (int i=0;i<10;i++) { float_list.Add(i); } // read the values and print them in a textbox // the values can be read as if the list were an array // go from zero up to the amount of elements in the list -1! for (int i=0;i<float_list.Count;i++) { txtoutput.Text = txtoutput.Text + " " + float_list[i]; }
In dit WPO zullen we de basis leren van gestructureerd programmeren. Tot hier toe werd alle code in de event-handler van een button geplaatst. Echter is deze techniek niet geschikt om grotere programma's te schrijven. In wat volgt zullen we de code opdelen in kleinere fragementen die we in procedures en functies zullen onderbrengen. In WPO 8 zullen we de procedures zien. In WPO 9 volgen de functies.
Procedures zijn methoden die toelaten om veelgebruikte code onder te brengen onder eenzelfde naam (bv. tekenRechthoek). Procedures bestaan uit 4 grote delen:
Hieronder wordt een voorbeeld van een procedure weergegeven.
// start a procedure with void // than after a space follows the name of the procedure // open the round brackets and write the arguments private void tekenDriehoek(int x1, int y1, int x2, int y2, int x3, int y3,Color kleur) { Line ln = new Line(); ln.Stroke = new SolidColorBrush(kleur); ln.StrokeThickness = 1; ln.X1 = x1; ln.Y1 = y1; ln.X2 = x2; ln.Y2 = y2; cvsDraw.Children.Add(ln); ln = new Line(); ln.Stroke = new SolidColorBrush(kleur); ln.StrokeThickness = 1; ln.X1 = x2; ln.Y1 = y2; ln.X2 = x3; ln.Y2 = y3; cvsDraw.Children.Add(ln); ln = new Line(); ln.Stroke = new SolidColorBrush(kleur); ln.StrokeThickness = 1; ln.X1 = x3; ln.Y1 = y3; ln.X2 = x1; ln.Y2 = y1; cvsDraw.Children.Add(ln); }
Bovenstaande procedure kan aangeroepen worden zoals afgebeeld in onderstaand codefragment.
// call the procedure as follows (draw 3 triangles) tekenDriehoek(0,0,100,100,0,100,Colors.Black); tekenDriehoek(200,200,250,200,100,100,Colors.Red); tekenDriehoek(500,600,250,250,400,400,Colors.Yellow);
Een procedure laat dus toe om gelijkaardige code te parametriseren en onder te brengen onder 1 naam (naam van de procedure). Procedures worden daarna vanop verschillende plaatsen in het programma opnieuw opgeroepen. Deze techniek laat toe om codeduplicatie te vermijden. Bovendien wordt op die manier gekende en geverifieerde code beschikbaar gemaakt in de rest van een programma terwijl dat fouten t.g.v. codeduplicatie vermeden worden.
In dit WPO worden de functies behandeld. Functies lijken enorm hard op procedures en om deze reden is de uitleg ervan grotendeels gelijklopend. Het grote verschil met procedures is dat een functie altijd een return statement heeft. Functies bestaan in tegenstelling tot procedures uit 5 grote delen:
Hieronder wordt een voorbeeld gegeven van een functie die de som van 2 getallen retourneert.
// write a function to sum two values private int Sum(int value1, int value2) { int s = 0; s = value1 + value2; // return the value, function will exit here even if // there is code behind the return statement return s; }
De functie kan ook aangeroepen worden. Merk op dat een functie een waarde teruggeeft. Dit betekent dus dat we deze waarde eerst in een variabele moeten opslaan. Dit gebeurt op dezelfde manier als het toewijzen van een waarde naar een variabele. Het verschil is echter dat de functie nu rechts van het gelijkheidsteken staat. De datatype van de variabele moet overeenkomen met de return-statement.
// first declare a variable for both input values int val1 = 5; int val2 = 10; // declare a variable for the return-statement int ret; ret = Sum(val1,val2); // gives 15
Als laatste zullen we meerdimensionale arrays behandelen. We zullen ons in dit WPO beperken tot 2D arrays. het gebruik van hogere dimensies verloopt op dezelde manier. In tegenstelling tot lineaire arrays, ttz. arrays met 1 enkele index, hebben meerdimensionale arrays verschillende indices. Hierbij is het dus heel belangrijk bij te houden welke dimensie welke informatie bijhoudt. Over het algemeen worden meerdimensionale arrays met meer dan 3 dimensies niet gebruikt.
Een voorbeeld van meerdimensionale arrays zijn de afbeeldingen in bitmap-formaat. In een afbeelding wordt de informatie in een 3D array bijgehouden. Dankzij de eerste 2 indices kan men de een pixel op een bepaalde x- en y-positie aanspreken. De 3de dimensie van de array geeft de kleur van die ene pixel weer (R,G,B).
Het gebruik van een meerdimensionale array verschilt in weze niet veel van die van een lineaire array. Het enige verschil is het kunnen aanpreken van de verschillende dimensies. Dit wordt weergegeven in onderstaand codefragment.
// create a 2D array of 100 elements, note the ',' inside the brackets double[,] arr = new double[10,10]; // fill our array with some values for (int i=0;i<arr.GetLength(0);i++) { for (int j=0;j<arr.GetLength(1);j++) { arr[i,j] = i+j; } }
Merk op dat we voor elke dimensie een aparte for loop gebruiken. Omdat we over een meerdimensionale array itereren, moeten we voor elke dimensie de lengte opvragen. Dit doen we door de methode "GetLength(int dimensie)" op te vragen. Deze methode retourneert hoeveel cellen er voor die dimensie aanwezig zijn. Om elke dimensie aan te spreken volstaat het om de indices te scheiden door komma's. Elke komma wijst op een extra dimensie.
Het doorgeven van van meerdimensionale arrays aan functies verloopt op een heel gelijkaardige manier als het doorgeven van een lineaire. Dit wordt in onderstaande codefragment geïllustreerd.
//------------------------------------------ private void processArray(double[,] arr) { ... } //------------------------------------------ private double[,] returnArray() { double[,] arr = new double[10,10]; ... return arr; } //------------------------------------------
Meerdimensionale arrays kunnen uiteraard aangemaakt worden van eender welk datatype.
Een laatste stap dat we hier willen behandelen zijn de null-waarden. Arrays kunnen in het algemeen ook gelijkgesteld worden aan null. Null in C# komt overeen met "het bestaat niet". Dit kan een handig mechanisme zijn indien men foutieve waarden uitkomt in een functie en dus niets wilt/kan retourneren. Een eenvoudige test op null laat toe om na te gaan of de array effectief geldige data bevat. Dit wordt hieronder weergegeven.
//------------------------------------------ private double[,] returnArray() { double[,] arr = new double[10,10]; // an error occured during calculations, so make the array null arr = null; return arr; } //------------------------------------------ private void callerFunction() { double[,] arr = returnArray(); if (arr!=null) { // do something with the array } else { MessageBox.Show("The array does not exist!"); } }
Een aantal elementaire concepten zijn intussen aan bod gekomen in de voorgaande WPO's. Om de basis van het programmeren te vervolledigen zullen we een aantal concepten introduceren die het leven van een (beginnende) programmeur soms kunnen vereenvoudigen:
De eerste 2 elementen worden in heel veel programmeertalen toegelaten. Vaak is er hier extra syntax nodig om dit te bewerkstelligen. Het concept achter het doorgeven van argumenten "by reference"/"by out" is dat men op die manier virtueel over meerdere return-statements kan beschikken. Een gewone functie retourneert maximaal 1 enkel waarde via de return-statement. Door argumeten bidirectioneel te maken kan men meer dan 1 return waarde bekomen. Het gebruik van by reference moet echter met de grootste voorzorg gedaan worden om zodoende ongewenste fouten te vermijden. Gebruik deze constructies dus enkel waar ze echt nodig zijn. Tussen "by reference" en "by out" is er een subtiel verschil. In het eerste geval moet het argument al een waarde toegewezen hebben gekregen voordat deze aan de functie meegegeven wordt. In het 2de geval is dit niet nodig (out van output). Het gebruik van "by reference"/"by out" wordt in onderstaande codefragment aangetoond.
//------------------------------------------ private void changevalue(ref int value) { // multiply our value by 2 value = value*2; // no return needed -> pass by reference } //------------------------------------------ private void callerFunction() { // we first need declare our variable and assign a value to it int val = 5; changevalue(ref val); MessageBox.Show(val.ToString()); }
//------------------------------------------ private void setValue(out int value) { value = 10; // no return needed -> pass by reference } //------------------------------------------ private void callerFunction() { // we first need declare our variable; // do not assign a value to it int val; changevalue(out val); MessageBox.Show(val.ToString()); }
In beide gevallen moeten de variabelen gedeclareerd zijn voordat de functie opgeroepen wordt. Het argument wordt steeds voorafgegaan door het woordje out of ref, en dit zelfs voordat de datatype geschreven wordt. Ook in het geval dat de functie opgeroepen wordt, moeten de woorden out of ref gebruikt worden zoals de voorbeelden het aangeven.
Indien men een enkelvoudige variabele "by reference" of "by out" wilt doorgeven, dan is men verplicht om de extra sleutelwoorden te gebruiken. Ook arrays en lijsten kunnen "by reference" doorgegeven worden. Hier moet men echter niets extra voor schrijven. Arrays en lijsten worden impliciet "by reference" doorgegeven.
//------------------------------------------ private void changeArray(int[] arr) { // arrays are implicitely passed by reference for (int i = 0; i < arr.Length; i++) { arr[i] = 2 * arr[i]; } } //------------------------------------------ private void callerFunction() { // we first need declare our array and assign values to it; int[] values = new int[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; changeArray(values); }
//------------------------------------------ private void changeList(List<int> arr) { for (int i = 0; i < arr.Count; i++) { arr[i] = 2 * arr[i]; } } //------------------------------------------ private void callerFunction() { //first declare and populate our list!! List<int> values = new List<int>(); for (int i = 0; i < 11; i++) values.Add(i); // now we can change the existing values in this list changeList(values); }
Het doorgeven van een array of lijst "by out" kan, maar wordt veel minder gebruikt. Dit wordt hier niet verder toegelicht.
Naast het doorgeven van parameters "by reference"/"by out" kan het soms nuttig zijn om een algoritme op recursieve wijze te implementeren. Recursie wordt bereikt doordat een functie zichzelf oproept. Dit oproepen kan rechtstreeks als onrechtstreeks gebeuren. IN het eerste geval roept de functie zichzelf als functie terug op. In het 2de geval roept de functie een andere functie op, die op zijn beurt de originele functie oproept. Een voorbeeld van recursie wordt hieronder weergegeven.
//------------------------------------------ private int factorial(int value) { if (value>=2) return value*factorial(value-1); // continue recursion here else return 1; // stop recursion here } //------------------------------------------ private void RunFactorial() { MessageBox.Show("Factorial of 10 = " + factorial(10)); }
Als laatste willen we nog de beginselen van het opdelen van code in verschillende codebestanden meegeven. Het zal de aandachtige student al zijn opgevallen dat de code die we schrijven al snel 100 lijnen of meer in beslag neemt. Ondanks de pogingen om de code in aparte functies onder te brengen zal dit alsnog tot een warboel uitgroeien. Om dit tegen te gaan kan men bij elkaar horende blokken code onderbrengen in een apart bestand (klasse). Men kan dan vanuit de hoofdcode de code uit die bestanden aanspreken via functies om zodoende het gewenste resultaat te bekomen. De functies die we hier zullen aanspreken zullen we voortaan methoden noemen. Voor de eenvoud worden hier enkel static-methoden gebruikt. Andere methoden bestaand ook, maar komen in de cursus van objectgeoriënteerd programmeren aan bod. Het gebruik van een dergelijke constructie wordt in onderstaande codefragmenten aangetoond met een aantal functies die het tekenen vergemakkelijken.
// write here a class with static methods which can be access from the main code // this code does reside in another file than the code of the main window public class Drawings { //--------------------------------------------------------------------------------------------------- public static void DrawLine(double x1, double y1, double x2, double y2, Color clr, Canvas cvsDraw) { // put here the code to draw a line on the canvas } //--------------------------------------------------------------------------------------------------- public static void DrawRectangle(double x1, double y1, double w, double h, Color clr, Canvas cvsDraw) { // put here the code to draw a rectangle on the canvas } //--------------------------------------------------------------------------------------------------- public static void DrawEllips(double x1, double y1, double w, double h, Color clr, Canvas cvsDraw) { // put here the code to draw an ellips on the canvas } //--------------------------------------------------------------------------------------------------- }
Het oproepen van deze functies vanuit een knop op de mainwindow geschiedt als volgt:
// this code is in the function of a button Drawings.DrawLine(10,10,100,100,Colors.Red,cvsDraw); Drawings.DrawRectangle(100,100,150,150,Colors.Blue,cvsDraw);
Merk op dat de methode-oproepen nu voorafgegaan worden door de naam van de klasse, nl. Drawings. Om de methoden aanspreekbaar te maken van buiten de klasse Drawings moeten deze public static staan. In andere gevallen zullen de methoden mogelijks niet aanspreekbaar (en dus niet te gebruiken) zijn.
In dit WPO zullen we de basis leggen van complexe datatypes. Complexe datatypes laten toe om verschillende variabelen die bij elkaar horen onder te brengen in eenzelfde datacontainer. Een voorbeeld hiervan is een punt in 2D die uit een x- en een y-component bestaat.
De declaratie van een complex datatype (struct) voor een 2D punt wordt in onderstaand codefragment getoond.
public struct Point2D { public double x; public double y; }
Van hieruit kan een nieuwe variabele aangemaakt worden. Dit gebeurt als volgt:
// declare a point from the struct and make it new (think of an object) Point2D p = new Point2D();
Waarden aan de subvariabelen toekennen verloopt op een gelijkaardige wijze als het toekennen van waarden aan gewone variabelen. Het enige verschil is dat men eerst de container moet aanspreken voordat men de subvariabele aanspreekt. Zie onderstaand codefragment.
p.x = 15; p.y = 150;
De waarden uit deze variabele halen gebeurt op een omgekeerde werkwijze, nl.:
double xpos = p.x; double ypos = p.y;
Merk op dat de variabele p hier steeds gedaclareerd en geïnstantieerd moet worden volgens het type van de struct. Alle velden (subvariabelen) van de struct worden ook public gedeclareerd zodat men er rechtstreeks via de code aankan.
Als laatste zullen we het gebruik van bestanden bespreken. In onderstaande codefragmenten wordt aangehaald hoe de verschillende operaties (read,write en append) tewerk gaan. De append-methode is een variant op het gewone schrijven, met als voornaamste verschil dat de append automatisch data achteraan het bestand toevoegt, terwijl het gewoon schrijven naar een bestand eerst het bestand volledig wist, en dan pas data in het bestand schrijft. De append-methode komt overeen met een += operatie bij een string.
private void btnRead_Click(object sender, EventArgs e) { // Open the file to read from. try { // generate string for the current file name // note the '@' at the beginning string path = @"E:\test.txt"; using (StreamReader sr = File.OpenText(path)) { string s = ""; // read line by line until the end of file (s==null) // has been reached while ((s = sr.ReadLine()) != null) { // write the current line in a textbox. txtOutput.Text += s + "\r\n"; } } } catch (Exception ex) { MessageBox.Show(ex.Message); } } // write data to a file private void btnWrite_Click(object sender, EventArgs e) { try { string path = @"E:\test.txt"; using (StreamWriter sw = File.CreateText(path)) { sw.Write(txtOutput.Text); } } catch (Exception ex) { MessageBox.Show(ex.Message); } } // append data to a file private void btnAppend_Click(object sender, EventArgs e) { try { string path = @"E:\test.txt"; using (StreamWriter sw = File.AppendText(path)) { sw.Write(txtOutput.Text); } } catch (Exception ex) { MessageBox.Show(ex.Message); } }
Merk op dat de strings die gebruikt worden als paden voor het filesysteem altijd voorafgegaan worden door een apenstaart. Paden worden in Windows vaak met het teken "\" aangeduid. Dit teken is echter ook een stringcommando in C# (\n, \r, \t). Om het stringcommando uit te schakelen wordt de string voorafgegaan door dit apenstaart. Een andere manier is om echter die commando uit te schakelen door een dubbele slash te gebruiken ("\\"). In dat geval wordt het apenstaart vooraan weggelaten. Hieronder wordt dezelfde string gebruikt op de 2 manieren.
// first way of decalring a string for path string path = @"E:\Bijlagen_WPO13\test.txt"; // second way to do it string path = "E:\\Bijlagen_WPO13\\test.txt";
Lezen, schrijven en data toevoegen zijn de basisbehandelingen die men kan uitvoeren op een bestand. Indien men veel data met een regelmatig patroon wenst op te slaan, kan het handig worden om een bepaalde opslagconventie te hanteren. Een voorbeeld van zo'n conventie is het CSV-formaat. Het CSV-formaat ("comma separated values") wordt gekenmerkt door het opslaan van data die gescheiden zijn door komma's. Door op elke regel maar een bepaalde set van waardes op te slaan, en dit zo op verschillende regels te hanteren, kan men grote datasets aanmaken. Zo bijvoorbeeld kunnen meetresultaten opgeslagen worden. Het CSV-formaat wordt als volgt toegepast: de eerste rij bevat de namen van de kolommen, waarij de namen gescheiden zijn door komma's. De volgende rijen bevatten de data (numerieke data, tekst, enz.), eveneens gescheiden door komma's.
Het lezen uit zo'n bestand verschilt niet veel van het lezen uit een random tekstbestand. Lees eerst regel per regel in. Als dit lukt, is de volgende stap het afzonderen van de data van de komma's. Hiervoor kan je de methode "Stringsplit" toepassen (met de komma als delimiter). Deze methode retourneert een aantal strings terug, waarbij het aantal overeenkomt met het aantal data per regel. Nu is het de moment om al deze afzonderlijke strings naar het juiste datatype om te zetten (int, float, double, enz.). De volgorde van de data (plaats in de regel) is hier zeer belangrijk, en wordt bepaald door de kolomnaam ("Caption").
Het schrijven naar een CSV-bestand kan op een eenvoudige wijze plaatsvinden. Voor elke regel maak je een lege string aan. Deze vul je op door alternerend de juiste waarde van van huidige kolom erin te schrijven, tesamen met een komma. Elke regel (string) sluit je af met een newline! Deze newline wordt bij het lezen gebruikt als einde voor de "Readln" methode, en duidt ook aan dat een gegeven dataset afgesloten is. Via de StreamWriter kan je de string toevoegen aan het bestand. Vergeet niet: de eerste regel bevat de kolomnamen!
Als voorbeeld wordt het uitlezen van een CSV-bestand gedemonstreerd. De code berekent het gemiddelde van alle waarden op eenzelfde lijn die door een kommas gescheiden worden.
// Open the file to read from. try { // generate string for the current file name // note the '@' at the beginning string path = @"E:\Bijlagen_WPO13\test.txt"; using (StreamReader sr = File.OpenText(path)) { string s = ""; int linenumber = 1; // read line by line until the end of file (s==null) // has been reached if ((s = sr.ReadLine())!=null) { // read caption here } while ((s = sr.ReadLine()) != null) { // split the values by ',' csv_values = s.Split(','); int sum=0; for (int i=0;i<csv_values.Length;i++) { int value = int.Parse(csv_values[i]); sum+=value; } int average = sum/csv_values.Length; txtOutput.Text += "Average on line " + linenumber + " = " + average.ToString() + "\n"; linenumber++; } } } catch (Exception ex) { MessageBox.Show(ex.Message); }
Tijdens deze les wordt een finale demo getoond waarin duidelijk wordt gemaakt hoe de samenhang van de verschillende aangeleerde programmatorische onderdelen tot een finaal programma kan leiden.
Daarom wordt tijdens deze les "The matrix" als screensaver-programma gedemonstreerd.
De finale oplossing (Visual Studio solution) kan hier gedownload worden. De map bevat de Visual Studio solution tesamen met de gepaste font ("matrix.ttf"). Het volstaat om de font te installeren in Windows om het effect in de screensaver te zien. In de submap "the_matrix\the_matrix\bin\Debug" kan het uivoerbaar bestand "the_matrix.exe" gevonden worden. Dit bestand kan ingesteld worden als echte Windows screensaver door de .exe extensie eerst te veranderen naar .scr. Een rechtermuisklik op dit bestand gevolgd door een install laat toe om dit ook effectief te doen. Via deze link kan meer informatie gevonden worden.
De screensaver die gemaakt werd in 2019 (vuurwerk) kan je hier vinden